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

from .schemas import UsuarioEmpresaPerfilCreate, UsuarioEmpresaPerfilOut, UsuarioEmpresaPerfilUpdate


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


_ALLOWED_SORT_FIELDS = {"usuarios", "empresas-dados", "usuario-perfis", "$createdAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


class UsuarioEmpresaPerfisService:
    def __init__(
        self,
        *,
        databases: Databases,
        database_id: str,
        collection_id: str,
        usuarios_collection_id: str,
        empresas_collection_id: str,
        perfis_collection_id: str,
    ) -> None:
        self._db = databases
        self._db_id = database_id
        self._col = collection_id
        self._usuarios_col = usuarios_collection_id
        self._empresas_col = empresas_collection_id
        self._perfis_col = perfis_collection_id

    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,
    ) -> 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 empresa_id:
            queries.append(Query.equal("empresas-dados", empresa_id))

        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("usuarios", term),
                        Query.search("empresas-dados", term),
                        Query.search("usuario-perfis", term),
                    ]
                )
            )

        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", [])
        cache: Dict[str, Dict[str, Dict[str, Any]]] = {
            "usuarios": {},
            "empresas-dados": {},
            "usuario-perfis": {},
        }
        items: List[Dict[str, Any]] = []
        for doc in docs:
            mapped = await self._map_with_relations(doc, cache)
            items.append(UsuarioEmpresaPerfilOut.model_validate(mapped).model_dump())
        return {"items": items, "total": res.get("total", len(items))}

    async def get(self, vinculo_id: str, *, empresa_id: str | None = None) -> UsuarioEmpresaPerfilOut:
        try:
            doc = await self._io(self._db.get_document, self._db_id, self._col, vinculo_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Vínculo não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        vinculo_empresa_id = _extract_rel_id(doc.get("empresas-dados"))
        if empresa_id and vinculo_empresa_id != empresa_id:
            raise HTTPException(status_code=403, detail="Vínculo fora do escopo da empresa.")
        cache = {"usuarios": {}, "empresas-dados": {}, "usuario-perfis": {}}
        mapped = await self._map_with_relations(doc, cache)
        return UsuarioEmpresaPerfilOut.model_validate(mapped)

    async def create(
        self, payload: UsuarioEmpresaPerfilCreate, *, empresa_id: str | None = None
    ) -> UsuarioEmpresaPerfilOut:
        data = payload.model_dump(by_alias=True, exclude_unset=True)
        if empresa_id and data.get("empresas-dados") != empresa_id:
            raise HTTPException(status_code=403, detail="Empresa do vínculo difere do escopo informado.")
        await self._ensure_references_exist(data)
        try:
            doc = await self._io(self._db.create_document, self._db_id, self._col, "unique()", data)
        except AppwriteException as e:
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        cache = {"usuarios": {}, "empresas-dados": {}, "usuario-perfis": {}}
        mapped = await self._map_with_relations(doc, cache)
        return UsuarioEmpresaPerfilOut.model_validate(mapped)

    async def update(
        self,
        vinculo_id: str,
        payload: UsuarioEmpresaPerfilUpdate,
        *,
        empresa_id: str | None = None,
    ) -> UsuarioEmpresaPerfilOut:
        updates = payload.model_dump(by_alias=True, exclude_unset=True)
        if updates:
            await self._ensure_references_exist(updates)
        try:
            existing = await self._io(self._db.get_document, self._db_id, self._col, vinculo_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Vínculo não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e

        existing_empresa_id = _extract_rel_id(existing.get("empresas-dados"))
        if empresa_id and existing_empresa_id != empresa_id:
            raise HTTPException(status_code=403, detail="Vínculo fora do escopo da empresa.")
        if empresa_id and updates.get("empresas-dados") not in (None, empresa_id):
            raise HTTPException(
                status_code=403,
                detail="Não é permitido alterar a empresa do vínculo para outro escopo.",
            )

        try:
            doc = await self._io(
                self._db.update_document,
                self._db_id,
                self._col,
                vinculo_id,
                updates,
            )
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Vínculo não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        cache = {"usuarios": {}, "empresas-dados": {}, "usuario-perfis": {}}
        mapped = await self._map_with_relations(doc, cache)
        return UsuarioEmpresaPerfilOut.model_validate(mapped)

    async def delete(self, vinculo_id: str, *, empresa_id: str | None = None) -> None:
        try:
            doc = await self._io(self._db.get_document, self._db_id, self._col, vinculo_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Vínculo não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e

        if empresa_id and _extract_rel_id(doc.get("empresas-dados")) != empresa_id:
            raise HTTPException(status_code=403, detail="Vínculo fora do escopo da empresa.")

        try:
            await self._io(self._db.delete_document, self._db_id, self._col, vinculo_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail="Vínculo não encontrado.") from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e

    async def _ensure_references_exist(self, data: Dict[str, Any]) -> None:
        checks = [
            ("usuarios", self._usuarios_col, "Usuário não encontrado."),
            ("empresas-dados", self._empresas_col, "Empresa não encontrada."),
            ("usuario-perfis", self._perfis_col, "Perfil de usuário não encontrado."),
        ]
        for field, collection, message in checks:
            value = data.get(field)
            if value is None:
                continue
            await self._ensure_document_exists(collection, value, message)

    async def _ensure_document_exists(self, collection_id: str, document_id: str, message: str) -> None:
        try:
            await self._io(self._db.get_document, self._db_id, collection_id, document_id)
        except AppwriteException as e:
            if e.code == 404:
                raise HTTPException(status_code=404, detail=message) from e
            raise HTTPException(status_code=e.code or 500, detail=e.message) from e

    async def _map_with_relations(
        self,
        doc: Dict[str, Any],
        cache: Dict[str, Dict[str, Dict[str, Any]]],
    ) -> Dict[str, Any]:
        usuario_rel = await self._fetch_related(
            cache["usuarios"],
            self._usuarios_col,
            doc.get("usuarios"),
            ("identificador", "nome", "nome-completo"),
        )
        empresa_rel = await self._fetch_related(
            cache["empresas-dados"],
            self._empresas_col,
            doc.get("empresas-dados"),
            ("nome-fantasia", "razao-social", "cnpj"),
        )
        perfil_rel = await self._fetch_related(
            cache["usuario-perfis"],
            self._perfis_col,
            doc.get("usuario-perfis"),
            ("nome", "codigo"),
        )
        return {
            "id": doc.get("$id"),
            "usuarioId": _extract_rel_id(doc.get("usuarios")) or "",
            "empresaId": _extract_rel_id(doc.get("empresas-dados")) or "",
            "perfilId": _extract_rel_id(doc.get("usuario-perfis")) or "",
            "usuario": usuario_rel,
            "empresa": empresa_rel,
            "perfil": perfil_rel,
            "createdAt": doc.get("$createdAt", ""),
            "updatedAt": doc.get("$updatedAt", ""),
        }

    async def _fetch_related(
        self,
        cache: Dict[str, Dict[str, Any]],
        collection_id: str,
        document_id: str | None,
        name_fields: tuple[str, ...],
    ) -> Dict[str, Any]:
        if not document_id:
            return {"id": "", "nome": None}
        if document_id in cache:
            return cache[document_id]
        try:
            doc = await self._io(self._db.get_document, self._db_id, collection_id, document_id)
        except AppwriteException as e:
            if e.code == 404:
                entity = {"id": document_id, "nome": None}
            else:
                raise HTTPException(status_code=e.code or 500, detail=e.message) from e
        else:
            name_value = None
            for field in name_fields:
                value = doc.get(field)
                if isinstance(value, str) and value.strip():
                    name_value = value
                    break
            entity = {"id": doc.get("$id", document_id), "nome": name_value}
        cache[document_id] = entity
        return entity
