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.enderecos.schemas import EnderecoCreate, EnderecoOut, EnderecoUpdate

_ALLOWED_SORT_FIELDS = {"titulo", "cep", "cidade", "estado", "$createdAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


class EnderecosService:
    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.or_(
                            [
                                Query.search("titulo", q_value),
                                Query.search("cep", q_value),
                                Query.search("cidade", q_value),
                                Query.search("estado", q_value),
                                Query.search("bairro", 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, endereco_id: str) -> EnderecoOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                endereco_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Endereco not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

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

    async def create(self, payload: EnderecoCreate) -> EnderecoOut:
        data = payload.model_dump(exclude_unset=True)
        sanitized = _sanitize_payload(data)

        try:
            document = await self._run(
                self._databases.create_document,
                self._database_id,
                self._collection_id,
                document_id="unique()",
                data=sanitized,
            )
        except AppwriteException as exc:
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Endereco 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 EnderecoOut.model_validate(self._map_document(document))

    async def update(self, endereco_id: str, payload: EnderecoUpdate) -> EnderecoOut:
        updates = payload.model_dump(exclude_unset=True)
        sanitized = _sanitize_payload(updates)

        if not sanitized:
            return await self.get(endereco_id)

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                endereco_id,
                data=sanitized,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Endereco not found.") from exc
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Endereco 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 EnderecoOut.model_validate(self._map_document(document))

    async def delete(self, endereco_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                endereco_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Endereco 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", ""),
            "titulo": document.get("titulo"),
            "cep": document.get("cep"),
            "logradouro": document.get("logradouro"),
            "numero": document.get("numero"),
            "complemento": document.get("complemento"),
            "cidade": document.get("cidade"),
            "estado": document.get("estado"),
            "pais": document.get("pais"),
            "latitude": document.get("latitude"),
            "longitude": document.get("longitude"),
            "bairro": document.get("bairro"),
            "createdAt": document.get("$createdAt"),
            "updatedAt": document.get("$updatedAt"),
        }


def _sanitize_payload(data: Dict[str, Any]) -> Dict[str, Any]:
    sanitized: Dict[str, Any] = {}
    for key, value in data.items():
        if value is None:
            continue
        if isinstance(value, str):
            sanitized[key] = value.strip()
        else:
            sanitized[key] = value
    return sanitized


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    needle = query.lower()
    for key in ("titulo", "cep", "cidade", "estado", "bairro"):
        value = document.get(key)
        if isinstance(value, str) and needle in value.lower():
            return True
    return False


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