from __future__ import annotations

import asyncio
from datetime import date
from decimal import Decimal, ROUND_HALF_UP
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
import httpx

from src.core.appwrite_endpoint import normalize_appwrite_endpoint
from src.core.appwrite_session import normalize_appwrite_session_cookie

from .schemas import (
    ProdutoVendaCreateRequest,
    ProdutoVendaOut,
    ProdutoVendaUpdate,
)

_ALLOWED_SORT_FIELDS = {
    "catalogo_id",
    "nome",
    "preco_venda",
    "data_validade",
    "$createdAt",
}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


def _parse_float(value: Any, default: float = 0.0) -> float:
    """Converte um valor potencialmente nulo em float sem levantar exceções.

    O Appwrite pode retornar campos numéricos vazios como ``None`` ou string
    vazia, o que fazia ``float(value)`` lançar ``TypeError``/``ValueError`` e
    resultava em erro 500. Essa função garante um fallback seguro.
    """

    if isinstance(value, (int, float)):
        return float(value)

    if isinstance(value, str):
        try:
            return float(value)
        except ValueError:
            return default

    return default


def _parse_int(value: Any, default: int = 0) -> int:
    """Converte um valor potencialmente nulo em ``int`` de forma segura."""

    if isinstance(value, int):
        return value

    if isinstance(value, float):
        return int(value)

    if isinstance(value, str):
        try:
            return int(float(value))
        except ValueError:
            return default

    return default


def _ensure_str(value: Any, default: str = "") -> str:
    """Return ``value`` as ``str`` while handling ``None`` safely."""

    if value is None:
        return default
    if isinstance(value, str):
        return value
    return str(value)


def _ensure_optional_str(value: Any) -> str | None:
    """Return ``value`` as ``str`` or ``None`` when absent."""

    if value is None:
        return None
    if isinstance(value, str):
        return value
    return str(value)


def _map(doc: Dict[str, Any]) -> ProdutoVendaOut:
    preco_venda = _parse_float(doc.get("preco_venda"), 0.0)
    preco_final = _parse_float(doc.get("preco_final"), 0.0)
    if preco_final <= 0 and preco_venda > 0:
        preco_final = _calculate_preco_final(preco_venda)
    return ProdutoVendaOut(
        id=_ensure_str(doc.get("$id")),
        catalogo_id=_ensure_str(doc.get("catalogo_id")),
        nome=_ensure_str(doc.get("nome")),
        preco_venda=preco_venda,
        preco_final=preco_final,
        qtdade_estoque=_parse_int(doc.get("qtdade_estoque"), 0),
        observacoes=_ensure_optional_str(doc.get("observacoes")),
        data_validade=_ensure_str(doc.get("data_validade")),
        empresa_id=_ensure_optional_str(doc.get("empresa_id")),
        createdAt=_ensure_str(doc.get("$createdAt")),
        updatedAt=_ensure_str(doc.get("$updatedAt")),
    )


def _calculate_preco_final(preco_venda: float) -> float:
    """Calcula o preço final com acréscimo de 12% usando arredondamento financeiro."""

    if preco_venda <= 0:
        return 0.0
    value = Decimal(str(preco_venda)) * Decimal("1.12")
    return float(value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))


