from __future__ import annotations

import asyncio
from typing import Any, Dict, List

from appwrite.exception import AppwriteException
from appwrite.query import Query
from appwrite.services.databases import Databases
from fastapi import HTTPException, status

from src.modules.subcategorias.schemas import SubcategoriaCreate, SubcategoriaOut, SubcategoriaUpdate

_ALLOWED_SORT_FIELDS = {"nome", "$createdAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


class SubcategoriasService:
    def __init__(self, databases: Databases, database_id: str, collection_id: str) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = collection_id

    async def _run(self, func: Any, *args: Any, **kwargs: Any) -> Any:
        return await asyncio.to_thread(func, *args, **kwargs)

    async def list(
        self,
        page: int,
        limit: int | None,
        q: str | None,
        sort: str | None,
        order: str | None,
    ) -> Dict[str, Any]:
        limit_value = limit or _DEFAULT_LIMIT
        offset = (page - 1) * limit_value

        queries: List[Any] = [Query.limit(limit_value), Query.offset(offset)]

        if sort:
            if sort not in _ALLOWED_SORT_FIELDS:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid sort field.")
            if order and order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")
            if order == "desc":
                queries.append(Query.order_desc(sort))
            else:
                queries.append(Query.order_asc(sort))
        elif order and order not in _ALLOWED_ORDER_VALUES:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")

        search_performed = False
        if q:
            q_value = q.strip()
            if q_value:
                try:
                    queries.append(Query.search("nome", q_value))
                    search_performed = True
                except AttributeError:
                    search_performed = False

        try:
            response = await self._run(
                self._databases.list_documents,
                self._database_id,
                self._collection_id,
                queries=queries,
            )
        except AppwriteException as exc:
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        documents: List[Dict[str, Any]] = response.get("documents", [])
        total = response.get("total", len(documents))

        if q and not search_performed:
            documents, total = await self._fallback_search(q, sort, order or "asc")
            documents = documents[offset : offset + limit_value]

        items = [self._map_document(doc) for doc in documents]

        return {
            "page": page,
            "limit": limit_value,
            "total": total,
            "items": items,
        }

    async def get(self, subcategoria_id: str) -> SubcategoriaOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                subcategoria_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subcategoria not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        return SubcategoriaOut.model_validate(self._map_document(document))

    async def create(self, payload: SubcategoriaCreate) -> SubcategoriaOut:
        data = payload.model_dump(exclude_unset=True)
        nome_value = data.get("nome")
        if isinstance(nome_value, str):
            nome_stripped = nome_value.strip()
            if not nome_stripped:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Nome cannot be empty.")
            data["nome"] = nome_stripped
        categoria_id = data.pop("categoriaId", None)
        if categoria_id is not None:
            data["categoria"] = categoria_id

        try:
            document = await self._run(
                self._databases.create_document,
                self._database_id,
                self._collection_id,
                document_id="unique()",
                data=data,
            )
        except AppwriteException as exc:
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Subcategoria already exists.",
                ) from exc
            if exc.code == 400:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=exc.message) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        return SubcategoriaOut.model_validate(self._map_document(document))

    async def update(self, subcategoria_id: str, payload: SubcategoriaUpdate) -> SubcategoriaOut:
        updates = payload.model_dump(exclude_unset=True)
        if not updates:
            return await self.get(subcategoria_id)

        if "nome" in updates and updates["nome"] is not None:
            nome_value = updates["nome"].strip()
            if not nome_value:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Nome cannot be empty.")
            updates["nome"] = nome_value

        if "categoriaId" in updates:
            categoria_id = updates.pop("categoriaId")
            updates["categoria"] = categoria_id

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                subcategoria_id,
                data=updates,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subcategoria not found.") from exc
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Subcategoria already exists.",
                ) from exc
            if exc.code == 400:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=exc.message) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        return SubcategoriaOut.model_validate(self._map_document(document))

    async def delete(self, subcategoria_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                subcategoria_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subcategoria not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    async def _fallback_search(
        self,
        q: str,
        sort: str | None,
        order: str,
    ) -> tuple[List[Dict[str, Any]], int]:
        try:
            response = await self._run(
                self._databases.list_documents,
                self._database_id,
                self._collection_id,
                queries=[Query.limit(100)],
            )
        except AppwriteException as exc:
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        documents: List[Dict[str, Any]] = response.get("documents", [])
        filtered = [doc for doc in documents if _matches_query(doc, q)]

        if sort:
            reverse = order == "desc"
            filtered.sort(key=lambda doc: _sort_key(doc, sort), reverse=reverse)

        return filtered, len(filtered)

    def _map_document(self, document: Dict[str, Any]) -> Dict[str, Any]:
        return {
            "id": document.get("$id", ""),
            "nome": document.get("nome", ""),
            "categoriaId": (document.get("categoria") or {}).get("$id")
            if isinstance(document.get("categoria"), dict)
            else document.get("categoriaId"),
            "createdAt": document.get("$createdAt"),
            "updatedAt": document.get("$updatedAt"),
        }


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    needle = query.lower()
    campo = document.get("nome")
    return isinstance(campo, str) and needle in campo.lower()


def _sort_key(document: Dict[str, Any], sort: str) -> Any:
    if sort == "$createdAt":
        return document.get("$createdAt")
    return document.get(sort)
