from collections.abc import Callable
from dataclasses import dataclass
from time import sleep
from typing import Any, cast

import pytz
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, ConfigDict, Field

from app.clinical_pipeline.domain.errors import HistoriaSummaryCoverageError
from app.clinical_pipeline.domain.models import (
    HistoriaProcessingMetrics,
    ProcessingFailureSource,
)
from app.config import config
from app.llm import (
    LLMOutputKind,
    LLMProviderError,
    LLMStructuredRequest,
    LLMTask,
    LLMTextRequest,
)
from app.llm.providers import GeminiAdapter, GroqAdapter, OpenAIAdapterStub
from app.llm.renderers import render_clinical_document_html
from app.llm.routing import DefaultModelRouter, DefaultModelSelectionPolicy
from app.llm.schemas import AntecedenteClinico, HistoriaClinicaStructured, dump_clinical_model
from app.services.clinical_extraction_quality import assess_clinical_extraction
from app.services.clinical_structured_extraction import ClinicalStructuredExtractionService
from app.services.context_budgeting import deduplicate_texts, limit_items, truncate_text
from app.services.deterministic_signals import extract_deterministic_signals
from app.services.historia_antecedentes import (
    antecedent_description_key,
    antecedentes_to_flat_list,
    antecedentes_to_prompt_text,
    build_historia_antecedent_context,
    extract_historia_antecedentes_structured,
    resolve_historia_antecedentes,
)
from app.services.historia_recomendaciones import build_historia_recommendation_context
from app.services.historia_summary import (
    calculate_historia_summary_policy,
    choose_historia_resumen,
    extract_historia_diagnosticos_text,
    extract_historia_medicamentos_text,
    extract_historia_motivo_text,
    extract_historia_procedimientos_text,
    extract_historia_resumen_text,
)
from app.services.llm_task_cache import build_llm_task_fingerprint


app = FastAPI()
colombia_tz = pytz.timezone("America/Bogota")
_HISTORIA_SUMMARY_PROMPT_VERSION = "v6"
_HISTORIA_PROMPT_PACK_VERSION = "v8"


@dataclass(frozen=True)
class PreparedHistoriaPrompt:
    prompt_text: str
    detailed_summary: str = ""
    processing_metrics: HistoriaProcessingMetrics | None = None


@dataclass(frozen=True)
class _SummaryGenerationFailure:
    source: ProcessingFailureSource
    provider: str
    model: str
    error_kind: str
    retryable: bool
    attempts: int
    retry_after_seconds: float = 0.0


def _parse_retry_delays(value: Any) -> list[float]:
    candidates = value if isinstance(value, (list, tuple)) else str(value or "").split(",")
    delays: list[float] = []
    for candidate in candidates:
        try:
            delays.append(max(0.0, float(candidate)))
        except (TypeError, ValueError):
            continue
    return delays or [2.0, 5.0]


def _resolve_summary_route(llm_router: Any, task: LLMTask) -> tuple[str, str]:
    if llm_router is None or not hasattr(llm_router, "_resolve_route"):
        return "gemini", ""
    try:
        route = llm_router._resolve_route(task)
        return str(route.provider or ""), str(route.model or "")
    except Exception:
        return "gemini", ""


def _retry_after_seconds(error: LLMProviderError) -> float:
    try:
        return max(0.0, float(error.details.get("retry_after_seconds") or 0))
    except (TypeError, ValueError):
        return 0.0


