from __future__ import annotations

import hashlib
import json
import os
from collections.abc import Callable
from pathlib import Path
from threading import RLock
from typing import Any

from app.config import config, require_env


MODEL_NOT_ALLOWED_MESSAGE = "Los proveedores legacy Groq/OpenAI están deshabilitados."


def _resolve_path(base_dir: Path, path_value: str) -> str:
    path = Path(path_value)
    return str(path if path.is_absolute() else (base_dir / path))


def validate_faiss_directory(index_dir: str | Path) -> Path:
    path = Path(index_dir)
    if not path.is_dir():
        raise FileNotFoundError(f"Directorio FAISS no disponible: {path}")
    for filename in ("index.faiss", "index.pkl"):
        artifact = path / filename
        if not artifact.is_file() or artifact.stat().st_size <= 0:
            raise ValueError(f"Artefacto FAISS ausente, irregular o vacío: {artifact}")
    manifest_path = path / "manifest.json"
    if manifest_path.exists():
        if not manifest_path.is_file() or manifest_path.stat().st_size <= 0:
            raise ValueError(f"Manifiesto FAISS irregular o vacío: {manifest_path}")
        try:
            manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
            artifact_hashes = manifest["artifacts"]
            for filename in ("index.faiss", "index.pkl"):
                digest = hashlib.sha256((path / filename).read_bytes()).hexdigest()
                if digest != artifact_hashes[filename]:
                    raise ValueError(f"Hash inválido para artefacto FAISS: {path / filename}")
        except (KeyError, TypeError, json.JSONDecodeError) as exc:
            raise ValueError(f"Manifiesto FAISS corrupto: {manifest_path}") from exc
    return path


def validate_loaded_faiss(vectorstore: Any, *, name: str) -> None:
    index = getattr(vectorstore, "index", None)
    ntotal = int(getattr(index, "ntotal", 0) or 0)
    dimension = int(getattr(index, "d", 0) or 0)
    mapping = getattr(vectorstore, "index_to_docstore_id", None)
    docstore = getattr(getattr(vectorstore, "docstore", None), "_dict", None)
    if ntotal <= 0 or dimension <= 0:
        raise ValueError(f"Índice FAISS {name} vacío o sin dimensión válida")
    if not isinstance(mapping, dict) or len(mapping) != ntotal:
        raise ValueError(f"Mapeo FAISS/docstore incompatible para {name}")
    if not isinstance(docstore, dict) or len(docstore) < ntotal:
        raise ValueError(f"Docstore FAISS incompleto para {name}")
    embedding_function = getattr(vectorstore, "embedding_function", None)
    embed_query = getattr(embedding_function, "embed_query", None)
    if callable(embed_query):
        probe = embed_query("validación dimensional del recurso")
        if len(probe) != dimension:
            raise ValueError(
                f"Dimensión incompatible para {name}: índice={dimension}, embeddings={len(probe)}"
            )


class LazyResourceProxy:
    def __init__(
        self,
        loader: Callable[[], Any],
        *,
        available: Callable[[], bool] | None = None,
        name: str,
    ) -> None:
        self._loader = loader
        self._available = available
        self._name = name
        self._resolved: Any | None = None
        self._lock = RLock()

    def __bool__(self) -> bool:
        if self._available is None:
            return True
        return bool(self._available())

    def __getattr__(self, item: str) -> Any:
        return getattr(self.force_load(), item)

    def __repr__(self) -> str:
        return f"<LazyResourceProxy name={self._name!r}>"

    def force_load(self) -> Any:
        if self._resolved is not None:
            return self._resolved
        with self._lock:
            if self._resolved is None:
                self._resolved = self._loader()
            return self._resolved

    def reset(self) -> None:
        with self._lock:
            self._resolved = None


