from __future__ import annotations

import asyncio
from numbers import Real
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

from src.modules.produtos_catalogo.schemas import (
    ProdutoCatalogoCreateRequest,
    ProdutoCatalogoOut,
    ProdutoCatalogoUpdateRequest,
)

_SORT_FIELD_MAP = {
    "nome": "nome",
    "codigo_barras": "codigo_ean",
    "codigo_ean": "codigo_ean",
    "preco_consumidor": "preco_base",
    "preco_base": "preco_base",
    "pmc": "pmc",
    "preco_nf": "preco_nf",
    "$createdAt": "$createdAt",
}
_ALLOWED_SORT_FIELDS = set(_SORT_FIELD_MAP.keys())
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20
_QUERY_SEARCH_FIELDS = ("nome", "descricao", "codigo_ean", "codigo_interno")
_FALLBACK_SEARCH_FIELDS = ("nome", "descricao", "codigo_ean", "codigo_barras", "codigo_interno")
_PRICE_FIELD = "preco_base"


class ProdutosCatalogoService:
    def __init__(
        self,
        *,
        databases: Databases,
        database_id: str,
        collection_id: str,
        laboratorios_collection_id: str,
        categorias_collection_id: str,
        subcategorias_collection_id: str,
    ) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = collection_id
        self._laboratorios_collection_id = laboratorios_collection_id
        self._categorias_collection_id = categorias_collection_id
        self._subcategorias_collection_id = subcategorias_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,
        laboratorio_id: str | None,
        categoria_id: str | None,
        subcategoria_id: str | None,
        preco_min: float | None,
        preco_max: float | None,
        pmc_min: float | None,
        pmc_max: float | None,
    ) -> Dict[str, Any]:
        limit_value = limit or _DEFAULT_LIMIT
        offset = (page - 1) * limit_value

        base_queries = self._build_filter_queries(
            laboratorio_id=laboratorio_id,
            categoria_id=categoria_id,
            subcategoria_id=subcategoria_id,
            preco_min=preco_min,
            preco_max=preco_max,
            pmc_min=pmc_min,
            pmc_max=pmc_max,
        )

        queries: List[Any] = [Query.limit(limit_value), Query.offset(offset), *base_queries]

        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.")
            mapped_sort = _SORT_FIELD_MAP[sort]
            if order == "desc":
                queries.append(Query.order_desc(mapped_sort))
            else:
                queries.append(Query.order_asc(mapped_sort))
        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
        normalized_query: str | None = None
        if q:
            q_value = q.strip()
            if q_value:
                normalized_query = q_value
                try:
                    queries.append(
                        Query.or_([Query.search(field, q_value) for field in _QUERY_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:
            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

        documents: List[Dict[str, Any]] = response.get("documents", [])
        total = response.get("total", len(documents))
        sum_aggregates = response.get("sum", [])
        if not isinstance(sum_aggregates, list):
            sum_aggregates = []

        stats = response.get("stats", [])
        if not isinstance(stats, list):
            stats = []

        if normalized_query and not search_performed:
            documents, total = await self._fallback_search(
                base_queries=base_queries,
                q=normalized_query,
                sort=_SORT_FIELD_MAP.get(sort, sort) if sort else None,
                order=(order or "asc"),
            )
            documents = documents[offset : offset + limit_value]

        normalized_documents = [self._map_document_for_list(document) for document in documents]

        return {
            "total": total,
            "documents": normalized_documents,
            "sum": sum_aggregates,
            "stats": stats,
            "limit": limit_value,
            "offset": offset,
        }

    async def get(self, produto_id: str) -> ProdutoCatalogoOut:
        document = await self._get_document(produto_id)
        return self._map_document(document)

    async def get_by_codigo_ean(self, codigo_ean: str) -> ProdutoCatalogoOut:
        normalized_codigo = self._require_non_empty(codigo_ean, "codigo_ean")

        try:
            response = await self._run(
                self._databases.list_documents,
                self._database_id,
                self._collection_id,
                queries=[Query.equal("codigo_ean", normalized_codigo), Query.limit(1)],
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Produto catálogo não encontrado.",
                ) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        documents = response.get("documents") or []
        if not documents:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Produto catálogo não encontrado.",
            )

        return self._map_document(documents[0])

    async def create(self, payload: ProdutoCatalogoCreateRequest) -> Dict[str, Any]:
        data_payload = payload.data

        codigo_barras = self._require_non_empty(data_payload.codigo_barras, "codigo_ean")
        nome = self._require_non_empty(data_payload.nome, "nome")

        await self._ensure_unique_codigo_barras(codigo_barras)

        data: Dict[str, Any] = {
            "codigo_ean": codigo_barras,
            "nome": nome,
            "preco_base": data_payload.preco_consumidor,
        }

        codigo_interno = self._normalize_optional_string(data_payload.codigo_interno)
        if codigo_interno is not None:
            data["codigo_interno"] = codigo_interno

        descricao = self._normalize_optional_string(data_payload.descricao)
        if descricao is not None:
            data["descricao"] = descricao

        if data_payload.preco_fabrica is not None:
            data["preco_fabrica"] = data_payload.preco_fabrica
        if data_payload.pmc is not None:
            data["pmc"] = data_payload.pmc
        if data_payload.desconto_comercial is not None:
            data["desconto_comercial"] = data_payload.desconto_comercial
        if data_payload.valor_substituicao_tributaria is not None:
            data["valor_substituicao_tributaria"] = data_payload.valor_substituicao_tributaria
        if data_payload.preco_nf is not None:
            data["preco_nf"] = data_payload.preco_nf

        laboratorio_id = self._normalize_optional_string(data_payload.laboratorioId)
        if laboratorio_id is not None:
            await self._ensure_document_exists(
                self._laboratorios_collection_id,
                laboratorio_id,
                "Laboratório não encontrado",
            )
            data["laboratorio"] = laboratorio_id

        categoria_id = self._normalize_optional_string(data_payload.categoriaId)
        if categoria_id is not None:
            await self._ensure_document_exists(
                self._categorias_collection_id,
                categoria_id,
                "Categoria não encontrada",
            )
            data["categoria"] = categoria_id

        lab_nome = self._normalize_optional_string(data_payload.lab_nome)
        if lab_nome is not None:
            data["lab_nome"] = lab_nome

        cat_nome = self._normalize_optional_string(data_payload.cat_nome)
        if cat_nome is not None:
            data["cat_nome"] = cat_nome

        subcategoria_id = self._normalize_optional_string(data_payload.subcategoriaId)
        if subcategoria_id is not None:
            await self._ensure_document_exists(
                self._subcategorias_collection_id,
                subcategoria_id,
                "Subcategoria não encontrada",
            )
            data["subcategoria"] = subcategoria_id

        create_kwargs: Dict[str, Any] = {"document_id": payload.document_id, "data": data}
        if payload.permissions is not None:
            create_kwargs["permissions"] = payload.permissions

        try:
            document = await self._run(
                self._databases.create_document,
                self._database_id,
                self._collection_id,
                **create_kwargs,
            )
        except AppwriteException as exc:
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Barcode 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 document

    async def update(self, produto_id: str, payload: ProdutoCatalogoUpdateRequest) -> Dict[str, Any]:
        existing_document = await self._get_document(produto_id)
        updates = payload.data.model_dump(exclude_unset=True)

        if not updates and payload.permissions is None:
            return existing_document

        update_data: Dict[str, Any] = {}

        if "codigo_barras" in updates:
            value = updates["codigo_barras"]
            if value is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="codigo_ean cannot be null.",
                )
            normalized = self._require_non_empty(value, "codigo_ean")
            await self._ensure_unique_codigo_barras(normalized, exclude_id=produto_id)
            update_data["codigo_ean"] = normalized

        if "nome" in updates:
            value = updates["nome"]
            if value is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="nome cannot be null.",
                )
            update_data["nome"] = self._require_non_empty(value, "nome")

        if "codigo_interno" in updates:
            update_data["codigo_interno"] = self._normalize_optional_string(updates["codigo_interno"])

        if "descricao" in updates:
            update_data["descricao"] = self._normalize_optional_string(updates["descricao"])

        if "preco_consumidor" in updates:
            value = updates["preco_consumidor"]
            if value is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="preco_base cannot be null.",
                )
            update_data["preco_base"] = value

        for numeric_field in ("preco_fabrica", "pmc", "desconto_comercial", "valor_substituicao_tributaria", "preco_nf"):
            if numeric_field in updates:
                update_data[numeric_field] = updates[numeric_field]

        if "laboratorioId" in updates:
            laboratorio_id = updates["laboratorioId"]
            if laboratorio_id is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="laboratorioId cannot be null.",
                )
            laboratorio_id = self._require_non_empty(laboratorio_id, "laboratorioId")
            await self._ensure_document_exists(
                self._laboratorios_collection_id,
                laboratorio_id,
                "Laboratório não encontrado",
            )
            update_data["laboratorio"] = laboratorio_id

        if "categoriaId" in updates:
            categoria_id = updates["categoriaId"]
            if categoria_id is None:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="categoriaId cannot be null.",
                )
            categoria_id = self._require_non_empty(categoria_id, "categoriaId")
            await self._ensure_document_exists(
                self._categorias_collection_id,
                categoria_id,
                "Categoria não encontrada",
            )
            update_data["categoria"] = categoria_id

        if "lab_nome" in updates:
            update_data["lab_nome"] = self._normalize_optional_string(updates["lab_nome"])

        if "cat_nome" in updates:
            update_data["cat_nome"] = self._normalize_optional_string(updates["cat_nome"])

        if "subcategoriaId" in updates:
            subcategoria_id = updates["subcategoriaId"]
            if subcategoria_id is None:
                update_data["subcategoria"] = None
            else:
                subcategoria_id = self._require_non_empty(subcategoria_id, "subcategoriaId")
                await self._ensure_document_exists(
                    self._subcategorias_collection_id,
                    subcategoria_id,
                    "Subcategoria não encontrada",
                )
                update_data["subcategoria"] = subcategoria_id

        if not update_data and payload.permissions is None:
            return existing_document

        update_kwargs: Dict[str, Any] = {"data": update_data}
        if payload.permissions is not None:
            update_kwargs["permissions"] = payload.permissions

        try:
            document = await self._run(
                self._databases.update_document,
                self._database_id,
                self._collection_id,
                produto_id,
                **update_kwargs,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Produto catálogo não encontrado.",
                ) from exc
            if exc.code == 409:
                raise HTTPException(
                    status_code=status.HTTP_409_CONFLICT,
                    detail="Barcode 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 document

    async def delete(self, produto_id: str) -> None:
        try:
            await self._run(
                self._databases.delete_document,
                self._database_id,
                self._collection_id,
                produto_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Produto catálogo não encontrado.",
                ) from exc
            if exc.code in {400, 409}:
                raise HTTPException(status_code=exc.code, detail=exc.message or "Operation not permitted.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    async def _get_document(self, produto_id: str) -> Dict[str, Any]:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                produto_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Produto catálogo não encontrado.",
                ) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc
        return document

    async def _ensure_unique_codigo_barras(
        self,
        codigo_barras: str,
        *,
        exclude_id: str | None = None,
    ) -> None:
        try:
            response = await self._run(
                self._databases.list_documents,
                self._database_id,
                self._collection_id,
                queries=[Query.equal("codigo_ean", codigo_barras)],
            )
        except AppwriteException as exc:
            if exc.code == 404:
                return
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

        for document in response.get("documents", []):
            if exclude_id and document.get("$id") == exclude_id:
                continue
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Barcode already exists")

    async def _ensure_document_exists(
        self,
        collection_id: str,
        document_id: str,
        not_found_message: str,
    ) -> None:
        try:
            await self._run(
                self._databases.get_document,
                self._database_id,
                collection_id,
                document_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=not_found_message) from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

    async def _fallback_search(
        self,
        *,
        base_queries: List[Any],
        q: str,
        sort: str | None,
        order: str,
    ) -> tuple[List[Dict[str, Any]], int]:
        queries = [*base_queries, Query.limit(100)]
        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", [])
        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)

    def _build_filter_queries(
        self,
        *,
        laboratorio_id: str | None,
        categoria_id: str | None,
        subcategoria_id: str | None,
        preco_min: float | None,
        preco_max: float | None,
        pmc_min: float | None,
        pmc_max: float | None,
    ) -> List[Any]:
        queries: List[Any] = []

        if laboratorio_id:
            queries.append(Query.equal("laboratorio", laboratorio_id))
        if categoria_id:
            queries.append(Query.equal("categoria", categoria_id))
        if subcategoria_id:
            queries.append(Query.equal("subcategoria", subcategoria_id))

        if preco_min is not None:
            queries.append(Query.greater_than_equal(_PRICE_FIELD, preco_min))
        if preco_max is not None:
            queries.append(Query.less_than_equal(_PRICE_FIELD, preco_max))
        if pmc_min is not None:
            queries.append(Query.greater_than_equal("pmc", pmc_min))
        if pmc_max is not None:
            queries.append(Query.less_than_equal("pmc", pmc_max))

        return queries

    @staticmethod
    def _normalize_optional_string(value: str | None) -> str | None:
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

    @staticmethod
    def _require_non_empty(value: str, field_name: str) -> str:
        normalized = value.strip()
        if not normalized:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"{field_name} cannot be empty.",
            )
        return normalized

    @staticmethod
    def _extract_reference_id(value: Any) -> str | None:
        if isinstance(value, str):
            return value
        if isinstance(value, dict):
            return value.get("$id") or value.get("id")
        if isinstance(value, list):
            for item in value:
                extracted = ProdutosCatalogoService._extract_reference_id(item)
                if extracted:
                    return extracted
        return None

    @staticmethod
    def _extract_codigo_barras(document: Dict[str, Any]) -> str:
        for field_name in ("codigo_ean", "codigo_barras"):
            value = document.get(field_name)
            if isinstance(value, str):
                normalized = value.strip()
                if normalized:
                    return normalized
            elif value is not None:
                return str(value)
        return ""

    def _map_document(self, document: Dict[str, Any]) -> ProdutoCatalogoOut:
        preco_base = document.get("preco_base")
        if preco_base is None:
            preco_base = document.get("preco_consumidor", 0.0)

        if isinstance(preco_base, Real):
            preco_consumidor_value = float(preco_base)
        elif isinstance(preco_base, str):
            try:
                preco_consumidor_value = float(preco_base.strip())
            except ValueError:
                preco_consumidor_value = 0.0
        else:
            preco_consumidor_value = 0.0

        return ProdutoCatalogoOut(
            id=document.get("$id", ""),
            collectionId=document.get("$collectionId"),
            codigo_barras=self._extract_codigo_barras(document),
            codigo_interno=document.get("codigo_interno"),
            nome=document.get("nome", ""),
            descricao=document.get("descricao"),
            preco_consumidor=preco_consumidor_value,
            preco_fabrica=document.get("preco_fabrica"),
            pmc=document.get("pmc"),
            desconto_comercial=document.get("desconto_comercial"),
            valor_substituicao_tributaria=document.get("valor_substituicao_tributaria"),
            preco_nf=document.get("preco_nf"),
            laboratorioId=self._extract_reference_id(document.get("laboratorio")),
            categoriaId=self._extract_reference_id(document.get("categoria")),
            lab_nome=self._normalize_optional_string(document.get("lab_nome")),
            cat_nome=self._normalize_optional_string(document.get("cat_nome")),
            subcategoriaId=self._extract_reference_id(document.get("subcategoria")),
            createdAt=document.get("$createdAt", ""),
            updatedAt=document.get("$updatedAt", ""),
        )

    def _map_document_for_list(self, document: Dict[str, Any]) -> Dict[str, Any]:
        mapped = self._map_document(document)

        permissions = document.get("$permissions")
        if isinstance(permissions, list):
            normalized_permissions = permissions
        else:
            normalized_permissions = []

        return {
            "$id": mapped.id,
            "$sequence": document.get("$sequence"),
            "$databaseId": document.get("$databaseId", self._database_id),
            "$collectionId": document.get("$collectionId", self._collection_id),
            "$createdAt": mapped.createdAt,
            "$updatedAt": mapped.updatedAt,
            "$permissions": normalized_permissions,
            "codigo_ean": mapped.codigo_barras,
            "codigo_interno": mapped.codigo_interno,
            "nome": mapped.nome,
            "descricao": mapped.descricao,
            "preco_base": mapped.preco_consumidor,
            "preco_fabrica": mapped.preco_fabrica,
            "pmc": mapped.pmc,
            "desconto_comercial": mapped.desconto_comercial,
            "valor_substituicao_tributaria": mapped.valor_substituicao_tributaria,
            "preco_nf": mapped.preco_nf,
            "laboratorio": mapped.laboratorioId,
            "categoria": mapped.categoriaId,
            "lab_nome": mapped.lab_nome,
            "cat_nome": mapped.cat_nome,
            "subcategoria": mapped.subcategoriaId,
        }


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    needle = query.lower()
    for field in _FALLBACK_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)