def _generate_historia_summary_with_retries(
    *,
    llm_router: Any,
    prompt: str,
    task: LLMTask,
    cache_repository: Any = None,
    username: str = "",
    max_retries: int,
    retry_delays: list[float],
    sleep_fn: Callable[[float], None],
) -> tuple[str, _SummaryGenerationFailure | None]:
    provider, model = _resolve_summary_route(llm_router, task)
    last_failure: _SummaryGenerationFailure | None = None
    total_attempts = max(0, max_retries) + 1
    for attempt_index in range(total_attempts):
        if attempt_index > 0:
            delay_index = min(attempt_index - 1, len(retry_delays) - 1)
            configured_delay = retry_delays[delay_index] if retry_delays else 0.0
            retry_after = last_failure.retry_after_seconds if last_failure else 0.0
            sleep_fn(max(configured_delay, retry_after))
        try:
            result = _generate_historia_summary_with_cache(
                llm_router=llm_router,
                prompt=prompt,
                task=task,
                cache_repository=cache_repository,
                username=username,
            )
            if result:
                return result, None
            last_failure = _SummaryGenerationFailure(
                source="external_provider",
                provider=provider,
                model=model,
                error_kind="empty_response",
                retryable=True,
                attempts=attempt_index + 1,
            )
        except LLMProviderError as exc:
            retry_after = _retry_after_seconds(exc)
            external = exc.kind.value not in {
                "invalid_configuration",
                "unsupported_capability",
            }
            last_failure = _SummaryGenerationFailure(
                source="external_provider" if external else "internal",
                provider=exc.provider or provider,
                model=exc.model or model,
                error_kind=exc.kind.value,
                retryable=bool(external and exc.retryable),
                attempts=attempt_index + 1,
                retry_after_seconds=retry_after,
            )
            if not external or not exc.retryable:
                break
        except Exception as exc:
            last_failure = _SummaryGenerationFailure(
                source="internal",
                provider=provider,
                model=model,
                error_kind=exc.__class__.__name__,
                retryable=False,
                attempts=attempt_index + 1,
            )
            break
    return "", last_failure


def _build_historia_llm_router() -> DefaultModelRouter:
    providers = {
        "gemini": GeminiAdapter(
            api_key=config.GEMINI_API_KEY,
            default_model=config.GEMINI_MODEL_DEFAULT,
            service="historia_clinica_gemini_provider",
        ),
    }
    if config.LEGACY_LLM_PROVIDERS_ENABLED:
        providers["groq"] = GroqAdapter(
            api_key=config.GROQ_API_KEY,
            default_model=config.GROQ_MODEL_DEFAULT,
            service="historia_clinica_groq_provider",
        )
        providers["openai"] = OpenAIAdapterStub(enabled=False)
    return DefaultModelRouter(
        providers=providers,
        policy=DefaultModelSelectionPolicy(),
        service="historia_clinica_router",
    )


