from __future__ import annotations

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

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

from src.modules.empresas.schemas import EmpresaCreate, EmpresaOut, EmpresaRawOut, EmpresaUpdate

_ALLOWED_SORT_FIELDS = {
    "cnpj",
    "razao-social",
    "razaoSocial",
    "nome-fantasia",
    "nomeFantasia",
    "data-abertura",
    "$createdAt",
}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


class EmpresasService:
    def __init__(
        self,
        databases: Databases,
        database_id: str,
        collection_id: str,
        enderecos_collection_id: str | None = None,
    ) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = collection_id
        self._enderecos_collection_id = enderecos_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.")
            sort_field = _map_sort_field(sort)
            if order == "desc":
                queries.append(Query.order_desc(sort_field))
            else:
                queries.append(Query.order_asc(sort_field))
        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("cnpj", q_value),
                                Query.search("razao-social", q_value),
                                Query.search("nome-fantasia", 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 = [
            EmpresaOut.model_validate(self._map_document(doc)).model_dump(by_alias=True)
            for doc in documents
        ]

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

    async def get(self, empresa_id: str, *, raw: bool = False) -> EmpresaOut | EmpresaRawOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                empresa_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Empresa not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        if raw:
            return EmpresaRawOut.model_validate(document)

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

    async def create(self, payload: EmpresaCreate) -> EmpresaOut:
        data = payload.model_dump(by_alias=True, exclude_none=True)
        cnpj_value = data.get("cnpj")
        if not cnpj_value:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CNPJ cannot be empty.")

        normalized_enderecos = None
        if "enderecos" in data:
            normalized_enderecos = _normalize_relationship_values(data.pop("enderecos"))
        elif "enderecoID" in data:
            normalized_enderecos = _normalize_relationship_values(data.pop("enderecoID"))

        if normalized_enderecos is not None:
            if normalized_enderecos:
                await self._ensure_enderecos_exist(normalized_enderecos)
                data["enderecoID"] = normalized_enderecos[0]
            else:
                data["enderecoID"] = None

        _map_frete_fields_to_appwrite(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 == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Empresa 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 EmpresaOut.model_validate(self._map_document(document))

    async def update(self, empresa_id: str, payload: EmpresaUpdate) -> EmpresaOut:
        updates = payload.model_dump(exclude_unset=True, by_alias=True)
        if not updates:
            return await self.get(empresa_id)

        appwrite_updates: Dict[str, Any] = {}
        for key, value in updates.items():
            if key == "cnpj" and (value is None or not str(value).strip()):
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CNPJ cannot be empty.")
            appwrite_updates[key] = value

        normalized_enderecos = None
        if "enderecos" in appwrite_updates:
            normalized_enderecos = _normalize_relationship_values(appwrite_updates.pop("enderecos"))
        elif "enderecoID" in appwrite_updates:
            normalized_enderecos = _normalize_relationship_values(appwrite_updates.pop("enderecoID"))

        if normalized_enderecos is not None:
            if normalized_enderecos:
                await self._ensure_enderecos_exist(normalized_enderecos)
                appwrite_updates["enderecoID"] = normalized_enderecos[0]
            else:
                appwrite_updates["enderecoID"] = None

        _map_frete_fields_to_appwrite(appwrite_updates)

        if not appwrite_updates:
            return await self.get(empresa_id)

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

    async def delete(self, empresa_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                empresa_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Empresa 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"
            sort_field = _map_sort_field(sort)
            filtered.sort(key=lambda doc: _sort_key(doc, sort_field), reverse=reverse)

        return filtered, len(filtered)

    def _map_document(self, document: Dict[str, Any]) -> Dict[str, Any]:
        raw_enderecos = document.get("enderecos") or document.get("enderecoID")

        return {
            "id": document.get("$id", ""),
            "cnpj": document.get("cnpj", ""),
            "razao-social": document.get("razao-social"),
            "nome-fantasia": document.get("nome-fantasia"),
            "data-abertura": document.get("data-abertura"),
            "situacao": document.get("situacao"),
            "natureza-juridica": document.get("natureza-juridica"),
            "porte": document.get("porte"),
            "capital-social": document.get("capital-social"),
            "telefone": document.get("telefone"),
            "email": document.get("email"),
            "atividade-principal-codigo": document.get("atividade-principal-codigo"),
            "atividade-principal-desc": document.get("atividade-principal-desc"),
            "enderecos": _normalize_relationship_values(raw_enderecos) or [],
            "latitude": document.get("latitude"),
            "longitude": document.get("longitude"),
            "tipoFrete": document.get("tipo-frete") or document.get("tipoFrete"),
            "taxaEntrega": document.get("taxa-entrega") or document.get("taxaEntrega"),
            "valorFreteKm": document.get("valor-frete-km") or document.get("valorFreteKm"),
            "raioEntregaKm": document.get("raio-entrega-km") or document.get("raioEntregaKm"),
            "createdAt": document.get("$createdAt"),
            "updatedAt": document.get("$updatedAt"),
        }

    async def _ensure_enderecos_exist(self, endereco_ids: List[str]) -> None:
        if not endereco_ids or not self._enderecos_collection_id:
            return

        seen: Set[str] = set()
        for endereco_id in endereco_ids:
            if endereco_id in seen:
                continue
            seen.add(endereco_id)
            try:
                await self._run(
                    self._databases.get_document,
                    self._database_id,
                    self._enderecos_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


def _map_sort_field(sort: str) -> str:
    if sort in {"razaoSocial", "razao-social"}:
        return "razao-social"
    if sort in {"nomeFantasia", "nome-fantasia"}:
        return "nome-fantasia"
    if sort in {"dataAbertura", "data-abertura"}:
        return "data-abertura"
    return sort


def _map_frete_fields_to_appwrite(payload: Dict[str, Any]) -> None:
    """Converte campos camelCase para nomes usados na collection Appwrite."""
    mappings = {
        "tipoFrete": "tipo-frete",
        "taxaEntrega": "taxa-entrega",
        "valorFreteKm": "valor-frete-km",
        "raioEntregaKm": "raio-entrega-km",
    }
    for src, dst in mappings.items():
        if src in payload:
            payload[dst] = payload.pop(src)


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    needle = query.lower()
    for key in ("cnpj", "razao-social", "nome-fantasia"):
        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)


def _normalize_relationship_values(value: Any) -> List[str] | None:
    if value is None:
        return None

    items: List[Any]
    if isinstance(value, list):
        items = value
    else:
        items = [value]

    normalized: List[str] = []
    seen: Set[str] = set()
    for item in items:
        candidate: str | None = None
        if isinstance(item, str):
            candidate = item.strip()
        elif isinstance(item, dict):
            raw = item.get("$id") or item.get("id") or item.get("_id")
            if isinstance(raw, str):
                candidate = raw.strip()
            elif raw is not None:
                candidate = str(raw).strip()
        elif item is not None:
            candidate = str(item).strip()

        if candidate and candidate not in seen:
            seen.add(candidate)
            normalized.append(candidate)

    return normalized
