from __future__ import annotations

import json
import re
import unicodedata
from typing import Any

from app.case_epicrisis.domain.models import (
    EpicrisisSummaryComposition,
    EpicrisisSummarySourceUnit,
)
from app.llm import LLMOutputKind, LLMStructuredRequest, LLMTask
from app.llm.schemas import EpicrisisSummaryCompositionStructured
from app.services.llm_task_cache import build_llm_task_fingerprint


EPICRISIS_SUMMARY_COMPOSITION_PROMPT_VERSION = "v1"
_ANCHOR_PATTERN = re.compile(
    r"(?<!\w)(?:\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?(?:[+-]\d{2}:\d{2})?)?"
    r"|\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|[A-Za-z]{1,5}[- ]?\d{2,8}|\d+(?:[.,]\d+)?(?:\s*%)?)(?!\w)",
    re.IGNORECASE,
)


class EpicrisisSummaryCompositionError(ValueError):
    pass


def _canonical_anchor(value: str) -> str:
    normalized = unicodedata.normalize("NFKD", str(value or ""))
    return re.sub(r"[^a-z0-9]", "", normalized.casefold())


def extract_explicit_anchors(text: str) -> set[str]:
    return {anchor for raw in _ANCHOR_PATTERN.findall(str(text or "")) if (anchor := _canonical_anchor(raw))}


def validate_summary_composition(
    composition: EpicrisisSummaryComposition,
    sources: list[EpicrisisSummarySourceUnit],
) -> None:
    expected_ids = {source.identifier for source in sources}
    covered_ids = {str(value).strip() for value in composition.covered_source_ids if str(value).strip()}
    if covered_ids != expected_ids:
        raise EpicrisisSummaryCompositionError("La composición no cubre todas las fuentes.")

    source_anchors = set().union(*(extract_explicit_anchors(source.summary) for source in sources))
    narrative_anchors = extract_explicit_anchors(composition.narrative)
    missing_anchors = source_anchors - narrative_anchors
    if missing_anchors:
        raise EpicrisisSummaryCompositionError("La composición omitió anclas clínicas explícitas.")


class LLMEpicrisisSummaryComposer:
    def __init__(self, llm_router: Any, cache_repository: Any = None) -> None:
        self._llm_router = llm_router
        self._cache_repository = cache_repository

    def compose(
        self,
        *,
        username: str,
        sources: list[EpicrisisSummarySourceUnit],
    ) -> EpicrisisSummaryComposition:
        if self._llm_router is None or len(sources) < 2:
            raise EpicrisisSummaryCompositionError("No hay proveedor o fuentes suficientes para componer.")

        route = self._resolve_route()
        fingerprint = self._fingerprint(route=route, sources=sources)
        if fingerprint and self._cache_repository is not None:
            cached = self._cache_repository.get(
                username=username,
                task=LLMTask.EPICRISIS_SUMMARY_COMPOSITION.value,
                fingerprint=fingerprint,
            )
            if cached and isinstance(cached.get("payload"), dict):
                composition = EpicrisisSummaryComposition.model_validate(cached["payload"])
                validate_summary_composition(composition, sources)
                return composition

        source_payload = [source.model_dump(mode="json") for source in sources]
        response = self._llm_router.generate_structured(
            LLMStructuredRequest(
                task=LLMTask.EPICRISIS_SUMMARY_COMPOSITION,
                prompt=(
                    "Integra las unidades fuente en una narración clínica cronológica nueva. Conserva toda la "
                    "información clínica de ambos resúmenes: no resumas, no reduzcas por longitud y no inventes datos. "
                    "Mantén literalmente fechas, códigos y valores numéricos. Devuelve en fc todos los identificadores "
                    "de fuente recibidos, únicamente cuando su contenido esté cubierto en nc. Usa las fechas disponibles "
                    "para ordenar; si una fecha falta, expresa la secuencia sin inventarla.\n\n"
                    f"Unidades fuente:\n{json.dumps(source_payload, ensure_ascii=False)}"
                ),
                output_model=EpicrisisSummaryCompositionStructured,
                output_kind=LLMOutputKind.STRUCTURED_OBJECT,
                system_prompt=(
                    "Eres un redactor clínico trazable. Responde solo JSON válido y preserva íntegramente el contenido "
                    "de las fuentes proporcionadas."
                ),
                metadata={
                    "operation": "epicrisis_summary_composition",
                    "prompt_version": EPICRISIS_SUMMARY_COMPOSITION_PROMPT_VERSION,
                    "source_unit_ids": [source.identifier for source in sources],
                },
            )
        )
        structured = EpicrisisSummaryCompositionStructured.model_validate(response.content)
        composition = EpicrisisSummaryComposition(
            narrative=structured.narrativa_cronologica,
            covered_source_ids=structured.fuentes_cubiertas,
        )
        validate_summary_composition(composition, sources)

        if fingerprint and self._cache_repository is not None:
            self._cache_repository.upsert(
                username=username,
                task=LLMTask.EPICRISIS_SUMMARY_COMPOSITION.value,
                fingerprint=fingerprint,
                provider=str(response.provider or route.provider),
                model=str(response.model or route.model),
                payload=composition.model_dump(mode="json"),
                metadata={
                    "prompt_version": EPICRISIS_SUMMARY_COMPOSITION_PROMPT_VERSION,
                    "source_unit_ids": [source.identifier for source in sources],
                },
            )
        return composition

    def _resolve_route(self):
        if not hasattr(self._llm_router, "_resolve_route"):
            raise EpicrisisSummaryCompositionError("No se pudo resolver el modelo de composición.")
        try:
            return self._llm_router._resolve_route(
                LLMTask.EPICRISIS_SUMMARY_COMPOSITION,
                metadata={"operation": "epicrisis_summary_composition"},
            )
        except TypeError:
            return self._llm_router._resolve_route(LLMTask.EPICRISIS_SUMMARY_COMPOSITION)

    def _fingerprint(self, *, route: Any, sources: list[EpicrisisSummarySourceUnit]) -> str:
        if self._cache_repository is None:
            return ""
        return build_llm_task_fingerprint(
            task=LLMTask.EPICRISIS_SUMMARY_COMPOSITION.value,
            provider=str(route.provider),
            model=str(route.model),
            cache_version=getattr(self._cache_repository, "cache_version", "v1"),
            prompt_version=EPICRISIS_SUMMARY_COMPOSITION_PROMPT_VERSION,
            schema_version="v1",
            payload=[source.model_dump(mode="json") for source in sources],
        )
