"""Serviço de integração com a coleção `empresas-dados` do Appwrite."""

from __future__ import annotations

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

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

from src.core.geocoding import geocode_with_mapbox

from .schemas import EmpresasDadosCreate, EmpresasDadosOut, EmpresasDadosUpdate

_DEFAULT_LIMIT = 20
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_SEARCHABLE_FIELDS = (
    "cnpj",
    "razao-social",
    "nome-fantasia",
    "natureza-juridica",
    "porte",
    "telefone",
    "email",
)
_APPWRITE_ENDERECO_FIELD = "enderecoID"

_FIELD_MAP = {
    "razaoSocial": "razao-social",
    "nomeFantasia": "nome-fantasia",
    "naturezaJuridica": "natureza-juridica",
    "capitalSocial": "capital-social",
    "atividadePrincipal": "atividade-principal-desc",
    "atividadePrincipalCodigo": "atividade-principal-codigo",
    "situacao": "situacao",
    "abertura": "data-abertura",
    "porte": "porte",
    "telefone": "telefone",
    "email": "email",
    "cnpj": "cnpj",
    "enderecoId": _APPWRITE_ENDERECO_FIELD,
    "tipoFrete": "tipo-frete",
    "taxaEntrega": "taxa-entrega",
    "valorFreteKm": "valor-frete-km",
    "raioEntregaKm": "raio-entrega-km",
    "latitude": "latitude",
    "longitude": "longitude",
}
_SORTABLE_FIELDS = {
    "cnpj",
    "razaoSocial",
    "nomeFantasia",
    "naturezaJuridica",
    "porte",
    "situacao",
    "$createdAt",
    "tipoFrete",
    "raioEntregaKm",
}


def _extract_rel_id(value: Any) -> Optional[str]:
    if value is None:
        return None
    if isinstance(value, str):
        return value
    if isinstance(value, dict):
        return value.get("$id") or value.get("id") or value.get("_id")
    if isinstance(value, list) and value:
        first = value[0]
        if isinstance(first, dict):
            return first.get("$id") or first.get("id") or first.get("_id")
        if isinstance(first, str):
            return first
    return None


def _map_document(document: Dict[str, Any]) -> EmpresasDadosOut:
    payload: Dict[str, Any] = {
        "id": document.get("$id", ""),
        "cnpj": document.get("cnpj"),
        "razaoSocial": document.get("razao-social"),
        "nomeFantasia": document.get("nome-fantasia"),
        "naturezaJuridica": document.get("natureza-juridica"),
        "porte": document.get("porte"),
        "capitalSocial": document.get("capital-social"),
        "telefone": document.get("telefone"),
        "email": document.get("email"),
        "atividadePrincipal": (
            document.get("atividade-principal-desc")
            or document.get("atividade-principal")
            or document.get("atividade-principal-descricao")
        ),
        "atividadePrincipalCodigo": (
            document.get("atividade-principal-codigo")
            or document.get("atividade-principal-code")
        ),
        "tipo": document.get("tipo"),
        "situacao": document.get("situacao"),
        "abertura": document.get("data-abertura") or document.get("abertura"),
        "enderecoId": _extract_rel_id(
            document.get(_APPWRITE_ENDERECO_FIELD) or document.get("enderecos")
        ),
        "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"),
        "latitude": document.get("latitude"),
        "longitude": document.get("longitude"),
        "createdAt": document.get("$createdAt"),
        "updatedAt": document.get("$updatedAt"),
    }
    return EmpresasDadosOut.model_validate(payload)