class HeavyResourceRegistry:
    def __init__(self, base_dir: Path) -> None:
        self.base_dir = Path(base_dir)
        self._lock = RLock()
        self._resources: dict[str, Any] = {}
        self._soat_index_dir = self.base_dir / "soat_faiss"
        self._soat_procedures_index_dir = self.base_dir / "soat_procedures_faiss"

    def groq_available(self) -> bool:
        return self.legacy_llm_providers_enabled() and bool(str(config.GROQ_API_KEY or "").strip())

    def gemini_available(self) -> bool:
        return bool(str(config.GEMINI_API_KEY or "").strip())

    def openai_stub_enabled(self) -> bool:
        return self.legacy_llm_providers_enabled() and bool(config.OPENAI_STUB_ENABLED)

    def legacy_llm_providers_enabled(self) -> bool:
        return bool(config.LEGACY_LLM_PROVIDERS_ENABLED)

    def soat_catalog_available(self) -> bool:
        try:
            from app.soat_tariffs.infrastructure.json_catalog import JsonSoatTariffCatalog

            JsonSoatTariffCatalog().validate_all()
        except (FileNotFoundError, ValueError):
            return False
        return True

    def soat_available(self) -> bool:
        try:
            validate_faiss_directory(self._soat_index_dir)
            validate_faiss_directory(self._soat_procedures_index_dir)
        except (FileNotFoundError, ValueError):
            return False
        return self.soat_catalog_available()

    def cie10_available(self) -> bool:
        cie10_index_path = Path(os.getenv("CIE10_FAISS_INDEX", "faiss_principal_jerarquico"))
        cie10_estructura_path = Path(os.getenv("CIE10_ESTRUCTURA_JSON", "cie10_estructura_completa.json"))
        resolved_index = cie10_index_path if cie10_index_path.is_absolute() else (self.base_dir / cie10_index_path)
        resolved_structure = (
            cie10_estructura_path if cie10_estructura_path.is_absolute() else (self.base_dir / cie10_estructura_path)
        )
        return resolved_index.exists() and resolved_structure.exists()

    def cups_available(self) -> bool:
        cups_index_path = Path(os.getenv("CUPS_FAISS_INDEX", "cups_faiss"))
        resolved_index = cups_index_path if cups_index_path.is_absolute() else (self.base_dir / cups_index_path)
        try:
            validate_faiss_directory(resolved_index)
        except (FileNotFoundError, ValueError):
            return False
        return True

    def set_soat_index_dir(self, index_dir: str | Path) -> None:
        with self._lock:
            self._soat_index_dir = Path(index_dir)
            self._resources.pop("soat", None)

    def _require_groq_api_key(self) -> str:
        return require_env(config.GROQ_API_KEY, "GROQ_API_KEY")

    def _get_or_create(self, key: str, loader: Callable[[], Any]) -> Any:
        resource = self._resources.get(key)
        if resource is not None:
            return resource
        with self._lock:
            resource = self._resources.get(key)
            if resource is None:
                resource = loader()
                self._resources[key] = resource
            return resource

    def get_groq_client(self) -> Any:
        def _loader() -> Any:
            if not self.legacy_llm_providers_enabled():
                raise RuntimeError(MODEL_NOT_ALLOWED_MESSAGE)
            from groq import Groq

            from app.core.logging import InstrumentedGroqClient

            return InstrumentedGroqClient(
                Groq(api_key=self._require_groq_api_key()),
                provider="groq",
                service="app_services",
            )

        return self._get_or_create("groq", _loader)

    def get_gemini_client(self) -> Any:
        def _loader() -> Any:
            from app.core.gemini_client import get_gemini_model

            return get_gemini_model(
                api_key=require_env(config.GEMINI_API_KEY, "GEMINI_API_KEY"),
                model_name=config.GEMINI_MODEL_DEFAULT,
            )

        return self._get_or_create("gemini", _loader)

    def get_groq_provider(self) -> Any:
        def _loader() -> Any:
            if not self.legacy_llm_providers_enabled():
                raise RuntimeError(MODEL_NOT_ALLOWED_MESSAGE)
            from app.llm.providers import GroqAdapter

            return GroqAdapter(
                api_key=config.GROQ_API_KEY,
                default_model=config.GROQ_MODEL_DEFAULT,
                service="heavy_resources_groq_provider",
            )

        return self._get_or_create("llm_groq_provider", _loader)

    def get_gemini_provider(self) -> Any:
        def _loader() -> Any:
            from app.llm.providers import GeminiAdapter

            return GeminiAdapter(
                api_key=config.GEMINI_API_KEY,
                default_model=config.GEMINI_MODEL_DEFAULT,
                service="heavy_resources_gemini_provider",
            )

        return self._get_or_create("llm_gemini_provider", _loader)

    def get_openai_provider(self) -> Any:
        def _loader() -> Any:
            if not self.legacy_llm_providers_enabled():
                raise RuntimeError(MODEL_NOT_ALLOWED_MESSAGE)
            from app.llm.providers import OpenAIAdapterStub

            return OpenAIAdapterStub(enabled=self.openai_stub_enabled())

        return self._get_or_create("llm_openai_provider", _loader)

    def get_model_selection_policy(self) -> Any:
        def _loader() -> Any:
            from app.llm.routing import DefaultModelSelectionPolicy

            return DefaultModelSelectionPolicy()

        return self._get_or_create("llm_policy", _loader)

    def get_llm_router(self) -> Any:
        def _loader() -> Any:
            from app.llm.routing import DefaultModelRouter

            providers = {
                "gemini": self.get_gemini_provider(),
            }
            if self.legacy_llm_providers_enabled():
                providers["groq"] = self.get_groq_provider()
                providers["openai"] = self.get_openai_provider()
            return DefaultModelRouter(
                providers=providers,
                policy=self.get_model_selection_policy(),
                service="heavy_resources_llm_router",
            )

        return self._get_or_create("llm_router", _loader)

    def get_cie10_retriever(self) -> Any:
        def _loader() -> Any:
            from modules.processing.cie10.RANGES_HTML import CIE10Retriever

            cie10_index_path = os.getenv("CIE10_FAISS_INDEX", "faiss_principal_jerarquico")
            cie10_estructura_path = os.getenv("CIE10_ESTRUCTURA_JSON", "cie10_estructura_completa.json")
            return CIE10Retriever(
                faiss_index_path=_resolve_path(self.base_dir, cie10_index_path),
                estructura_json_path=_resolve_path(self.base_dir, cie10_estructura_path),
                groq_api_key=config.GROQ_API_KEY if self.legacy_llm_providers_enabled() else None,
                groq_model=config.GROQ_MODEL_CIE10,
                llm_router=self.get_llm_router(),
            )

        return self._get_or_create("cie10", _loader)

    def get_cups_retriever(self) -> Any:
        def _loader() -> Any:
            from modules.processing.cie10.RANGES_CUPS import CUPSRetriever

            cups_index_path = os.getenv("CUPS_FAISS_INDEX", "cups_faiss")
            validate_faiss_directory(_resolve_path(self.base_dir, cups_index_path))
            retriever = CUPSRetriever(
                faiss_index_path=_resolve_path(self.base_dir, cups_index_path),
                groq_api_key=config.GROQ_API_KEY if self.legacy_llm_providers_enabled() else None,
                groq_model=config.GROQ_MODEL_CUPS,
                llm_router=self.get_llm_router(),
            )
            if retriever.db is None:
                raise ValueError("El índice FAISS CUPS no pudo cargarse")
            validate_loaded_faiss(retriever.db, name="cups_2026")
            return retriever

        return self._get_or_create("cups", _loader)

    def get_soat_retriever(self) -> Any:
        def _loader() -> Any:
            from modules.processing.soat.soat_retriever import SOATRetriever

            validate_faiss_directory(self._soat_index_dir)
            retriever = SOATRetriever(
                faiss_index_path=str(self._soat_index_dir),
                groq_api_key=config.GROQ_API_KEY if self.legacy_llm_providers_enabled() else None,
                groq_model=config.GROQ_MODEL_SOAT,
                llm_router=self.get_llm_router(),
            )
            validate_loaded_faiss(retriever.db, name="manual_soat")
            return retriever

        return self._get_or_create("soat", _loader)

    def get_soat_tariff_catalog(self) -> Any:
        def _loader() -> Any:
            from app.soat_tariffs.infrastructure.json_catalog import JsonSoatTariffCatalog

            catalog = JsonSoatTariffCatalog()
            catalog.validate_all()
            return catalog

        return self._get_or_create("soat_tariffs", _loader)

    def get_cups_2026_catalog(self) -> Any:
        def _loader() -> Any:
            from app.soat_crosswalk.infrastructure.catalogs import XlsxCupsCatalog

            catalog = XlsxCupsCatalog()
            catalog.load()
            return catalog

        return self._get_or_create("cups_2026_catalog", _loader)

    def get_cups_soat_crosswalk(self) -> Any:
        def _loader() -> Any:
            from app.soat_crosswalk.infrastructure.catalogs import JsonCupsSoatCrosswalk

            adapter = JsonCupsSoatCrosswalk(
                cups_catalog=self.get_cups_2026_catalog(),
                tariff_catalog=self.get_soat_tariff_catalog(),
            )
            adapter.validate()
            return adapter

        return self._get_or_create("cups_soat_crosswalk", _loader)

    def get_soat_procedures_retriever(self) -> Any:
        def _loader() -> Any:
            from langchain_community.vectorstores import FAISS

            from modules.processing.embeddings import get_hf_embeddings

            path = validate_faiss_directory(self._soat_procedures_index_dir)
            embeddings = get_hf_embeddings(os.getenv("EMBEDDINGS_MODEL", "intfloat/e5-small-v2"))
            vectorstore = FAISS.load_local(
                str(path),
                embeddings,
                allow_dangerous_deserialization=True,
            )
            validate_loaded_faiss(vectorstore, name="soat_procedures")
            return vectorstore

        return self._get_or_create("soat_procedures", _loader)

    def warmup(self, *resource_names: str) -> dict[str, str]:
        requested = set(resource_names or ("groq", "gemini", "cie10", "cups", "soat"))
        results: dict[str, str] = {}
        for name in requested:
            try:
                if name == "groq":
                    if not self.groq_available():
                        results[name] = "skipped"
                        continue
                    self.get_groq_client()
                elif name == "gemini":
                    if not self.gemini_available():
                        results[name] = "skipped"
                        continue
                    self.get_gemini_client()
                elif name == "cie10":
                    self.get_cie10_retriever()
                elif name == "cups":
                    self.get_cups_retriever()
                    self.get_cups_2026_catalog()
                elif name == "soat":
                    self.get_soat_retriever()
                    self.get_soat_tariff_catalog()
                    self.get_cups_soat_crosswalk()
                    self.get_soat_procedures_retriever()
                else:
                    results[name] = "unknown"
                    continue
                results[name] = "ready"
            except Exception as exc:
                results[name] = f"error:{exc.__class__.__name__}"
        return results

    def status(self) -> dict[str, bool]:
        return {
            "legacy_llm_providers_enabled": self.legacy_llm_providers_enabled(),
            "groq_configured": self.groq_available(),
            "gemini_configured": self.gemini_available(),
            "openai_stub_enabled": self.openai_stub_enabled(),
            "cie10_available": self.cie10_available(),
            "cups_available": self.cups_available(),
            "soat_index_present": self._soat_index_dir.exists(),
            "soat_catalog_available": self.soat_catalog_available(),
            "llm_groq_provider_loaded": "llm_groq_provider" in self._resources,
            "llm_gemini_provider_loaded": "llm_gemini_provider" in self._resources,
            "llm_openai_provider_loaded": "llm_openai_provider" in self._resources,
            "llm_router_loaded": "llm_router" in self._resources,
            "cie10_loaded": "cie10" in self._resources,
            "cups_loaded": "cups" in self._resources,
            "cups_2026_catalog_loaded": "cups_2026_catalog" in self._resources,
            "soat_loaded": "soat" in self._resources,
            "soat_tariffs_loaded": "soat_tariffs" in self._resources,
            "cups_soat_crosswalk_loaded": "cups_soat_crosswalk" in self._resources,
            "soat_procedures_loaded": "soat_procedures" in self._resources,
        }

    def describe_status(self) -> dict[str, dict[str, bool]]:
        return {
            "groq_provider": {
                "enabled": self.legacy_llm_providers_enabled(),
                "configured": self.groq_available(),
                "loaded": "llm_groq_provider" in self._resources,
            },
            "gemini_provider": {
                "enabled": True,
                "configured": self.gemini_available(),
                "loaded": "llm_gemini_provider" in self._resources,
            },
            "openai_stub": {
                "enabled": self.openai_stub_enabled(),
                "loaded": "llm_openai_provider" in self._resources,
            },
            "llm_router": {
                "loaded": "llm_router" in self._resources,
            },
            "cie10_resource": {
                "available": self.cie10_available(),
                "loaded": "cie10" in self._resources,
            },
            "cups_resource": {
                "available": self.cups_available(),
                "loaded": "cups" in self._resources and "cups_2026_catalog" in self._resources,
            },
            "cups_2026_catalog": {
                "available": (self.base_dir / "TablaReferencia_CUPS__1.xlsx").is_file(),
                "loaded": "cups_2026_catalog" in self._resources,
            },
            "soat_manual": {
                "available": self._faiss_available(self._soat_index_dir),
                "loaded": "soat" in self._resources,
            },
            "soat_tariff_catalog": {
                "available": self.soat_catalog_available(),
                "loaded": "soat_tariffs" in self._resources,
            },
            "cups_soat_crosswalk": {
                "available": (self.base_dir / "app/soat_crosswalk/catalogs/cups-soat-2026.json").is_file(),
                "loaded": "cups_soat_crosswalk" in self._resources,
            },
            "soat_procedures_faiss": {
                "available": self._faiss_available(self._soat_procedures_index_dir),
                "loaded": "soat_procedures" in self._resources,
            },
            "soat_resource": {
                "available": self.soat_available(),
                "loaded": all(
                    name in self._resources
                    for name in (
                        "soat",
                        "soat_tariffs",
                        "cups_soat_crosswalk",
                        "soat_procedures",
                    )
                ),
            },
        }

    @staticmethod
    def _faiss_available(path: Path) -> bool:
        try:
            validate_faiss_directory(path)
        except (FileNotFoundError, ValueError):
            return False
        return True
