from __future__ import annotations

import asyncio
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.core.auth import get_password_hash
from src.modules.usuarios.schemas import UsuarioCreate, UsuarioOut, UsuarioUpdate

_ALLOWED_SORT_FIELDS = {"identificador", "$createdAt"}
_ALLOWED_ORDER_VALUES = {"asc", "desc"}
_DEFAULT_LIMIT = 20


class UsuariosService:
    def __init__(self, databases: Databases, database_id: str, collection_id: str) -> None:
        self._databases = databases
        self._database_id = database_id
        self._collection_id = 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,
        empresas_dados_id: str | None,
        ativo: bool | 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)]

        empresas_filter: str | None = None
        if empresas_dados_id is not None:
            empresas_filter = empresas_dados_id.strip()
            if not empresas_filter:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail="empresas_dados_id cannot be empty.",
                )
            queries.append(Query.equal("empresasDados", empresas_filter))

        ativo_filter = ativo
        if ativo_filter is not None:
            queries.append(Query.equal("ativo", ativo_filter))

        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.")
            if order == "desc":
                queries.append(Query.order_desc(sort))
            else:
                queries.append(Query.order_asc(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
        if q:
            q_value = q.strip()
            if q_value:
                try:
                    queries.append(Query.search("identificador", q_value))
                    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 and not search_performed:
            documents, total = await self._fallback_search(
                q,
                sort,
                order or "asc",
                empresas_filter,
                ativo_filter,
            )
            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, usuario_id: str) -> UsuarioOut:
        try:
            document = await self._run(
                self._databases.get_document,
                self._database_id,
                self._collection_id,
                usuario_id,
            )
        except AppwriteException as exc:
            if exc.code == 404:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Usuario not found.") from exc
            raise HTTPException(status_code=exc.code or 500, detail=exc.message) from exc

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

    async def create(self, payload: UsuarioCreate) -> UsuarioOut:
        raw_data = payload.model_dump(by_alias=True)
        data = {key: value for key, value in raw_data.items() if value is not None}
        identificador = data.get("identificador")
        if isinstance(identificador, str):
            ident_stripped = identificador.strip()
            if not ident_stripped:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Identificador cannot be empty.")
            data["identificador"] = ident_stripped

        senha_value = data.get("senha")
        if isinstance(senha_value, str):
            senha_stripped = senha_value.strip()
            if not senha_stripped:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Senha cannot be empty.")
            data["senha"] = get_password_hash(senha_stripped)

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

    async def update(self, usuario_id: str, payload: UsuarioUpdate) -> UsuarioOut:
        updates = payload.model_dump(exclude_unset=True, by_alias=True)
        if not updates:
            return await self.get(usuario_id)

        if "identificador" in updates and updates["identificador"] is not None:
            ident_value = updates["identificador"].strip()
            if not ident_value:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Identificador cannot be empty.")
            updates["identificador"] = ident_value

        if "senha" in updates and updates["senha"] is not None:
            senha_value = updates["senha"].strip()
            if not senha_value:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Senha cannot be empty.")
            updates["senha"] = get_password_hash(senha_value)

        sanitized_updates = {key: value for key, value in updates.items() if value is not None}

        if not sanitized_updates:
            return await self.get(usuario_id)

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

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

    async def _fallback_search(
        self,
        q: str,
        sort: str | None,
        order: str,
        empresas_filter: str | None,
        ativo_filter: bool | 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(100)],
            )
        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 empresas_filter:
            filtered = [
                doc
                for doc in filtered
                if empresas_filter in _ensure_list(doc.get("empresasDados"))
            ]

        if ativo_filter is not None:
            filtered = [
                doc
                for doc in filtered
                if _matches_bool_filter(doc.get("ativo"), ativo_filter)
            ]

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

        return filtered, len(filtered)

    def _map_document(self, document: Dict[str, Any]) -> Dict[str, Any]:
        return {
            "id": document.get("$id", ""),
            "identificador": document.get("identificador", ""),
            "ativo": bool(document.get("ativo", True)),
            "superadmin": bool(document.get("superadmin", False)),
            "auth_id_appwrite": document.get("auth-id-appwrite"),
            "nome": document.get("nome"),
            "email": document.get("email"),
            "telefone": document.get("telefone"),
            "nivel": document.get("nivel"),
            "registro_completo": bool(document.get("registro-completo", False)),
            "enderecos": _ensure_list(document.get("enderecos")),
            "empresas_dados": _ensure_list(document.get("empresasDados")),
            "nome_social": document.get("nome-social"),
            "cpf": document.get("cpf"),
            "createdAt": document.get("$createdAt"),
            "updatedAt": document.get("$updatedAt"),
        }


def _matches_query(document: Dict[str, Any], query: str) -> bool:
    identificador = document.get("identificador")
    return isinstance(identificador, str) and query.lower() in identificador.lower()


def _sort_key(document: Dict[str, Any], sort: str) -> Any:
    if sort == "$createdAt":
        return document.get("$createdAt")
    return document.get(sort)


def _ensure_list(value: Any) -> List[str]:
    if isinstance(value, list):
        return [str(item) for item in value]
    if value is None:
        return []
    return [str(value)]


def _matches_bool_filter(value: Any, expected: bool) -> bool:
    if isinstance(value, bool):
        return value is expected
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in {"true", "1", "t", "y", "yes"}:
            return expected is True
        if lowered in {"false", "0", "f", "n", "no"}:
            return expected is False
        return False
    if isinstance(value, (int, float)):
        if value in (0, 1):
            return bool(value) is expected
        return False
    return False