class EmpresasDadosService:
    """Camada de acesso aos dados da coleção `empresas-dados`."""

    def __init__(
        self,
        databases: Databases,
        database_id: str,
        collection_id: str,
        enderecos_collection_id: str | None = None,
        mapbox_access_token: str | None = None,
    ) -> None:
        self._db = databases
        self._db_id = database_id
        self._col_id = collection_id
        self._enderecos_collection_id = enderecos_collection_id
        self._mapbox_access_token = mapbox_access_token

    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,
        cnpj: str | None,
        endereco_id: 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 cnpj and cnpj.strip():
            queries.append(Query.equal("cnpj", cnpj.strip()))
        if endereco_id and endereco_id.strip():
            queries.append(Query.equal(_APPWRITE_ENDERECO_FIELD, endereco_id.strip()))

        if sort:
            if sort not in _SORTABLE_FIELDS:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="Campo de ordenação inválido.",
                )
            if order and order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="Ordem inválida.",
                )
            sort_field = _FIELD_MAP.get(sort, sort)
            queries.append(
                Query.order_desc(sort_field) if order == "desc" else Query.order_asc(sort_field)
            )
        elif order and order not in _ALLOWED_ORDER_VALUES:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Ordem inválida.")

        if q and q.strip():
            term = q.strip()
            search_queries = []
            for field in _SEARCHABLE_FIELDS:
                try:
                    search_queries.append(Query.search(field, term))
                except AttributeError:
                    search_queries = []
                    break
            if search_queries:
                queries.append(Query.or_(search_queries))

        try:
            response = await self._run(
                self._db.list_documents,
                self._db_id,
                self._col_id,
                queries,
            )
        except AppwriteException as exc:
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        documents = response.get("documents", [])
        items = [_map_document(doc).model_dump() for doc in documents]
        total = response.get("total", len(items))

        return {"items": items, "total": total}

    async def get(self, document_id: str) -> EmpresasDadosOut:
        try:
            document = await self._run(
                self._db.get_document,
                self._db_id,
                self._col_id,
                document_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Empresa não encontrada.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc
        return _map_document(document)

    async def create(
        self, payload: EmpresasDadosCreate, *, document_id: str = "unique()"
    ) -> EmpresasDadosOut:
        data = payload.model_dump(exclude_unset=True)
        await self._populate_coordinates(data)
        appwrite_payload = self._prepare_appwrite_payload(data)

        try:
            document = await self._run(
                self._db.create_document,
                self._db_id,
                self._col_id,
                document_id=document_id or "unique()",
                data=appwrite_payload,
            )
        except AppwriteException as exc:
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Empresa já cadastrada.",
                ) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc
        return _map_document(document)

    async def update(self, document_id: str, payload: EmpresasDadosUpdate) -> EmpresasDadosOut:
        updates = payload.model_dump(exclude_unset=True)
        if not updates:
            return await self.get(document_id)
        await self._populate_coordinates(updates)
        appwrite_updates = self._prepare_appwrite_payload(updates)

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

        try:
            document = await self._run(
                self._db.update_document,
                self._db_id,
                self._col_id,
                document_id,
                appwrite_updates,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Empresa não encontrada.") from exc
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Empresa já cadastrada.",
                ) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc
        return _map_document(document)

    async def delete(self, document_id: str) -> None:
        try:
            await self._run(self._db.delete_document, self._db_id, self._col_id, document_id)
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Empresa não encontrada.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    async def _populate_coordinates(self, data: Dict[str, Any]) -> None:
        endereco_id = data.get("enderecoId")
        if not endereco_id:
            return

        if data.get("latitude") is not None and data.get("longitude") is not None:
            return

        if not self._enderecos_collection_id:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Coleção de endereços não configurada para geocodificação.",
            )

        endereco_document = await self._get_endereco_document(endereco_id)
        if endereco_document is None:
            return

        latitude = endereco_document.get("latitude")
        longitude = endereco_document.get("longitude")
        if latitude is not None and longitude is not None:
            data["latitude"] = latitude
            data["longitude"] = longitude
            return

        address_string = self._compose_address_string(endereco_document)
        if not address_string:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Endereço insuficiente para geocodificação.",
            )

        lat, lng = await geocode_with_mapbox(address_string, self._mapbox_access_token or "")
        data["latitude"] = lat
        data["longitude"] = lng

    async def _get_endereco_document(self, endereco_id: str) -> Dict[str, Any] | None:
        try:
            return await self._run(
                self._db.get_document,
                self._db_id,
                self._enderecos_collection_id,
                endereco_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="Endereço não encontrado para geocodificação.",
                ) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    def _compose_address_string(self, endereco: Dict[str, Any]) -> str:
        parts = [
            endereco.get("logradouro"),
            endereco.get("numero"),
            endereco.get("bairro"),
            endereco.get("cidade"),
            endereco.get("estado"),
            endereco.get("cep"),
            endereco.get("pais"),
        ]
        cleaned = [str(part).strip() for part in parts if part and str(part).strip()]
        return ", ".join(cleaned)

    def _prepare_appwrite_payload(self, data: Dict[str, Any]) -> Dict[str, Any]:
        payload: Dict[str, Any] = {}
        for key, value in data.items():
            if key not in _FIELD_MAP:
                continue
            appwrite_key = _FIELD_MAP[key]
            if value is None:
                payload[appwrite_key] = None
                continue
            if isinstance(value, str):
                normalized = value.strip()
                payload[appwrite_key] = normalized
            else:
                payload[appwrite_key] = value
        return payload
