from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path

import pandas as pd

from app.soat_crosswalk.domain.models import CrosswalkCatalog, CrosswalkRelationship
from app.soat_tariffs.domain.ports import SoatTariffCatalogPort


PROJECT_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_CUPS_MANIFEST = Path(__file__).resolve().parents[1] / "catalogs" / "cups-2026-manifest.json"
DEFAULT_CROSSWALK = Path(__file__).resolve().parents[1] / "catalogs" / "cups-soat-2026.json"


class XlsxCupsCatalog:
    def __init__(self, manifest_path: str | Path = DEFAULT_CUPS_MANIFEST) -> None:
        self.manifest_path = Path(manifest_path)
        self._version = "unloaded"
        self._items: dict[str, str] | None = None
        self._chapters: dict[str, int] | None = None

    @property
    def version(self) -> str:
        self.load()
        return self._version

    def load(self) -> None:
        if self._items is not None:
            return
        try:
            manifest = json.loads(self.manifest_path.read_text(encoding="utf-8"))
            source_path = Path(manifest["local_file"])
            if not source_path.is_absolute():
                source_path = PROJECT_ROOT / source_path
            if not source_path.is_file() or source_path.stat().st_size <= 0:
                raise ValueError("Fuente CUPS 2026 ausente o vacía")
            digest = hashlib.sha256(source_path.read_bytes()).hexdigest()
            if digest != manifest["sha256"]:
                raise ValueError("Hash inválido para la fuente CUPS 2026")
            frame = pd.read_excel(source_path, dtype=str)
            if not {"Codigo", "Nombre", "Descripcion"}.issubset(frame.columns):
                raise ValueError("La fuente CUPS no contiene Codigo, Nombre y Descripcion")
            items: dict[str, str] = {}
            chapters: dict[str, int] = {}
            for code, name, metadata in zip(
                frame["Codigo"], frame["Nombre"], frame["Descripcion"], strict=False
            ):
                normalized = str(code or "").strip().zfill(6)
                description = str(name or "").strip()
                if normalized.isdigit() and len(normalized) == 6 and description:
                    if normalized in items and items[normalized] != description:
                        raise ValueError(f"Código CUPS duplicado con descripciones distintas: {normalized}")
                    items[normalized] = description
                    chapter_match = re.search(r"cap[ií]tulo\s+(\d{1,2})\b", str(metadata or ""), re.I)
                    if chapter_match:
                        chapters[normalized] = int(chapter_match.group(1))
            if not items:
                raise ValueError("La fuente CUPS no contiene códigos válidos")
            self._version = str(manifest["version"])
            self._items = items
            self._chapters = chapters
        except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
            raise ValueError("Manifiesto o fuente CUPS 2026 corruptos") from exc

    def contains(self, code: str) -> bool:
        self.load()
        return str(code) in (self._items or {})

    def description(self, code: str) -> str | None:
        self.load()
        return (self._items or {}).get(str(code))

    def chapter(self, code: str) -> int | None:
        self.load()
        return (self._chapters or {}).get(str(code))

    def classification(self, code: str) -> str | None:
        chapter = self.chapter(code)
        if chapter is None:
            return None
        if 1 <= chapter <= 14:
            return "quirurgico"
        if 15 <= chapter <= 24:
            return "no_quirurgico"
        return None


class JsonCupsSoatCrosswalk:
    def __init__(
        self,
        path: str | Path = DEFAULT_CROSSWALK,
        *,
        cups_catalog: XlsxCupsCatalog | None = None,
        tariff_catalog: SoatTariffCatalogPort | None = None,
    ) -> None:
        self.path = Path(path)
        self.cups_catalog = cups_catalog
        self.tariff_catalog = tariff_catalog
        self._catalog: CrosswalkCatalog | None = None
        self._index: dict[str, list[CrosswalkRelationship]] = {}

    def load(self) -> CrosswalkCatalog:
        if self._catalog is not None:
            return self._catalog
        if not self.path.is_file() or self.path.stat().st_size <= 0:
            raise FileNotFoundError("Cruce CUPS-SOAT 2026 ausente")
        try:
            catalog = CrosswalkCatalog.model_validate_json(self.path.read_text(encoding="utf-8"))
        except Exception as exc:
            raise ValueError("Cruce CUPS-SOAT 2026 corrupto") from exc
        if catalog.effective_from.isoformat() != "2026-01-01":
            raise ValueError("La vigencia del cruce debe iniciar en 2026-01-01")
        self._catalog = catalog
        self._index = {}
        for item in catalog.relationships:
            self._index.setdefault(item.cups_code, []).append(item)
        return catalog

    def find_by_cups(self, cups_code: str) -> list[CrosswalkRelationship]:
        self.load()
        return list(self._index.get(str(cups_code), []))

    def validate(self) -> dict[str, int | str]:
        catalog = self.load()
        if self.cups_catalog is not None:
            self.cups_catalog.load()
        tariff = self.tariff_catalog.load(2026) if self.tariff_catalog is not None else None
        tariff_index = {entry.code: entry for entry in tariff.entries} if tariff else {}
        for item in catalog.relationships:
            if self.cups_catalog is not None and not self.cups_catalog.contains(item.cups_code):
                raise ValueError(f"CUPS desconocido en el cruce: {item.cups_code}")
            if tariff is not None:
                entry = tariff_index.get(item.soat_code)
                if entry is None:
                    raise ValueError(f"SOAT desconocido en el cruce: {item.soat_code}")
                if entry.surgical_group != item.surgical_group:
                    raise ValueError(f"Grupo inconsistente para SOAT {item.soat_code}")
        return {
            "status": catalog.source.status,
            "relationships": len(catalog.relationships),
            "cups_codes": len(self._index),
        }
