from __future__ import annotations

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

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.core.geocoding import driving_distance_km_mapbox, geocode_with_mapbox
from src.modules.empresas_dados.service import EmpresasDadosService
from src.modules.entregas.schemas import (
    CotacaoFreteRequest,
    CotacaoFreteResponse,
    DestinoCotacao,
    EntregaCreateRequest,
    EntregaOut,
    EntregaUpdate,
)

_ALLOWED_SORT_FIELDS = {
    "status",
    "codigoRastreamento",
    "dataCriacao",
    "dataAtualizacao",
    "$createdAt",
    "$updatedAt",
    "nomeDestinatario",
    "entregadorId",
}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20
_SEARCH_FIELDS = (
    "codigoRastreamento",
    "nomeDestinatario",
    "status",
    "cidadeEntrega",
    "enderecoEntrega",
    "bairroEntrega",
    "entregadorId",
)


class EntregasService:
    def __init__(
        self,
        databases: Databases,
        database_id: str,
        collection_id: str,
        empresas_service: EmpresasDadosService | None = None,
        mapbox_access_token: str | None = None,
    ) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = collection_id
        self._empresas_service = empresas_service
        self._mapbox_access_token = mapbox_access_token

    async def cotar_frete(self, payload: CotacaoFreteRequest) -> CotacaoFreteResponse:
        return await self._calcular_cotacao(payload.empresa_id, payload.destino)

    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,
        status_filter: str | None,
        entregador_id: str | None,
        empresa_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)]

        normalized_order = order.lower() if isinstance(order, str) else None

        status_value = _normalize_optional_filter(status_filter, "status")
        if status_value is not None:
            queries.append(Query.equal("status", status_value))

        entregador_value = _normalize_optional_filter(entregador_id, "entregadorId")
        if entregador_value is not None:
            queries.append(Query.equal("entregadorId", entregador_value))

        empresa_value = _normalize_optional_filter(empresa_id, "empresaId")
        if empresa_value is not None:
            queries.append(Query.equal("empresaId", empresa_value))

        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))
                if normalized_order is None:
                    normalized_order = "asc"
        else:
            effective_order = normalized_order or "desc"
            if effective_order not in _ALLOWED_ORDER_VALUES:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid order value.")
            if effective_order == "desc":
                queries.append(Query.order_desc("$createdAt"))
            else:
                queries.append(Query.order_asc("$createdAt"))
            normalized_order = effective_order

        search_performed = False
        q_value = q.strip() if isinstance(q, str) else ""
        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_value and not search_performed:
            documents, total = await self._fallback_search(
                q_value,
                sort,
                normalized_order or "desc",
                status_value,
                entregador_value,
                empresa_value,
            )
            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, entrega_id: str) -> EntregaOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                entrega_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrega not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

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

    async def create(self, payload: EntregaCreateRequest) -> EntregaOut:
        data = payload.data.model_dump(by_alias=True, exclude_none=True, exclude_unset=True)
        sanitized = _sanitize_payload(data)
        document_identifier = _resolve_document_id(payload.document_id)

        cotacao = await self._cotacao_from_entrega(sanitized)
        sanitized["taxaEntrega"] = cotacao.valor
        sanitized["distancia"] = cotacao.distancia_km

        try:
            document = await self._run(
                self._databases.create_document,
                self._database_id,
                self._collection_id,
                document_id=document_identifier,
                data=sanitized,
            )
        except AppwriteException as exc:
            if exc.code == 409:
                raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Entrega 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 EntregaOut.model_validate(self._map_document(document))

    async def update(self, entrega_id: str, payload: EntregaUpdate) -> EntregaOut:
        updates = payload.model_dump(by_alias=True, exclude_none=True, exclude_unset=True)
        sanitized = _sanitize_payload(updates)

        if not sanitized:
            return await self.get(entrega_id)

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

    async def delete(self, entrega_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                entrega_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrega not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    async def _cotacao_from_entrega(self, entrega_data: Dict[str, Any]) -> CotacaoFreteResponse:
        empresa_id = entrega_data.get("empresaId")
        if not empresa_id or not isinstance(empresa_id, str):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="empresaId e obrigatorio para calcular o frete.",
            )

        destino = DestinoCotacao(
            enderecoEntrega=entrega_data.get("enderecoEntrega"),
            numeroEntrega=entrega_data.get("numeroEntrega"),
            bairroEntrega=entrega_data.get("bairroEntrega"),
            cidadeEntrega=entrega_data.get("cidadeEntrega"),
            estadoEntrega=entrega_data.get("estadoEntrega"),
            cepEntrega=entrega_data.get("cepEntrega"),
            paisEntrega=entrega_data.get("paisEntrega"),
            latitude=entrega_data.get("latitude"),
            longitude=entrega_data.get("longitude"),
        )
        return await self._calcular_cotacao(empresa_id, destino)

    async def _calcular_cotacao(self, empresa_id: str, destino: DestinoCotacao) -> CotacaoFreteResponse:
        if not self._empresas_service:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Servico de empresas nao configurado para calculo de frete.",
            )

        empresa = await self._empresas_service.get(empresa_id)
        if empresa.latitude is None or empresa.longitude is None:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Empresa sem latitude/longitude para calcular frete.",
            )

        access_token = self._mapbox_access_token or ""
        dest_lat, dest_lng = await self._resolver_destino(destino, access_token)
        distancia_km = await driving_distance_km_mapbox(
            empresa.latitude,
            empresa.longitude,
            dest_lat,
            dest_lng,
            access_token,
        )

        if empresa.raioEntregaKm is not None and distancia_km > float(empresa.raioEntregaKm):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Destino fora do raio de entrega configurado para a empresa.",
            )

        valor = self._calcular_preco(
            tipo_frete=empresa.tipoFrete,
            taxa_fixa=empresa.taxaEntrega,
            valor_km=empresa.valorFreteKm,
            distancia_km=distancia_km,
        )

        return CotacaoFreteResponse(
            valor=round(valor, 2),
            distancia_km=round(distancia_km, 3),
            tipo_frete=(empresa.tipoFrete or "fixo"),
        )

    async def _resolver_destino(self, destino: DestinoCotacao, access_token: str) -> Tuple[float, float]:
        if destino.latitude is not None and destino.longitude is not None:
            return float(destino.latitude), float(destino.longitude)

        address = _compose_destino_address(destino)
        return await geocode_with_mapbox(address, access_token)

    def _calcular_preco(
        self,
        tipo_frete: str | None,
        taxa_fixa: float | None,
        valor_km: float | None,
        distancia_km: float,
    ) -> float:
        tipo = (tipo_frete or "fixo").lower()
        if tipo == "gratis":
            return 0.0
        if tipo == "fixo":
            if taxa_fixa is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="taxaEntrega nao configurada para frete fixo.",
                )
            return float(taxa_fixa)
        if tipo in {"por_km", "por-km", "km"}:
            if valor_km is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="valorFreteKm nao configurado para frete por km.",
                )
            return distancia_km * float(valor_km)

        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="tipoFrete invalido.",
        )

    async def _fallback_search(
        self,
        query: str,
        sort: str | None,
        order: str,
        status_value: str | None,
        entregador_value: str | None,
        empresa_value: 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(200)],
            )
        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_filters(doc, status_value, entregador_value, empresa_value)
            and _matches_query(doc, query)
        ]

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

        return filtered, len(filtered)

    def _map_document(self, document: Dict[str, Any]) -> Dict[str, Any]:
        return {
            "id": document.get("$id", ""),
            "entregadorId": document.get("entregadorId"),
            "empresaId": document.get("empresaId"),
            "enderecoEntrega": document.get("enderecoEntrega"),
            "cidadeEntrega": document.get("cidadeEntrega"),
            "estadoEntrega": document.get("estadoEntrega"),
            "cepEntrega": document.get("cepEntrega"),
            "paisEntrega": document.get("paisEntrega"),
            "bairroEntrega": document.get("bairroEntrega"),
            "numeroEntrega": document.get("numeroEntrega"),
            "complementoEntrega": document.get("complementoEntrega"),
            "nomeDestinatario": document.get("nomeDestinatario"),
            "telefoneDestinatario": document.get("telefoneDestinatario"),
            "emailDestinatario": document.get("emailDestinatario"),
            "status": document.get("status"),
            "codigoRastreamento": document.get("codigoRastreamento"),
            "dataEntregaEstimativa": document.get("dataEntregaEstimativa"),
            "dataEntregaReal": document.get("dataEntregaReal"),
            "dataColeta": document.get("dataColeta"),
            "dataAtribuicao": document.get("dataAtribuicao"),
            "dataCriacao": document.get("dataCriacao"),
            "dataAtualizacao": document.get("dataAtualizacao"),
            "taxaEntrega": document.get("taxaEntrega"),
            "distancia": document.get("distancia"),
            "observacoesEntrega": document.get("observacoesEntrega"),
            "urlComprovanteEntrega": document.get("urlComprovanteEntrega"),
            "assinaturaEntrega": document.get("assinaturaEntrega"),
            "motivoFalha": document.get("motivoFalha"),
            "motivoCancelamento": document.get("motivoCancelamento"),
            "createdAt": document.get("$createdAt"),
            "updatedAt": document.get("$updatedAt"),
        }


