from __future__ import annotations

import asyncio
from typing import Any, Dict, List, NoReturn

from appwrite.exception import AppwriteException
from appwrite.query import Query
from appwrite.services.databases import Databases
from appwrite.id import ID
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.pedidos.schemas import (
    PedidoCreateRequest,
    PedidoOut,
    PedidoStatusUpdate,
    PedidoUpdate,
)

_ALLOWED_SORT_FIELDS = {
    "status",
    "valor-total",
    "valorTotal",
    "valor-produtos",
    "valorProdutos",
    "valor-frete",
    "valorFrete",
    "$createdAt",
    "$updatedAt",
}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_ALLOWED_STATUS = {"pendente", "aprovado", "entregue", "cancelado"}
_SEARCH_FIELDS = ("status", "pagamentos", "usuarios", "carrinhos")
_DEFAULT_LIMIT = 20

_APPWRITE_AUTH_ERROR_MESSAGE = (
    "Falha na autenticação com Appwrite. Verifique as credenciais, permissões e sessão "
    "configuradas para o BFF."
)


class PedidosService:
    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,
        usuario_id: 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)]
        effective_order = order.lower() if isinstance(order, str) else None
        default_sort_field = "$createdAt"

        usuario_filter: str | None = None
        if usuario_id is not None:
            if not isinstance(usuario_id, str):
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="usuarioId inválido.",
                )
            trimmed_usuario = usuario_id.strip()
            if not trimmed_usuario:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="usuarioId não pode ser vazio.",
                )
            queries.append(Query.equal("usuarios", trimmed_usuario))
            usuario_filter = trimmed_usuario

        if sort:
            if sort not in _ALLOWED_SORT_FIELDS:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid sort field.")
            if effective_order and effective_order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")
            sort_field = _map_sort_field(sort)
            if effective_order == "desc":
                queries.append(Query.order_desc(sort_field))
            else:
                queries.append(Query.order_asc(sort_field))
                if effective_order is None:
                    effective_order = "asc"
        else:
            if effective_order and effective_order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")
            sort_field = default_sort_field
            if effective_order == "asc":
                queries.append(Query.order_asc(sort_field))
            else:
                queries.append(Query.order_desc(sort_field))
                if effective_order is None:
                    effective_order = "desc"

        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:
            self._handle_appwrite_exception(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,
                effective_order or "desc",
                usuario_filter,
            )
            documents = documents[offset : offset + limit_value]
        elif usuario_filter:
            documents = [doc for doc in documents if _matches_usuario(doc, usuario_filter)]
            total = response.get("total", len(documents))

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

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

    async def get(self, pedido_id: str) -> PedidoOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                pedido_id,
            )
        except AppwriteException as exc:
            self._handle_appwrite_exception(
                exc,
                not_found_detail="Pedido não encontrado.",
            )

        return self._map_document(document)

    async def create(self, payload: PedidoCreateRequest) -> PedidoOut:
        data = self._sanitize_payload(
            payload.data.model_dump(by_alias=True, exclude_unset=True, exclude_none=True)
        )

        document_identifier = self._resolve_document_id(payload.document_id)

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

        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:
            self._handle_appwrite_exception(exc)

        return self._map_document(document)

    async def _create_via_http(self, data: Dict[str, Any], document_id: str) -> PedidoOut:
        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": document_id, "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:
            if response.status_code == status.HTTP_401_UNAUTHORIZED:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail=_APPWRITE_AUTH_ERROR_MESSAGE,
                )
            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."
            if response.status_code == status.HTTP_400_BAD_REQUEST:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
            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, pedido_id: str, payload: PedidoUpdate) -> PedidoOut:
        updates = payload.model_dump(by_alias=True, exclude_unset=True, exclude_none=True)
        if not updates:
            return await self.get(pedido_id)

        updates = self._sanitize_payload(updates)

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                pedido_id,
                data=updates,
            )
        except AppwriteException as exc:
            self._handle_appwrite_exception(
                exc,
                not_found_detail="Pedido não encontrado.",
            )

        return self._map_document(document)

    async def update_status(self, pedido_id: str, payload: PedidoStatusUpdate) -> PedidoOut:
        status_value = self._normalize_status(payload.status)

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                pedido_id,
                data={"status": status_value},
            )
        except AppwriteException as exc:
            self._handle_appwrite_exception(
                exc,
                not_found_detail="Pedido não encontrado.",
            )

        return self._map_document(document)

    async def delete(self, pedido_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                pedido_id,
            )
        except AppwriteException as exc:
            self._handle_appwrite_exception(
                exc,
                not_found_detail="Pedido não encontrado.",
            )

    @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

    def _sanitize_payload(self, data: Dict[str, Any]) -> Dict[str, Any]:
        if "status" in data and data["status"] is not None:
            data["status"] = self._normalize_status(str(data["status"]))
        for field in ("pagamentos", "carrinhos", "usuarios", "faturas"):
            if field not in data or data[field] is None:
                continue
            relation_value = _sanitize_relation_value(field, data[field])
            data[field] = relation_value
        for list_field in ("itens",):
            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 "valor-total" in data and data["valor-total"] is not None:
            data["valor-total"] = float(data["valor-total"])
        return data

    def _normalize_status(self, value: str) -> str:
        normalized = value.strip().lower()
        if normalized not in _ALLOWED_STATUS:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Status inválido.",
            )
        return normalized

    async def _fallback_search(
        self,
        q: str,
        sort: str | None,
        order: str,
        usuario_id: str | None,
    ) -> 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:
            self._handle_appwrite_exception(exc)

        documents: List[Dict[str, Any]] = response.get("documents", [])
        filtered = [
            doc
            for doc in documents
            if _matches_query(doc, q) and (usuario_id is None or _matches_usuario(doc, usuario_id))
        ]

        if sort:
            mapped_sort = _map_sort_field(sort)
            reverse = order == "desc"
            filtered.sort(key=lambda doc: _sort_key(doc, mapped_sort), reverse=reverse)
        else:
            reverse = order != "asc"
            filtered.sort(
                key=lambda doc: _sort_key(doc, "$createdAt"),
                reverse=reverse,
            )

        return filtered, len(filtered)

    @staticmethod
    def _map_document(document: Dict[str, Any]) -> PedidoOut:
        return PedidoOut(
            id=document.get("$id", ""),
            status=str(document.get("status", "pendente")),
            valor_total=(
                float(document.get("valor-total"))
                if document.get("valor-total") is not None
                else None
            ),
            valor_produtos=(
                float(document.get("valor-produtos"))
                if document.get("valor-produtos") is not None
                else None
            ),
            valor_frete=(
                float(document.get("valor-frete"))
                if document.get("valor-frete") is not None
                else None
            ),
            itens=[str(item) for item in (document.get("itens") or [])],
            quantidade=[int(item) for item in (document.get("quantidade") or [])],
            pagamentos=_extract_relation_id(document.get("pagamentos")),
            carrinhos=_extract_relation_id(document.get("carrinhos")),
            usuarios=_extract_relation_id(document.get("usuarios")),
            faturas=_extract_relation_id(document.get("faturas")),
            created_at=document.get("$createdAt", ""),
            updated_at=document.get("$updatedAt", ""),
        )

    def _handle_appwrite_exception(
        self,
        exc: AppwriteException,
        *,
        not_found_detail: str | None = None,
    ) -> NoReturn:
        if exc.code == status.HTTP_401_UNAUTHORIZED:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=_APPWRITE_AUTH_ERROR_MESSAGE,
            ) from exc
        if exc.code == status.HTTP_404_NOT_FOUND and not_found_detail is not None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=not_found_detail) from exc
        if exc.code == status.HTTP_400_BAD_REQUEST:
            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


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 _matches_usuario(document: Dict[str, Any], usuario_id: str) -> bool:
    value = document.get("usuarios")
    if isinstance(value, str):
        return value == usuario_id
    if isinstance(value, dict):
        candidate = value.get("$id")
        return isinstance(candidate, str) and candidate == usuario_id
    if isinstance(value, list):
        for item in value:
            candidate = _extract_relation_id(item)
            if candidate == usuario_id:
                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 _map_sort_field(sort: str) -> str:
    if sort in {"valorTotal", "valor-total"}:
        return "valor-total"
    if sort in {"valorProdutos", "valor-produtos"}:
        return "valor-produtos"
    if sort in {"valorFrete", "valor-frete"}:
        return "valor-frete"
    return sort


def _sanitize_relation_value(field: str, value: Any) -> str:
    if isinstance(value, str):
        relation_id = value.strip()
        if not relation_id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"{field} não pode ser vazio.",
            )
        return relation_id

    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
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"{field} precisa conter um identificador válido.",
        )

    raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=f"{field} deve ser uma string ou objeto de relação válido.",
    )


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 None

    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 None
        if isinstance(first, str):
            stripped = first.strip()
            return stripped or None
        return None

    return str(value)