def _chunk_text(text: str, size: int, *, overlap: int = 0) -> list[str]:
    """Divide cronológicamente priorizando límites de página o párrafo."""

    source = str(text or "")
    if not source:
        return []
    normalized_size = max(500, int(size or 0))
    normalized_overlap = min(max(0, int(overlap or 0)), normalized_size // 4)
    chunks: list[str] = []
    start = 0
    while start < len(source):
        hard_end = min(len(source), start + normalized_size)
        end = hard_end
        if hard_end < len(source):
            search_start = start + int(normalized_size * 0.7)
            candidates = [
                source.rfind("\n\f\n", search_start, hard_end),
                source.rfind("\n\n", search_start, hard_end),
                source.rfind("\n", search_start, hard_end),
            ]
            boundary = max(candidates)
            if boundary > start:
                end = boundary + 1
        chunk = source[start:end]
        if chunk:
            chunks.append(chunk)
        if end >= len(source):
            break
        next_start = max(start + 1, end - normalized_overlap)
        start = next_start
    return chunks


def _budget_historia_text(text: str, *, max_chars: int) -> str:
    lines, _ = deduplicate_texts(str(text or "").splitlines())
    budgeted_lines, _ = limit_items(lines, max_items=max(1, max_chars // 40) if max_chars else None)
    compacted = "\n".join(budgeted_lines)
    truncated, _ = truncate_text(compacted, max_chars=max_chars)
    return truncated


def _format_section(title: str, value: str) -> str:
    normalized = str(value or "").strip()
    if not normalized:
        return ""
    return f"[{title}]\n{normalized}"


def _build_identity_section(snapshot) -> str:
    values = []
    if snapshot.patient_name:
        values.append(f"Nombre del paciente: {snapshot.patient_name}")
    if snapshot.patient_id:
        values.append(f"Documento: {snapshot.patient_id}")
    if snapshot.case_number:
        values.append(f"Caso: {snapshot.case_number}")
    if snapshot.dates:
        values.append("Fechas relevantes: " + ", ".join(snapshot.dates[:5]))
    return "\n".join(values)


def _build_remaining_context(compacted_text: str, prioritized_sections: list[str], *, max_chars: int) -> str:
    if not compacted_text:
        return ""
    prioritized_lines = {
        line.strip().casefold()
        for section in prioritized_sections
        for line in section.splitlines()
        if line.strip() and not line.startswith("[")
    }
    remaining_lines = []
    for line in compacted_text.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        if stripped.casefold() in prioritized_lines:
            continue
        remaining_lines.append(stripped)
    remaining_text, _ = truncate_text("\n".join(remaining_lines), max_chars=max_chars)
    return remaining_text


def _should_expand_historia_summary(explicit_summary: str) -> bool:
    normalized = str(explicit_summary or "").strip()
    if not normalized:
        return False
    word_count = len(normalized.split())
    return word_count <= 45 or normalized == normalized.upper()


def _build_historia_prompt_pack(
    descripcion: str,
    *,
    snapshot,
    compacted_text: str,
    max_chars: int,
    previous_summary: str = "",
) -> str:
    identity_section = _build_identity_section(snapshot)
    motivo_section = extract_historia_motivo_text(descripcion)
    resumen_section = extract_historia_resumen_text(descripcion)
    diagnosticos_section = extract_historia_diagnosticos_text(descripcion)
    procedimientos_section = extract_historia_procedimientos_text(descripcion)
    medicamentos_section = extract_historia_medicamentos_text(descripcion)
    antecedentes_section = antecedentes_to_prompt_text(extract_historia_antecedentes_structured(descripcion))
    recomendaciones_section = build_historia_recommendation_context(descripcion)

    prioritized_sections = [
        _format_section(
            "PACK HISTORIA CLINICA",
            "version=v5/v6" if _HISTORIA_PROMPT_PACK_VERSION == "v6" else f"version={_HISTORIA_PROMPT_PACK_VERSION}",
        ),
        _format_section("IDENTIDAD CLINICA BASICA", identity_section),
        _format_section("MOTIVO DE CONSULTA", motivo_section),
        _format_section("RECOMENDACIONES MEDICAS CANDIDATAS", recomendaciones_section),
        _format_section("ANTECEDENTES ESTRUCTURADOS DETERMINISTAS", antecedentes_section),
        _format_section("RESUMEN CLINICO", resumen_section),
        _format_section("DIAGNOSTICOS", diagnosticos_section),
        _format_section("PROCEDIMIENTOS", procedimientos_section),
        _format_section("MEDICAMENTOS", medicamentos_section),
    ]
    if previous_summary:
        prioritized_sections.append(
            _format_section("RESUMEN PREVIO DEL DOCUMENTO COMPLETO", previous_summary)
        )
    prioritized_sections = [section for section in prioritized_sections if section]
    fallback_context = _build_remaining_context(
        compacted_text,
        prioritized_sections,
        max_chars=max(800, max_chars // 3),
    )
    if fallback_context:
        prioritized_sections.append(_format_section("CONTEXTO ADICIONAL PRIORIZADO", fallback_context))
    packed_text = "\n\n".join(prioritized_sections)
    budgeted_prompt, _ = truncate_text(packed_text, max_chars=max_chars)
    return budgeted_prompt


def _resolve_historia_cache_context(*, llm_router, task: LLMTask, username: str) -> dict[str, str] | None:
    normalized_username = str(username or "").strip()
    if not normalized_username or llm_router is None or not hasattr(llm_router, "_resolve_route"):
        return None
    route = llm_router._resolve_route(task)
    return {"username": normalized_username, "provider": route.provider, "model": route.model}


def _generate_historia_summary_with_cache(
    *,
    llm_router,
    prompt: str,
    task: LLMTask,
    cache_repository=None,
    username: str = "",
) -> str:
    cache_context = _resolve_historia_cache_context(llm_router=llm_router, task=task, username=username)
    fingerprint = None
    if cache_repository is not None and cache_context is not None:
        fingerprint = build_llm_task_fingerprint(
            task=task.value,
            provider=cache_context["provider"],
            model=cache_context["model"],
            cache_version=getattr(cache_repository, "cache_version", "v1"),
            prompt_version=_HISTORIA_SUMMARY_PROMPT_VERSION,
            payload={"prompt": prompt},
        )
        cached = cache_repository.get(
            username=cache_context["username"],
            task=task.value,
            fingerprint=fingerprint,
        )
        if cached and isinstance(cached.get("payload"), dict):
            cached_text = str((cached["payload"] or {}).get("text") or "").strip()
            if cached_text:
                return cached_text

    response = llm_router.generate_text(
        LLMTextRequest(
            task=task,
            prompt=prompt,
            system_prompt="Eres un analista médico experto. Responde en texto plano controlado.",
        )
    )
    result_text = str(response.content or "").strip()
    if result_text and cache_repository is not None and cache_context is not None and fingerprint is not None:
        cache_repository.upsert(
            username=cache_context["username"],
            task=task.value,
            fingerprint=fingerprint,
            provider=cache_context["provider"],
            model=cache_context["model"],
            payload={"text": result_text},
            metadata={
                "prompt_version": _HISTORIA_SUMMARY_PROMPT_VERSION,
                "response_provider": response.provider,
                "response_model": response.model,
            },
        )
    return result_text


def _resumir_historia_larga(
    texto: str,
    *,
    summary_chunk: int,
    llm_router,
    cache_repository=None,
    username: str = "",
    return_metrics: bool = False,
    chunk_overlap: int | None = None,
    max_retries: int | None = None,
    retry_delays: list[float] | None = None,
    sleep_fn: Callable[[float], None] = sleep,
) -> str | tuple[str, HistoriaProcessingMetrics]:
    instrucciones = (
        "Resume la historia clinica manteniendo todos los datos clinicos críticos: "
        "diagnosticos, procedimientos, medicamentos (separando nombre, dosis, vía, frecuencia, duración, cantidad "
        "y estado administrado/formulado/ordenado cuando aparezcan), "
        "fechas, números, hallazgos y contexto relevante. No inventes datos. "
        "Entrega el resumen en espanol, en texto plano con secciones claras."
    )
    overlap = config.HISTORIA_SUMMARY_CHUNK_OVERLAP if chunk_overlap is None else chunk_overlap
    retries = config.HISTORIA_SUMMARY_MAX_RETRIES if max_retries is None else max_retries
    delays = _parse_retry_delays(
        config.HISTORIA_SUMMARY_RETRY_DELAYS_SECONDS if retry_delays is None else retry_delays
    )
    partes = _chunk_text(texto, summary_chunk, overlap=overlap)
    res_parcial = []
    failed_chunks: list[int] = []
    chunk_failures: list[_SummaryGenerationFailure] = []
    for idx, parte in enumerate(partes, start=1):
        prompt_resumen = f"{instrucciones}\n\nParte {idx} de {len(partes)}:\n{parte}"
        res_text, failure = _generate_historia_summary_with_retries(
            llm_router=llm_router,
            prompt=prompt_resumen,
            task=LLMTask.HISTORIA_CHUNK_SUMMARY,
            cache_repository=cache_repository,
            username=username,
            max_retries=retries,
            retry_delays=delays,
            sleep_fn=sleep_fn,
        )
        if res_text:
            res_parcial.append(res_text.strip())
        else:
            failed_chunks.append(idx)
            if failure:
                chunk_failures.append(failure)

    if failed_chunks:
        primary_failure = chunk_failures[0] if chunk_failures else None
        raise HistoriaSummaryCoverageError(
            "No fue posible resumir todos los bloques de la historia clínica.",
            failed_chunks=failed_chunks,
            stage=LLMTask.HISTORIA_CHUNK_SUMMARY.value,
            attempts=max((item.attempts for item in chunk_failures), default=max(0, retries) + 1),
            provider=primary_failure.provider if primary_failure else "",
            model=primary_failure.model if primary_failure else "",
            error_kind=primary_failure.error_kind if primary_failure else "",
            retryable=bool(chunk_failures) and all(item.retryable for item in chunk_failures),
            source=primary_failure.source if primary_failure else "internal",
        )

    if not res_parcial:
        raise HistoriaSummaryCoverageError("La historia clínica no produjo bloques resumibles.")

    consolidation_levels = 0
    current_summaries = list(res_parcial)
    reduce_budget = max(1000, int(config.HISTORIA_SUMMARY_REDUCE_MAX_CHARS or 0))
    final_instruction = (
        "Consolida los siguientes resúmenes parciales en un único resumen clínico "
        "detallado, coherente y completo. Preserva diagnósticos, procedimientos, "
        "medicamentos, fechas, cifras, hallazgos, antecedentes y evolución relevante. "
        "Corrige ortografía y puntuación. Entrega un texto narrativo clínico de uno o dos "
        "párrafos, sin viñetas ni encabezados, y no omitas datos relevantes.\n\n"
    )
    while len(current_summaries) > 1 or consolidation_levels == 0:
        groups: list[list[str]] = []
        current_group: list[str] = []
        current_chars = 0
        for summary in current_summaries:
            added_chars = len(summary) + (2 if current_group else 0)
            if current_group and current_chars + added_chars > reduce_budget:
                groups.append(current_group)
                current_group = []
                current_chars = 0
            current_group.append(summary)
            current_chars += added_chars
        if current_group:
            groups.append(current_group)
        if len(current_summaries) > 1 and len(groups) >= len(current_summaries):
            groups = [current_summaries[index : index + 2] for index in range(0, len(current_summaries), 2)]

        next_summaries: list[str] = []
        for group in groups:
            prompt_final = final_instruction + "\n\n".join(group)
            consolidated, failure = _generate_historia_summary_with_retries(
                llm_router=llm_router,
                prompt=prompt_final,
                task=LLMTask.HISTORIA_FINAL_SUMMARY,
                cache_repository=cache_repository,
                username=username,
                max_retries=retries,
                retry_delays=delays,
                sleep_fn=sleep_fn,
            )
            if not consolidated:
                raise HistoriaSummaryCoverageError(
                    "No fue posible consolidar los resúmenes de la historia clínica.",
                    stage=LLMTask.HISTORIA_FINAL_SUMMARY.value,
                    attempts=failure.attempts if failure else max(0, retries) + 1,
                    provider=failure.provider if failure else "",
                    model=failure.model if failure else "",
                    error_kind=failure.error_kind if failure else "",
                    retryable=bool(failure and failure.retryable),
                    source=failure.source if failure else "internal",
                )
            next_summaries.append(consolidated.strip())
        consolidation_levels += 1
        current_summaries = next_summaries

    metrics = HistoriaProcessingMetrics(
        source_characters=len(texto),
        covered_characters=len(texto),
        chunks_total=len(partes),
        chunks_succeeded=len(res_parcial),
        consolidation_levels=consolidation_levels,
        chunk_size=summary_chunk,
        chunk_overlap=overlap,
        chunk_model=config.GEMINI_MODEL_EXTRACT_LOW_RISK,
        final_model=config.GEMINI_MODEL_REASONING,
        failed_chunks=[],
        source_words=len(texto.split()),
        final_words=len(current_summaries[0].split()),
        policy_version="v1",
    )
    policy = calculate_historia_summary_policy(
        texto,
        threshold_characters=config.HISTORIA_SUMMARY_THRESHOLD,
        target_ratio=config.HISTORIA_SUMMARY_TARGET_RATIO,
        long_min_words=config.HISTORIA_SUMMARY_LONG_MIN_WORDS,
        max_words=config.HISTORIA_SUMMARY_MAX_WORDS,
    )
    metrics.target_words = policy.target_words
    metrics.acceptable_min_words = policy.acceptable_min_words
    metrics.acceptable_max_words = policy.acceptable_max_words
    metrics.paragraphs = max(1, len(current_summaries[0].split("\n\n")))
    result = current_summaries[0]
    return (result, metrics) if return_metrics else result


def _prepare_historia_prompt_payload(
    descripcion: str,
    *,
    summary_threshold: int,
    summary_chunk: int,
    max_chars: int,
    llm_router=None,
    cache_repository=None,
    username: str = "",
) -> PreparedHistoriaPrompt:
    snapshot = extract_deterministic_signals(
        descripcion,
        "historia_clinica",
        summary_threshold=summary_threshold,
        max_chars=max_chars,
    )
    compact_budget = min(max_chars or config.HISTORIA_MAX_CHARS, config.HISTORIA_COMPACT_MAX_CHARS)
    base_text = _budget_historia_text(
        snapshot.compacted_text or (descripcion or ""),
        max_chars=compact_budget,
    )
    resumen_previo = ""
    processing_metrics = HistoriaProcessingMetrics(
        source_characters=len(descripcion),
        covered_characters=len(descripcion),
        chunks_total=0,
        chunks_succeeded=0,
        chunk_size=summary_chunk,
        chunk_model=config.GEMINI_MODEL_EXTRACT_LOW_RISK,
        final_model=config.GEMINI_MODEL_REASONING,
        chunk_overlap=config.HISTORIA_SUMMARY_CHUNK_OVERLAP,
        source_words=len(descripcion.split()),
        policy_version="v1",
    )
    should_generate_summary = bool(summary_threshold and len(descripcion) > summary_threshold)
    if should_generate_summary:
        router = llm_router or _build_historia_llm_router()
        summary_result = _resumir_historia_larga(
            descripcion,
            summary_chunk=summary_chunk,
            llm_router=router,
            cache_repository=cache_repository,
            username=username,
            return_metrics=True,
        )
        if isinstance(summary_result, tuple):
            resumen_previo, processing_metrics = summary_result
        else:
            resumen_previo = str(summary_result or "")

    policy = calculate_historia_summary_policy(
        descripcion,
        threshold_characters=summary_threshold,
        target_ratio=config.HISTORIA_SUMMARY_TARGET_RATIO,
        long_min_words=config.HISTORIA_SUMMARY_LONG_MIN_WORDS,
        max_words=config.HISTORIA_SUMMARY_MAX_WORDS,
    )
    processing_metrics.target_words = policy.target_words
    processing_metrics.acceptable_min_words = policy.acceptable_min_words
    processing_metrics.acceptable_max_words = policy.acceptable_max_words

    prompt_text = _build_historia_prompt_pack(
        descripcion,
        snapshot=snapshot,
        compacted_text=base_text,
        max_chars=max_chars,
        previous_summary=resumen_previo,
    )
    processing_metrics.prompt_characters = len(prompt_text)
    return PreparedHistoriaPrompt(
        prompt_text=prompt_text,
        detailed_summary=resumen_previo,
        processing_metrics=processing_metrics,
    )


def _prepare_historia_prompt_text(
    descripcion: str,
    *,
    summary_threshold: int,
    summary_chunk: int,
    max_chars: int,
    llm_router=None,
    cache_repository=None,
    username: str = "",
) -> str:
    return _prepare_historia_prompt_payload(
        descripcion,
        summary_threshold=summary_threshold,
        summary_chunk=summary_chunk,
        max_chars=max_chars,
        llm_router=llm_router,
        cache_repository=cache_repository,
        username=username,
    ).prompt_text


class _HistoriaAntecedentsRetryPayload(BaseModel):
    model_config = ConfigDict(populate_by_name=True, extra="ignore")

    antecedentes_estructurados: list[AntecedenteClinico] = Field(default_factory=list, alias="ae")


def _retry_historia_antecedentes(
    *,
    raw_text: str,
    reason_codes: tuple[str, ...],
    llm_router: Any,
) -> list[AntecedenteClinico] | None:
    if llm_router is None or not hasattr(llm_router, "generate_structured"):
        return None
    context = build_historia_antecedent_context(raw_text)
    if not context:
        return []
    prompt = (
        "Reextrae únicamente los antecedentes positivos del paciente. "
        "No incluyas negaciones, datos administrativos, síntomas, diagnósticos, procedimientos, "
        "medicamentos del episodio actual ni evolución. Cada descripción debe ser breve. "
        "En antecedentes farmacológicos separa el nombre del medicamento en descripción y agrega contexto "
        "solo si es una frase breve explícita y localizable en la evidencia; no infieras indicación ni temporalidad. "
        "Incluye categoría, página, evidencia textual exacta, origen explicito o inferido y confianza. "
        "Solo admite inferencias con confianza alta y evidencia localizable. "
        f"Corrige estas razones: {', '.join(reason_codes)}.\n\n"
        f"Contexto documental:\n{context}"
    )
    try:
        response = llm_router.generate_structured(
            LLMStructuredRequest(
                task=LLMTask.CLINICAL_DOCUMENT_EXTRACT,
                prompt=prompt,
                output_model=_HistoriaAntecedentsRetryPayload,
                output_kind=LLMOutputKind.STRUCTURED_OBJECT,
                system_prompt=(
                    "Eres un extractor de antecedentes clínicos. Responde únicamente un objeto JSON válido "
                    "que cumpla el esquema, sin Markdown ni texto adicional."
                ),
                metadata={
                    "document_type": "historia_clinica",
                    "risk_level": "high",
                    "retry_stage": "antecedents_quality",
                    "retry_reason_codes": list(reason_codes),
                    "prompt_version": "v10",
                },
            )
        )
        payload = _HistoriaAntecedentsRetryPayload.model_validate(response.content)
    except Exception:
        return None
    return list(payload.antecedentes_estructurados)


def _apply_historia_antecedent_resolution(
    model: HistoriaClinicaStructured,
    resolution,
) -> None:
    model.antecedentes_estructurados = list(resolution.items)
    model.antecedentes = antecedentes_to_flat_list(resolution.items)
    accepted_procedure_keys = set(resolution.accepted_procedure_keys)
    model.procedimientos_antecedentes = [
        item
        for item in model.procedimientos_antecedentes
        if antecedent_description_key(item.descripcion) in accepted_procedure_keys
    ]


class HistoriaClinicaRequest(BaseModel):
    descripcion: str

    @staticmethod
    def extraer_historia_estructurada(
        descripcion: str,
        *,
        llm_router=None,
        cache_repository=None,
        username: str = "",
    ):
        descripcion = descripcion or ""
        summary_threshold = config.HISTORIA_SUMMARY_THRESHOLD
        summary_chunk = max(500, config.HISTORIA_SUMMARY_CHUNK)
        max_chars = config.HISTORIA_MAX_CHARS
        router = llm_router or _build_historia_llm_router()
        prepared_prompt = _prepare_historia_prompt_payload(
            descripcion,
            summary_threshold=summary_threshold,
            summary_chunk=summary_chunk,
            max_chars=max_chars,
            llm_router=router,
            cache_repository=cache_repository,
            username=username,
        )

        try:
            result = ClinicalStructuredExtractionService(
                llm_router=router,
                cache_repository=cache_repository,
            ).extract(
                raw_text=descripcion,
                document_type="historia_clinica",
                summary_text=prepared_prompt.prompt_text,
            )
            route_metadata = dict(getattr(result, "route_metadata", {}) or {})
            if prepared_prompt.processing_metrics is not None:
                route_metadata["historia_processing"] = prepared_prompt.processing_metrics.to_document()
            result.route_metadata = route_metadata
            analysis_model = cast(HistoriaClinicaStructured, result.analysis_model)
            analysis_model.resumen_clinico = choose_historia_resumen(
                analysis_model.resumen_clinico,
                detailed_candidate=prepared_prompt.detailed_summary,
                raw_text=descripcion,
                analisis_html=result.rendered_html,
                policy=calculate_historia_summary_policy(
                    descripcion,
                    threshold_characters=config.HISTORIA_SUMMARY_THRESHOLD,
                    target_ratio=config.HISTORIA_SUMMARY_TARGET_RATIO,
                    long_min_words=config.HISTORIA_SUMMARY_LONG_MIN_WORDS,
                    max_words=config.HISTORIA_SUMMARY_MAX_WORDS,
                ),
                patient_name=analysis_model.patient_name,
                patient_id=analysis_model.identificacion_paciente or "",
                case_number=analysis_model.numero_caso or "",
            )
            procedure_candidates = list(analysis_model.procedimientos_antecedentes)
            antecedent_resolution = resolve_historia_antecedentes(
                descripcion,
                structured_candidates=analysis_model.antecedentes_estructurados,
                procedure_candidates=procedure_candidates,
                legacy_flat_candidates=analysis_model.antecedentes,
            )
            antecedent_retries = 0
            unresolved_antecedent_reasons = tuple(antecedent_resolution.reason_codes)
            if (
                antecedent_resolution.should_retry
                and config.HISTORIA_ANTECEDENTS_QUALITY_RETRIES > 0
            ):
                antecedent_retries = 1
                retry_antecedents = _retry_historia_antecedentes(
                    raw_text=descripcion,
                    reason_codes=antecedent_resolution.reason_codes,
                    llm_router=router,
                )
                contamination_only = set(antecedent_resolution.reason_codes) <= {
                    "antecedents_contaminated_narrative",
                    "antecedents_inconsistent_flat_projection",
                    "antecedents_incomplete_value",
                    "antecedents_cross_column_contamination",
                }
                if retry_antecedents is not None and (retry_antecedents or contamination_only):
                    antecedent_resolution = resolve_historia_antecedentes(
                        descripcion,
                        structured_candidates=[
                            *antecedent_resolution.items,
                            *retry_antecedents,
                        ],
                        procedure_candidates=procedure_candidates,
                    )
                    unresolved_antecedent_reasons = tuple(antecedent_resolution.reason_codes)
                else:
                    unresolved_antecedent_reasons = tuple(
                        dict.fromkeys(
                            [
                                *antecedent_resolution.reason_codes,
                                "antecedents_retry_exhausted",
                            ]
                        )
                    )
            _apply_historia_antecedent_resolution(analysis_model, antecedent_resolution)

            antecedent_metrics = antecedent_resolution.to_metrics(retries=antecedent_retries)
            antecedent_metrics["reason_codes"] = list(unresolved_antecedent_reasons)
            processing_metrics = prepared_prompt.processing_metrics
            if processing_metrics is not None:
                processing_metrics.antecedentes = antecedent_metrics
                route_metadata["historia_processing"] = processing_metrics.to_document()
            else:
                historia_processing = dict(route_metadata.get("historia_processing") or {})
                historia_processing["antecedentes"] = antecedent_metrics
                route_metadata["historia_processing"] = historia_processing
            result.analysis_structured = dump_clinical_model(analysis_model)
            result.rendered_html = render_clinical_document_html(analysis_model)
            quality = assess_clinical_extraction(
                document_type="historia_clinica",
                raw_text=descripcion,
                analysis_model=analysis_model,
            )
            if quality.should_retry and any(
                reason.startswith("summary_") for reason in quality.reason_codes
            ) and config.HISTORIA_SUMMARY_QUALITY_RETRIES > 0:
                retry_prompt = (
                    "Reescribe únicamente el resumen clínico completo de la historia. "
                    "Conserva el orden cronológico, usa párrafos y frases completas, cubre los temas clínicos "
                    "presentes y no incluyas nombre, documento, número de caso ni médicos. "
                    f"Corrige estas razones: {', '.join(quality.reason_codes)}.\n\n"
                    f"Resumen actual:\n{analysis_model.resumen_clinico}"
                )
                retry_summary, _failure = _generate_historia_summary_with_retries(
                    llm_router=router,
                    prompt=retry_prompt,
                    task=LLMTask.HISTORIA_FINAL_SUMMARY,
                    cache_repository=cache_repository,
                    username=username,
                    max_retries=0,
                    retry_delays=[],
                    sleep_fn=lambda _delay: None,
                )
                if retry_summary:
                    analysis_model.resumen_clinico = choose_historia_resumen(
                        retry_summary,
                        detailed_candidate=retry_summary,
                        raw_text=descripcion,
                        policy=calculate_historia_summary_policy(
                            descripcion,
                            threshold_characters=config.HISTORIA_SUMMARY_THRESHOLD,
                            target_ratio=config.HISTORIA_SUMMARY_TARGET_RATIO,
                            long_min_words=config.HISTORIA_SUMMARY_LONG_MIN_WORDS,
                            max_words=config.HISTORIA_SUMMARY_MAX_WORDS,
                        ),
                        patient_name=analysis_model.patient_name,
                        patient_id=analysis_model.identificacion_paciente or "",
                        case_number=analysis_model.numero_caso or "",
                    )
                    processing_metrics = prepared_prompt.processing_metrics
                    if processing_metrics is not None:
                        processing_metrics.quality_retries += 1
                        processing_metrics.final_words = len(analysis_model.resumen_clinico.split())
                        route_metadata["historia_processing"] = processing_metrics.to_document()
                    result.analysis_structured = dump_clinical_model(analysis_model)
                    result.rendered_html = render_clinical_document_html(analysis_model)
                    quality = assess_clinical_extraction(
                        document_type="historia_clinica",
                        raw_text=descripcion,
                        analysis_model=analysis_model,
                    )
            quality_payload = quality.to_payload()
            if unresolved_antecedent_reasons:
                quality_payload.update(
                    {
                        "status": "degraded",
                        "low_confidence": True,
                        "should_retry": True,
                        "reason_codes": list(
                            dict.fromkeys(
                                [
                                    *list(quality_payload.get("reason_codes") or []),
                                    *unresolved_antecedent_reasons,
                                ]
                            )
                        ),
                    }
                )
            result.quality_metadata = quality_payload
            return result
        except HistoriaSummaryCoverageError:
            raise
        except Exception as e:
            proveedor = "llm_router"
            if hasattr(router, "_resolve_route"):
                try:
                    proveedor = router._resolve_route(LLMTask.CLINICAL_DOCUMENT_EXTRACT).provider
                except Exception:
                    proveedor = "llm_router"
            raise HTTPException(status_code=500, detail=f"Error usando {proveedor}: {e}")

    @staticmethod
    def analizar_historiaclinica(descripcion: str) -> str:
        return HistoriaClinicaRequest.extraer_historia_estructurada(descripcion).rendered_html

    @staticmethod
    def guardar_historia(descripcion: str, analisis: str) -> str:
        # Desactivado: esta colección no se usa en flujos posteriores.
        return ""