def _sanitize_payload(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, str):
            stripped = value.strip()
            if stripped:
                sanitized[key] = stripped
        else:
            sanitized[key] = value
    return sanitized


def _resolve_document_id(document_id: str | None) -> str:
    if isinstance(document_id, str) and document_id.strip():
        return document_id.strip()
    return ID.unique()


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 in {"$createdAt", "$updatedAt"}:
        return document.get(sort)
    return document.get(sort)


def _matches_filters(
    document: Dict[str, Any],
    status_value: str | None,
    entregador_value: str | None,
    empresa_value: str | None,
) -> bool:
    if status_value is not None and document.get("status") != status_value:
        return False
    if entregador_value is not None and document.get("entregadorId") != entregador_value:
        return False
    if empresa_value is not None and document.get("empresaId") != empresa_value:
        return False
    return True


def _normalize_optional_filter(value: str | None, field_name: str) -> str | None:
    if value is None:
        return None
    if not isinstance(value, str):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"{field_name} must be provided as a non-empty string.",
        )
    cleaned = value.strip()
    if not cleaned:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"{field_name} cannot be empty.",
        )
    return cleaned



def _compose_destino_address(destino: DestinoCotacao) -> str:
    parts = [
        destino.endereco_entrega,
        destino.numero_entrega,
        destino.bairro_entrega,
        destino.cidade_entrega,
        destino.estado_entrega,
        destino.cep_entrega,
        destino.pais_entrega,
    ]
    cleaned = [str(part).strip() for part in parts if part is not None and str(part).strip()]
    if not cleaned:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Endereco destino insuficiente para geocodificacao.",
        )
    return ', '.join(cleaned)