class ProdutosVendaService:
    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._db = databases
        self._db_id = database_id
        self._col = 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 _io(self, fn: Any, *args: Any, **kwargs: Any) -> Any:
        return await asyncio.to_thread(fn, *args, **kwargs)

    async def list(
        self,
        *,
        page: int,
        limit: int | None,
        q: str | None,
        sort: str | None,
        order: str | None,
        empresa_id: str | None = None,
    ) -> Dict[str, Any]:
        limit_v = limit or _DEFAULT_LIMIT
        off = (page - 1) * limit_v
        queries: List[Any] = [Query.limit(limit_v), Query.offset(off)]

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

        if q and q.strip():
            term = q.strip()
            queries.append(
                Query.or_(
                    [
                        Query.search("catalogo_id", term),
                        Query.search("nome", term),
                        Query.search("observacoes", term),
                    ]
                )
            )

        if empresa_id:
            queries.append(Query.equal("empresa_id", empresa_id))

        if self._project_id and self._endpoint and (self._session_cookie or self._api_key):
            query_strings = [q if isinstance(q, str) else str(q) for q in queries]
            return await self._list_via_http(query_strings)

        try:
            res = await self._io(self._db.list_documents, self._db_id, self._col, queries)
        except AppwriteException as e:
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e

        docs = res.get("documents", [])
        items = [_map(d).model_dump() for d in docs]
        return {"items": items, "total": res.get("total", len(items))}

    async def _list_via_http(self, query_strings: List[str]) -> Dict[str, Any]:
        if not (self._endpoint and self._project_id and (self._session_cookie or self._api_key)):
            raise HTTPException(
                status_code=500,
                detail="Configuração de Appwrite ausente para consulta HTTP.",
            )

        url = (
            f"{self._endpoint}/databases/{self._db_id}/collections/{self._col}/documents"
        )
        headers = {
            "X-Appwrite-Project": self._project_id,
            "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
        params = [("queries[]", q) for q in query_strings]

        try:
            async with httpx.AsyncClient(timeout=30.0) as client:
                response = await client.get(url, headers=headers, params=params)
        except httpx.HTTPError as exc:
            raise HTTPException(
                status_code=502,
                detail="Falha ao se comunicar com Appwrite.",
            ) from exc

        if response.status_code >= 400:
            detail: Any = None
            try:
                payload = response.json()
                if isinstance(payload, dict):
                    detail = payload.get("message") or payload.get("error") or payload
                else:
                    detail = payload
            except ValueError:
                detail = response.text or "Erro desconhecido no Appwrite."
            raise HTTPException(status_code=response.status_code, detail=detail)

        payload = response.json()
        if not isinstance(payload, dict):
            raise HTTPException(status_code=502, detail="Resposta inválida do Appwrite.")

        docs = payload.get("documents", [])
        items = [_map(d).model_dump() for d in docs]
        return {"items": items, "total": payload.get("total", len(items))}

    async def get(self, venda_id: str) -> ProdutoVendaOut:
        try:
            doc = await self._io(self._db.get_document, self._db_id, self._col, venda_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Produto de venda não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        return _map(doc)

    async def create(self, payload: ProdutoVendaCreateRequest) -> ProdutoVendaOut:
        data_payload = payload.data
        data = data_payload.model_dump(exclude_unset=True)
        data["data_validade"] = _serialize_date(data.get("data_validade"))
        preco_venda_value = _parse_float(data.get("preco_venda"), 0.0)
        if preco_venda_value > 0:
            data["preco_final"] = _calculate_preco_final(preco_venda_value)

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

        try:
            doc = await self._io(
                self._db.create_document,
                self._db_id,
                self._col,
                payload.document_id,
                data,
            )
        except AppwriteException as e:
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        return _map(doc)

    async def _create_via_http(self, document_id: str, data: Dict[str, Any]) -> ProdutoVendaOut:
        if not (self._endpoint and self._project_id and (self._session_cookie or self._api_key)):
            raise HTTPException(
                status_code=500,
                detail="Configuração de Appwrite ausente para criação HTTP.",
            )

        url = (
            f"{self._endpoint}/databases/{self._db_id}/collections/{self._col}/documents/"
        )
        headers = {
            "X-Appwrite-Project": self._project_id,
            "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=502,
                detail="Falha ao se comunicar com Appwrite.",
            ) from exc

        if response.status_code >= 400:
            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."
            raise HTTPException(status_code=response.status_code, detail=detail)

        try:
            payload = response.json()
        except ValueError as exc:  # pragma: no cover - defensive
            raise HTTPException(
                status_code=502,
                detail="Resposta inválida do Appwrite.",
            ) from exc

        if not isinstance(payload, dict):
            raise HTTPException(status_code=502, detail="Resposta inválida do Appwrite.")

        return _map(payload)

    async def update(self, venda_id: str, payload: ProdutoVendaUpdate) -> ProdutoVendaOut:
        updates = payload.model_dump(exclude_unset=True)
        if "data_validade" in updates:
            updates["data_validade"] = _serialize_date(updates.get("data_validade"))
        if "preco_venda" in updates:
            preco_venda_value = _parse_float(updates.get("preco_venda"), 0.0)
            if preco_venda_value > 0:
                updates["preco_final"] = _calculate_preco_final(preco_venda_value)
            else:
                updates.pop("preco_final", None)
        elif "preco_final" in updates:
            # ``preco_final`` é derivado de ``preco_venda`` e não deve ser atualizado manualmente.
            updates.pop("preco_final", None)
        try:
            doc = await self._io(
                self._db.update_document,
                self._db_id,
                self._col,
                venda_id,
                updates,
            )
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Produto de venda não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        return _map(doc)

    async def delete(self, venda_id: str) -> None:
        try:
            await self._io(self._db.delete_document, self._db_id, self._col, venda_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Produto de venda não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e


def _serialize_date(value: date | str | None) -> str | None:
    if value is None:
        return None
    if isinstance(value, date):
        return value.isoformat()
    return value
