from __future__ import annotations

import asyncio
from typing import Any, Dict, List

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

from src.modules.carrinhos.schemas import (
    CarrinhoCreateRequest,
    CarrinhoOut,
    CarrinhoUpdateRequest,
)

_ALLOWED_SORT_FIELDS = {"codigoInterno", "$createdAt", "$updatedAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20
_SEARCH_FIELDS = ("codigoInterno", "usuarios")


class CarrinhosService:
    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(field, q_value) for field in _SEARCH_FIELDS])
                    )
                    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, carrinho_id: str) -> CarrinhoOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                carrinho_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Carrinho não encontrado.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        return self._map_document(document)

    async def create(self, payload: CarrinhoCreateRequest) -> CarrinhoOut:
        data = self._sanitize_payload(
            payload.data.model_dump(exclude_unset=True)
        )

        document_identifier = self._resolve_document_id(payload.documentId)

        try:
            document = await self._run(
                self._databases.create_document,
                self._database_id,
                self._collection_id,
                document_id=document_identifier,
                data=data,
            )
        except AppwriteException as 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 self._map_document(document)

    async def update(self, carrinho_id: str, payload: CarrinhoUpdateRequest) -> CarrinhoOut:
        updates = payload.data.model_dump(exclude_unset=True)
        if not updates:
            return await self.get(carrinho_id)

        updates = self._sanitize_payload(updates)

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                carrinho_id,
                data=updates,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Carrinho não encontrado.") 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 self._map_document(document)

    @staticmethod
    def _resolve_document_id(document_id: str | None) -> str:
        if document_id is None:
            return ID.unique()
        if not isinstance(document_id, str):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="documentId inválido.",
            )
        trimmed = document_id.strip()
        if not trimmed:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="documentId não pode ser vazio.",
            )
        if trimmed == "unique()":
            return ID.unique()
        return trimmed

    async def delete(self, carrinho_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                carrinho_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Carrinho não encontrado.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    def _sanitize_payload(self, data: Dict[str, Any]) -> Dict[str, Any]:
        if "codigoInterno" in data and data["codigoInterno"] is not None:
            value = data["codigoInterno"]
            if isinstance(value, str):
                trimmed = value.strip()
                if not trimmed:
                    raise HTTPException(
                        status_code=status.HTTP_400_BAD_REQUEST,
                        detail="codigoInterno não pode ser vazio.",
                    )
                data["codigoInterno"] = trimmed

        if "usuarios" in data and data["usuarios"] is not None:
            relation_id = _first_relation_id(data["usuarios"])
            if relation_id is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="usuarios não pode ser vazio.",
                )
            data["usuarios"] = relation_id
        for list_field in ("itens", "labNome"):
            if list_field in data and data[list_field] is not None:
                data[list_field] = [str(item) for item in data[list_field]]
        if "quantidade" in data and data["quantidade"] is not None:
            try:
                data["quantidade"] = [int(item) for item in data["quantidade"]]
            except (TypeError, ValueError):
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="quantidade deve conter números inteiros.",
                )

        if "valorTotal" in data and data["valorTotal"] is not None:
            data["valorTotal"] = [float(item) for item in data["valorTotal"]]
        return data

    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)

    @staticmethod
    def _map_document(document: Dict[str, Any]) -> CarrinhoOut:
        return CarrinhoOut(
            id=document.get("$id", ""),
            itens=list(document.get("itens") or []),
            codigoInterno=document.get("codigoInterno"),
            labNome=list(document.get("labNome") or []),
            quantidade=[int(item) for item in (document.get("quantidade") or [])],
            valorTotal=[float(item) for item in (document.get("valorTotal") or [])],
            usuarios=_first_relation_id(document.get("usuarios")),
            createdAt=document.get("$createdAt", ""),
            updatedAt=document.get("$updatedAt", ""),
        )


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    needle = query.lower()
    for field in _SEARCH_FIELDS:
        value = document.get(field)
        if isinstance(value, str) and needle in value.lower():
            return True
        relation_id = _first_relation_id(value)
        if relation_id and needle in relation_id.lower():
            return True
    return False


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


def _first_relation_id(value: Any) -> str | None:
    if value is None:
        return None
    if isinstance(value, str):
        trimmed = value.strip()
        return trimmed or None
    if isinstance(value, dict):
        for key in ("$id", "id"):
            candidate = value.get(key)
            if isinstance(candidate, str):
                trimmed = candidate.strip()
                if trimmed:
                    return trimmed
        return None
    if isinstance(value, list):
        for item in value:
            candidate = _first_relation_id(item)
            if candidate:
                return candidate
        return None
    return None
