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
import httpx

from src.core.appwrite_endpoint import normalize_appwrite_endpoint
from src.core.appwrite_session import normalize_appwrite_session_cookie
from src.modules.pagamentos.schemas import PagamentoCreate, PagamentoOut, PagamentoUpdate


_ALLOWED_SORT_FIELDS = {"status", "metodo", "valor", "$createdAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20
_SEARCH_FIELDS = ("metodo",)


class PagamentosService:
    def __init__(
        self,
        databases: Databases,
        database_id: str,
        collection_id: str,
        *,
        project_id: str | None = None,
        endpoint: str | None = None,
        session_cookie: str | None = None,
        api_key: str | None = None,
    ) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = collection_id
        self._project_id = project_id.strip() if isinstance(project_id, str) else None
        self._endpoint = normalize_appwrite_endpoint(endpoint, database_id)
        self._session_cookie = normalize_appwrite_session_cookie(self._project_id, session_cookie)
        self._api_key = api_key.strip() if isinstance(api_key, str) else None

    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

        normalized_order = order.lower() if order else None

        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 normalized_order and normalized_order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")
            if normalized_order == "desc":
                queries.append(Query.order_desc(sort))
            else:
                queries.append(Query.order_asc(sort))
        elif normalized_order and normalized_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, normalized_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, pagamento_id: str) -> PagamentoOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                pagamento_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found.") 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: PagamentoCreate) -> PagamentoOut:
        data = self._sanitize_payload(payload.model_dump(exclude_unset=True))

        if self._project_id and self._endpoint and (self._session_cookie or self._api_key):
            return await self._create_via_http(data)

        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 == 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 _create_via_http(self, data: Dict[str, Any]) -> PagamentoOut:
        base_endpoint = (self._endpoint or "").rstrip("/")
        url = (
            f"{base_endpoint}/databases/{self._database_id}/collections/{self._collection_id}/documents"
        )
        headers = {
            "X-Appwrite-Project": self._project_id or "",
            "Accept": "application/json",
            "Content-Type": "application/json",
        }
        if self._session_cookie:
            headers["Cookie"] = self._session_cookie
        if self._api_key:
            headers["X-Appwrite-Key"] = self._api_key

        payload = {"documentId": "unique()", "data": data}

        try:
            async with httpx.AsyncClient(timeout=30.0) as client:
                response = await client.post(url, headers=headers, json=payload)
        except httpx.HTTPError as exc:
            raise HTTPException(
                status_code=status.HTTP_502_BAD_GATEWAY,
                detail="Falha ao se comunicar com Appwrite.",
            ) from exc

        if response.status_code >= 400:
            detail: Any = None
            try:
                body = response.json()
                if isinstance(body, dict):
                    detail = body.get("message") or body.get("error") or body.get("errors") or body
                else:
                    detail = body
            except ValueError:
                detail = response.text or "Erro desconhecido no Appwrite."
            raise HTTPException(status_code=response.status_code, detail=detail)

        try:
            document = response.json()
        except ValueError as exc:  # pragma: no cover - defensive
            raise HTTPException(
                status_code=status.HTTP_502_BAD_GATEWAY,
                detail="Resposta inválida do Appwrite.",
            ) from exc

        if not isinstance(document, dict):
            raise HTTPException(
                status_code=status.HTTP_502_BAD_GATEWAY,
                detail="Resposta inválida do Appwrite.",
            )

        return self._map_document(document)

    async def update(self, pagamento_id: str, payload: PagamentoUpdate) -> PagamentoOut:
        updates = payload.model_dump(exclude_unset=True)
        if not updates:
            return await self.get(pagamento_id)

        updates = self._sanitize_payload(updates)
        if not updates:
            return await self.get(pagamento_id)

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                pagamento_id,
                data=updates,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found.") 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)

    async def delete(self, pagamento_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                pagamento_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found.") 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]:
        sanitized: Dict[str, Any] = {}
        for key, value in data.items():
            if value is None:
                continue
            if isinstance(value, list) and not value:
                continue
            if isinstance(value, str):
                value = value.strip()
                if not value:
                    raise HTTPException(
                        status_code=status.HTTP_400_BAD_REQUEST,
                        detail=f"{key} cannot be empty.",
                    )
            sanitized[key] = value
        return sanitized

    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]) -> PagamentoOut:
        return PagamentoOut(
            id=str(document.get("$id", "")),
            status=str(document.get("status")),
            metodo=str(document.get("metodo")),
            valor=(
                float(document.get("valor"))
                if document.get("valor") is not None
                else None
            ),
            pedidos=_extract_relation_id(document.get("pedidos")),
            createdAt=str(document.get("$createdAt", "")),
            updatedAt=str(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
    return False


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


def _extract_relation_id(value: Any) -> str | None:
    if value is None:
        return None

    if isinstance(value, str):
        stripped = value.strip()
        return stripped or None

    if isinstance(value, dict):
        relation_id = value.get("$id") or value.get("id")
        if isinstance(relation_id, str):
            relation_id = relation_id.strip()
            if relation_id:
                return relation_id
        return str(value)

    if isinstance(value, list):
        if not value:
            return None
        first = value[0]
        if isinstance(first, dict):
            relation_id = first.get("$id") or first.get("id")
            if isinstance(relation_id, str):
                relation_id = relation_id.strip()
                if relation_id:
                    return relation_id
        return str(first)

    return str(value)
