from __future__ import annotations

import logging
import re
from typing import Any

from groq import Groq

from app.config import config
from app.core.logging import get_log_context
from app.llm import LLMOutputKind, LLMStructuredRequest, LLMTask
from app.llm.schemas import (
    SoatCodeCandidateStructured,
    SoatEvidenceItemStructured,
    SoatGeneratedCodeStructured,
    SoatGlosaStructured,
    SoatValidationStructured,
)
from app.llm.task_text import generate_task_text
from app.services.clinical_processing import extraer_procedimientos_para_soat
from app.services.context_budgeting import (
    deduplicate_records,
    limit_items,
    normalize_budget_key,
    truncate_text,
)
from app.services.llm_task_cache import build_llm_task_fingerprint
from app.services.quirurgico_html_legacy import extract_quirurgico_text_section


logger = logging.getLogger(__name__)
_SOAT_CODE_GENERATION_PROMPT_VERSION = "v1"


def _legacy_llm_enabled() -> bool:
    return bool(config.LEGACY_LLM_PROVIDERS_ENABLED)


def _normalize_whitespace(value: Any) -> str:
    return re.sub(r"\s+", " ", str(value or "")).strip()


def _bullet_block(items: list[str]) -> str:
    return "\n".join(f"- {item}" for item in items if _normalize_whitespace(item))


def _coerce_code_candidate(
    item: dict[str, Any],
    *,
    code_key: str,
    desc_key: str,
) -> dict[str, Any] | None:
    codigo = _normalize_whitespace(item.get(code_key) or item.get("codigo"))
    descripcion = _normalize_whitespace(item.get(desc_key) or item.get("descripcion"))
    if not codigo and not descripcion:
        return None
    payload: dict[str, Any] = {"codigo": codigo, "descripcion": descripcion}
    justificacion = _normalize_whitespace(item.get("justificacion"))
    if justificacion:
        payload["justificacion"] = justificacion
    score = item.get("score")
    if isinstance(score, (int, float)):
        payload["score"] = float(score)
    return payload


def _coerce_cups_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    seen: set[tuple[str, str]] = set()
    candidates: list[dict[str, Any]] = []
    for item in items or []:
        if not isinstance(item, dict):
            continue
        candidate = _coerce_code_candidate(item, code_key="codigo_cups", desc_key="procedimiento")
        if not candidate:
            continue
        key = (str(candidate.get("codigo") or "").lower(), str(candidate.get("descripcion") or "").lower())
        if key in seen:
            continue
        seen.add(key)
        candidates.append(candidate)
    return candidates


def _coerce_cups_candidate_models(items: list[dict[str, Any]]) -> list[SoatCodeCandidateStructured]:
    return [SoatCodeCandidateStructured.model_validate(item) for item in _coerce_cups_candidates(items)]


def _coerce_cie10_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    seen: set[tuple[str, str]] = set()
    candidates: list[dict[str, Any]] = []
    for item in items or []:
        if not isinstance(item, dict):
            continue
        candidate = _coerce_code_candidate(item, code_key="codigo", desc_key="descripcion")
        if not candidate:
            continue
        key = (str(candidate.get("codigo") or "").lower(), str(candidate.get("descripcion") or "").lower())
        if key in seen:
            continue
        seen.add(key)
        candidates.append(candidate)
    return candidates


def _coerce_cie10_candidate_models(items: list[dict[str, Any]]) -> list[SoatCodeCandidateStructured]:
    return [SoatCodeCandidateStructured.model_validate(item) for item in _coerce_cie10_candidates(items)]


def _build_legacy_cups_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    legacy: list[dict[str, Any]] = []
    for item in _coerce_cups_candidates(items):
        legacy.append(
            {
                "codigo_cups": item.get("codigo", ""),
                "procedimiento": item.get("descripcion", ""),
                "justificacion": item.get("justificacion", ""),
                "score": item.get("score", 0.0),
            }
        )
    return legacy


def _build_legacy_cie10_candidates(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    legacy: list[dict[str, Any]] = []
    for item in _coerce_cie10_candidates(items):
        legacy.append(
            {
                "codigo": item.get("codigo", ""),
                "descripcion": item.get("descripcion", ""),
                "justificacion": item.get("justificacion", ""),
                "score": item.get("score", 0.0),
            }
        )
    return legacy


def _render_glosa_html(model: SoatGlosaStructured) -> str:
    parts = [
        "<p><b>Análisis de Riesgo de Glosa</b></p>",
        f"<p>{model.resumen}</p>",
    ]
    if model.riesgos_altos:
        parts.append("<p><b>Riesgos Altos</b></p>")
        parts.append("<ul>" + "".join(f"<li>{item}</li>" for item in model.riesgos_altos) + "</ul>")
    if model.riesgos_medios:
        parts.append("<p><b>Riesgos Medios</b></p>")
        parts.append("<ul>" + "".join(f"<li>{item}</li>" for item in model.riesgos_medios) + "</ul>")
    if model.mitigaciones:
        parts.append("<p><b>Mitigaciones Recomendadas</b></p>")
        parts.append("<ul>" + "".join(f"<li>{item}</li>" for item in model.mitigaciones) + "</ul>")
    if model.nota_sugerida:
        parts.append("<p><b>Nota Aclaratoria Recomendada</b></p>")
        parts.append(f"<p>{model.nota_sugerida}</p>")
    return "\n".join(parts)


def _render_validation_summary(model: SoatValidationStructured) -> str:
    blocks = [
        f"ESTADO: {model.estado_validacion}",
        model.resumen_validacion,
        f"VERIFICACIÓN EN MANUAL SOAT:\n{model.verificacion_manual}",
    ]
    return "\n\n".join(block for block in blocks if _normalize_whitespace(block))


def _render_generated_code_summary(model: SoatGeneratedCodeStructured) -> str:
    lines = []
    if model.cie10_principal:
        lines.append(
            f"CIE-10 principal: {model.cie10_principal.codigo} - {model.cie10_principal.descripcion}"
        )
    if model.cups_principal:
        lines.append(f"CUPS principal: {model.cups_principal.codigo} - {model.cups_principal.descripcion}")
    if model.justificacion:
        lines.append(f"Justificación: {model.justificacion}")
    if model.analisis_clinico:
        lines.append(f"Análisis clínico: {model.analisis_clinico}")
    return "\n".join(lines)


def _coerce_validation_evidence_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    normalized: list[dict[str, Any]] = []
    for item in items or []:
        if not isinstance(item, dict):
            continue
        page = item.get("page")
        extracto = _normalize_whitespace(item.get("extracto"))
        if not extracto:
            continue
        extracto, _ = truncate_text(extracto, max_chars=config.SOAT_RAG_MAX_EXTRACT_CHARS)
        payload: dict[str, Any] = {"page": page, "extracto": extracto}
        score = item.get("score")
        if isinstance(score, (int, float)):
            payload["score"] = float(score)
        normalized.append(payload)
    unique, _ = deduplicate_records(
        normalized,
        key_fn=lambda item: f"{item.get('page')}|{normalize_budget_key(item.get('extracto'))}",
    )
    unique, _ = limit_items(unique, max_items=config.SOAT_RAG_MAX_EVIDENCE)
    return unique


def _coerce_validation_evidence_models(items: list[dict[str, Any]]) -> list[SoatEvidenceItemStructured]:
    return [SoatEvidenceItemStructured.model_validate(item) for item in _coerce_validation_evidence_items(items)]


def _build_fallback_glosa_structured(*, historia_html: str, qx_html: str) -> SoatGlosaStructured:
    has_context = bool(_normalize_whitespace(historia_html) or _normalize_whitespace(qx_html))
    resumen = (
        "No se pudo generar análisis estructurado de glosa; se recomienda revisión manual del soporte quirúrgico."
    )
    if has_context:
        resumen = (
            "Se recuperó contexto clínico y quirúrgico, pero la evaluación estructurada de glosa quedó pendiente de "
            "validación manual."
        )
    return SoatGlosaStructured(
        rs=resumen,
        rm=["Verificar consistencia entre nota operatoria, hallazgos y soporte SOAT."],
        mt=["Adjuntar evidencia documental y revisar manual SOAT antes de cierre."],
        nt="Pendiente de validación manual.",
    )


def _build_fallback_validation_structured(
    *,
    evidencia_soat: list[dict[str, Any]],
    propuestas_cups: list[dict[str, Any]],
    propuestas_cie10: list[dict[str, Any]],
    contexto_cups: str,
    contexto_cie10: str,
) -> SoatValidationStructured:
    return SoatValidationStructured(
        ev="REQUIERE REVISIÓN",
        vm=(
            "No se pudo completar la validación estructurada; utilice la evidencia SOAT recuperada para revisión manual."
        ),
        cd=(
            "La validación automática estructurada no estuvo disponible; se entregan candidatos preliminares."
        ),
        rg="Riesgo indeterminado hasta validar manualmente el soporte clínico y normativo.",
        rc="Confirmar manualmente códigos y soporte antes de aprobar la factura.",
        pc="Candidatos preliminares basados en evidencia SOAT y retrievers clínicos.",
        na="Pendiente de validación manual.",
        rv=(
            "No se pudo generar validación estructurada SOAT; se devuelven candidatos preliminares compatibles."
        ),
        dx=_coerce_cie10_candidate_models(propuestas_cie10),
        cp=_coerce_cups_candidate_models(propuestas_cups),
        es=_coerce_validation_evidence_models(evidencia_soat),
        cc=_normalize_whitespace(contexto_cups) or None,
        cx=_normalize_whitespace(contexto_cie10) or None,
    )


def _build_fallback_generated_code_structured(
    *,
    codigo_soat: str,
    descripcion: str,
    error: str,
) -> SoatGeneratedCodeStructured:
    return SoatGeneratedCodeStructured(
        cs=codigo_soat,
        d=descripcion,
        ac="No se pudo generar codificación estructurada SOAT.",
        j="Pendiente de validación manual.",
        f="fallback",
        er=error,
    )


def _collect_soat_reasoning_context(
    *,
    codigo_soat: str,
    descripcion_soat: str,
    evidencia_query: str,
    cie10_query_text: str,
    cups_query_text: str,
    soat_retriever,
    retriever,
    cups_retriever,
) -> dict[str, Any]:
    evidencia_soat: list[dict[str, Any]] = []
    propuestas_cie10: list[dict[str, Any]] = []
    propuestas_cups: list[dict[str, Any]] = []
    contexto_cie10 = ""
    contexto_cups = ""

    try:
        evidencia_soat = _buscar_evidencia_soat(
            evidencia_query,
            codigo_soat,
            descripcion_soat,
            soat_retriever,
        )
    except Exception:
        logger.exception("Error en búsqueda SOAT")

    try:
        propuestas_cie10, contexto_cie10 = _buscar_propuestas_cie10(cie10_query_text, retriever)
    except Exception:
        logger.exception("Error en búsqueda CIE-10")

    try:
        propuestas_cups, contexto_cups = _buscar_propuestas_cups(cups_query_text, cups_retriever)
    except Exception:
        logger.exception("Error en búsqueda CUPS")

    return {
        "evidencia_soat": evidencia_soat,
        "propuestas_cie10": propuestas_cie10,
        "propuestas_cups": propuestas_cups,
        "contexto_cie10": contexto_cie10,
        "contexto_cups": contexto_cups,
    }


def _resolve_soat_validation_model(
    *,
    llm_router,
    metadata: dict[str, Any],
    codigo_soat: str,
    descripcion_soat: str,
    diagnosticos_prequx: str,
    procedimientos: str,
    hallazgos: str,
    descripcion_proc: str,
    justificacion: str,
    diagnosticos_postqx: str,
    evidence_bundle: dict[str, Any],
) -> SoatValidationStructured:
    try:
        structured_validation = _generate_structured_soat_response(
            llm_router=llm_router,
            task=LLMTask.SOAT_REASONING,
            prompt=_build_validation_structured_prompt(
                codigo_soat=codigo_soat,
                descripcion_soat=descripcion_soat,
                diagnosticos_prequx=diagnosticos_prequx,
                procedimientos=procedimientos,
                hallazgos=hallazgos,
                descripcion_proc=descripcion_proc,
                justificacion=justificacion,
                diagnosticos_postqx=diagnosticos_postqx,
                evidencia_soat=evidence_bundle["evidencia_soat"],
                propuestas_cups=evidence_bundle["propuestas_cups"],
                propuestas_cie10=evidence_bundle["propuestas_cie10"],
                contexto_cups=evidence_bundle["contexto_cups"],
                contexto_cie10=evidence_bundle["contexto_cie10"],
            ),
            output_model=SoatValidationStructured,
            output_kind=LLMOutputKind.STRUCTURED_OBJECT,
            metadata=metadata,
        )
    except Exception:
        logger.exception("Error generando validación estructurada SOAT")
        structured_validation = None

    if structured_validation is not None:
        return structured_validation

    return _build_fallback_validation_structured(
        evidencia_soat=evidence_bundle["evidencia_soat"],
        propuestas_cups=evidence_bundle["propuestas_cups"],
        propuestas_cie10=evidence_bundle["propuestas_cie10"],
        contexto_cups=evidence_bundle["contexto_cups"],
        contexto_cie10=evidence_bundle["contexto_cie10"],
    )


def _render_agent_soat_result(
    *,
    item: dict[str, str],
    validation_model: SoatValidationStructured,
) -> dict[str, Any]:
    propuestas_cups = _build_legacy_cups_candidates(
        [candidate.model_dump(by_alias=False) for candidate in validation_model.cups_propuestos]
    )
    propuestas_cie10 = _build_legacy_cie10_candidates(
        [candidate.model_dump(by_alias=False) for candidate in validation_model.cie10_propuestos]
    )
    cups_seleccionados, cups_alternativas, cie10_seleccionados, cie10_alternativas = _seleccionar_codigos_rag(
        propuestas_cups,
        propuestas_cie10,
    )
    analisis_completo = _render_validation_summary(validation_model)
    return {
        "input": {
            "codigo_soat": _normalize_whitespace(item.get("codigo_soat")),
            "descripcion": _normalize_whitespace(item.get("descripcion")),
        },
        "cups_seleccionados": cups_seleccionados,
        "cups_alternativas": cups_alternativas,
        "cie10_seleccionados": cie10_seleccionados,
        "cie10_alternativas": cie10_alternativas,
        "analisis_completo": analisis_completo,
        "recomendaciones_glosa": validation_model.riesgo_glosa,
        "pertinencia_codigos": validation_model.pertinencia_codigos,
        "nota_aclaratoria": validation_model.nota_aclaratoria,
        "evidencia": {
            "soat": [item.model_dump(by_alias=False) for item in validation_model.evidencia_soat][:8],
            "cups_contexto": validation_model.contexto_cups or "",
            "cie10_contexto": validation_model.contexto_cie10 or "",
        },
        "justificacion": analisis_completo,
        "cups_seleccionado": cups_seleccionados[0] if cups_seleccionados else None,
    }


def _render_invoice_validation_result(validation_model: SoatValidationStructured) -> dict[str, Any]:
    return {
        "validacion_resultado": _render_validation_summary(validation_model),
        "coherencia_documental": validation_model.coherencia_documental,
        "riesgo_glosa": validation_model.riesgo_glosa,
        "recomendaciones": validation_model.recomendaciones,
        "cie10_propuestos": _build_legacy_cie10_candidates(
            [item.model_dump(by_alias=False) for item in validation_model.cie10_propuestos]
        )[:8],
        "cups_propuestos": _build_legacy_cups_candidates(
            [item.model_dump(by_alias=False) for item in validation_model.cups_propuestos]
        )[:6],
        "evidencia_soat": [item.model_dump(by_alias=False) for item in validation_model.evidencia_soat][:8],
        "contexto_rag": {
            "cups_completo": validation_model.contexto_cups or "",
            "cie10_completo": validation_model.contexto_cie10 or "",
            "total_evidencia_soat": len(validation_model.evidencia_soat),
        },
    }


def _render_generated_code_payload(
    *,
    codigo_soat: str,
    descripcion: str,
    structured_code: SoatGeneratedCodeStructured,
) -> dict[str, Any]:
    return {
        "codigo_soat": codigo_soat,
        "descripcion": descripcion,
        "cie10_principal": (
            structured_code.cie10_principal.model_dump(by_alias=False) if structured_code.cie10_principal else None
        ),
        "cie10_secundarios": [item.model_dump(by_alias=False) for item in structured_code.cie10_secundarios],
        "cups_principal": (
            structured_code.cups_principal.model_dump(by_alias=False) if structured_code.cups_principal else None
        ),
        "cups_alternativos": [item.model_dump(by_alias=False) for item in structured_code.cups_alternativos],
        "analisis_clinico": structured_code.analisis_clinico,
        "justificacion": structured_code.justificacion or structured_code.analisis_clinico,
        "fuente": structured_code.fuente,
        "respuesta_completa": _render_generated_code_summary(structured_code),
        "error": structured_code.error,
    }


def _generate_structured_soat_response(
    *,
    llm_router,
    task: LLMTask,
    prompt: str,
    output_model,
    output_kind: LLMOutputKind,
    metadata: dict[str, Any],
):
    if llm_router is None:
        return None
    response = llm_router.generate_structured(
        LLMStructuredRequest(
            task=task,
            prompt=prompt,
            output_model=output_model,
            output_kind=output_kind,
            system_prompt=(
                "Eres un auditor médico experto en SOAT y codificación clínica en Colombia. "
                "Responde únicamente un objeto JSON válido que cumpla el esquema solicitado. "
                "No inventes códigos fuera del contexto proporcionado."
            ),
            metadata=metadata,
        )
    )
    return output_model.model_validate(response.content)


def _resolve_soat_cache_context(*, llm_router, task: LLMTask, username: str = "") -> dict[str, str] | None:
    normalized_username = str(username or get_log_context().get("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 _build_glosa_structured_prompt(historia_html: str, qx_html: str) -> str:
    return f"""
Tarea: analiza riesgo de glosa y nota aclaratoria para un caso quirúrgico.
Devuelve un JSON con resumen, riesgos altos, riesgos medios, mitigaciones y nota sugerida.

Historia / diagnósticos:
{historia_html or ""}

Descripción quirúrgica:
{qx_html or ""}
"""


def _build_code_generation_structured_prompt(codigo_soat: str, descripcion: str) -> str:
    return f"""
Tarea: proponer codificación CIE-10 y CUPS desde una descripción SOAT.
Devuelve un JSON con un código principal y secundarios relevantes si aplican.

Código SOAT: {codigo_soat or "No especificado"}
Descripción: {descripcion}
"""


def _build_validation_structured_prompt(
    *,
    codigo_soat: str,
    descripcion_soat: str,
    diagnosticos_prequx: str,
    procedimientos: str,
    hallazgos: str,
    descripcion_proc: str,
    justificacion: str,
    diagnosticos_postqx: str,
    evidencia_soat: list[dict[str, Any]],
    propuestas_cups: list[dict[str, Any]],
    propuestas_cie10: list[dict[str, Any]],
    contexto_cups: str,
    contexto_cie10: str,
) -> str:
    evidencia_texto = "\n\n".join(
        f"Página {item.get('page', 'N/A')}: {item.get('extracto', '')}" for item in evidencia_soat[:8]
    )
    cups_texto = "\n".join(
        f"- {item.get('codigo_cups', '')}: {item.get('procedimiento', '')}" for item in propuestas_cups[:8]
    )
    cie_texto = "\n".join(
        f"- {item.get('codigo', '')}: {item.get('descripcion', '')}" for item in propuestas_cie10[:8]
    )
    return f"""
Tarea: validar un código SOAT facturado contra documentación quirúrgica y evidencia RAG.
Devuelve un JSON con estado de validación, verificación manual, coherencia documental, riesgo de glosa,
recomendaciones, pertinencia, nota aclaratoria, resumen, códigos CIE-10/CUPS propuestos y evidencia relevante.

Código SOAT: {codigo_soat}
Descripción SOAT: {descripcion_soat}

Diagnósticos prequirúrgicos:
{diagnosticos_prequx}

Procedimientos realizados:
{procedimientos}

Hallazgos:
{hallazgos}

Descripción del procedimiento:
{descripcion_proc}

Justificación:
{justificacion}

Diagnósticos postquirúrgicos:
{diagnosticos_postqx}

Evidencia SOAT:
{evidencia_texto}

CUPS candidatos:
{cups_texto}

CIE-10 candidatos:
{cie_texto}

Contexto CUPS:
{contexto_cups}

Contexto CIE-10:
{contexto_cie10}
"""


def analizar_glosa_y_generar_nota(
    descripcion_qx: str,
    hallazgos: str,
    diagnosticos_html: str,
    client_groq: Groq | None,
    *,
    llm_router=None,
) -> str:
    try:
        prompt = f"""
Eres un especialista en auditoría médica y codificación clínica del sistema de salud colombiano. Analiza la coherencia documental para prevenir glosas y propone una Nota Aclaratoria si es necesario.

Diagnósticos/Historia:
{diagnosticos_html}

Descripción Qx / Procedimientos:
{descripcion_qx}

Hallazgos:
{hallazgos}

Formato de respuesta (HTML simple):
<p><b>Análisis de Riesgo de Glosa</b></p>
<ul><li>[puntos clave de riesgo o coherencia]</li></ul>
<p><b>Nota Aclaratoria Recomendada</b></p>
<p>[texto sugerido, si aplica]</p>
"""
        if llm_router is not None:
            response = generate_task_text(
                llm_router,
                task=LLMTask.GLOSA_NOTE_GENERATION,
                prompt=prompt,
                system_prompt="Eres auditor clínico. Sé preciso y conciso.",
                metadata={"operation": "legacy_glosa_note_generation"},
                generation_config={"temperature": 0.0},
            )
            return response.content

        if not _legacy_llm_enabled() or client_groq is None:
            return ""
        resp = client_groq.chat.completions.create(
            model=config.GROQ_MODEL_CLINICAL,
            temperature=0.0,
            messages=[
                {"role": "system", "content": "Eres auditor clínico. Sé preciso y conciso."},
                {"role": "user", "content": prompt},
            ],
        )
        return resp.choices[0].message.content or ""
    except Exception:
        return ""


def _construir_soat_y_glosa(
    historia_html: str,
    qx_html: str,
    soat_retriever,
    client_groq: Groq,
    *,
    llm_router=None,
):
    soat_resultados = []
    glosa_html = ""
    if qx_html:
        try:
            procedimientos = extraer_procedimientos_para_soat(qx_html)
            if soat_retriever and procedimientos:
                soat_resultados = soat_retriever.asignar_codigos(procedimientos)
        except Exception:
            soat_resultados = []
    try:
        structured_glosa = _generate_structured_soat_response(
            llm_router=llm_router,
            task=LLMTask.SOAT_REASONING,
            prompt=_build_glosa_structured_prompt(historia_html or "", qx_html or ""),
            output_model=SoatGlosaStructured,
            output_kind=LLMOutputKind.STRUCTURED_OBJECT,
            metadata={"operation": "soat_glosa"},
        )
        if structured_glosa is None:
            structured_glosa = _build_fallback_glosa_structured(
                historia_html=historia_html or "",
                qx_html=qx_html or "",
            )
        glosa_html = _render_glosa_html(structured_glosa)
    except Exception:
        glosa_html = _render_glosa_html(
            _build_fallback_glosa_structured(
                historia_html=historia_html or "",
                qx_html=qx_html or "",
            )
        )
    return soat_resultados, glosa_html


def _parse_cie10_lines(text: str) -> list[dict[str, str]]:
    resultados = []
    if not text:
        return resultados
    for line in text.splitlines():
        line = line.strip()
        if not line:
            continue
        m = re.match(r"^Código:\s*(?P<code>[A-Z0-9\.\-]+)\s*-\s*Descripción:\s*(?P<desc>.+)$", line)
        if m:
            resultados.append({"codigo": m.group("code"), "descripcion": m.group("desc")})
        else:
            # fallback: try simple split
            if "-" in line:
                partes = line.split("-", 1)
                resultados.append({"codigo": partes[0].strip(), "descripcion": partes[1].strip()})
    return resultados


def _buscar_evidencia_soat(
    consulta: str, codigo_soat: str, descripcion: str, soat_retriever
) -> list[dict[str, Any]]:
    if not soat_retriever or not consulta:
        return []

    palabras_clave = " ".join(
        [
            palabra
            for palabra in descripcion.split()
            if len(palabra) > 3
            and palabra.upper() not in ["CON", "SIN", "POR", "DEL", "LOS", "LAS", "UNA", "DOS", "TRES"]
        ]
    )
    docs_principal = soat_retriever.buscar(consulta, k=config.SOAT_RAG_PRIMARY_K)
    docs_codigo = soat_retriever.buscar(codigo_soat, k=config.SOAT_RAG_CODE_K) if codigo_soat else []
    docs_descripcion = soat_retriever.buscar(palabras_clave, k=config.SOAT_RAG_DESC_K) if palabras_clave else []

    evidence = [
        {
            "page": d.metadata.get("page"),
            "extracto": (d.page_content or ""),
            "score": getattr(d, "score", 0.0),
        }
        for d in docs_principal + docs_codigo + docs_descripcion
    ]
    return _coerce_validation_evidence_items(evidence)


def _buscar_propuestas_cups(descripcion: str, cups_retriever) -> tuple[list[dict[str, Any]], str]:
    if not descripcion:
        return [], ""

    df_cups = cups_retriever.asignar_codigos([descripcion])
    propuestas_cups: list[dict[str, Any]] = []
    for _, row in df_cups.iterrows():
        cups_item = {
            "procedimiento": row["procedimiento"],
            "codigo_cups": row["codigo_cups"],
            "score": getattr(row, "score", 0.0),
        }
        if not any(c["codigo_cups"] == cups_item["codigo_cups"] for c in propuestas_cups):
            propuestas_cups.append(cups_item)

    propuestas_cups = sorted(propuestas_cups, key=lambda x: x.get("score", 0.0), reverse=True)
    propuestas_cups, _ = limit_items(propuestas_cups, max_items=config.SOAT_PROMPT_MAX_CUPS)
    contexto = "\n".join(
        [
            f"CÓDIGO {c['codigo_cups']}: {c['procedimiento']} (Score: {c.get('score', 'N/A')})"
            for c in propuestas_cups
        ]
    )
    return propuestas_cups, contexto


def _buscar_propuestas_cie10(descripcion: str, retriever) -> tuple[list[dict[str, str]], str]:
    if not descripcion:
        return [], ""

    texto_cie10 = retriever.asignar_codigo_cie10(descripcion)
    propuestas_cie10 = _parse_cie10_lines(texto_cie10)
    propuestas_cie10, _ = limit_items(propuestas_cie10, max_items=config.SOAT_PROMPT_MAX_CIE10)
    contexto = "\n".join([f"CÓDIGO {c['codigo']}: {c['descripcion']}" for c in propuestas_cie10])
    return propuestas_cie10, contexto


def _construir_prompt_analisis_soat(
    codigo_soat: str,
    descripcion: str,
    evidencia_soat: list[dict[str, Any]],
    contexto_cups_detallado: str,
    contexto_cie10_detallado: str,
) -> str:
    contexto_soat = "\n\n".join(
        [
            f"📄 PÁGINA {e['page']} (Score: {e.get('score', 'N/A')}):\n{e['extracto']}"
            for e in evidencia_soat[:8]
        ]
    )
    return f"""
Eres un especialista en auditoría médica y codificación clínica en Colombia. Analiza el procedimiento SOAT y proporciona un análisis ESTRUCTURADO Y CLARO usando la evidencia RAG.

╔══════════════════════════════════════════════════════════════╗
║ PROCEDIMIENTO SOAT A ANALIZAR                                 ║
╚══════════════════════════════════════════════════════════════╝
• Código SOAT: {codigo_soat}
• Descripción: {descripcion}

╔══════════════════════════════════════════════════════════════╗
║ CONTEXTO RAG DISPONIBLE                                       ║
╚══════════════════════════════════════════════════════════════╝

📚 MANUAL SOAT:
{contexto_soat}

🔧 CÓDIGOS CUPS DISPONIBLES:
{contexto_cups_detallado}

🏥 CÓDIGOS CIE-10 DISPONIBLES:
{contexto_cie10_detallado}

╔══════════════════════════════════════════════════════════════╗
║ FORMATO DE RESPUESTA REQUERIDO                               ║
╚══════════════════════════════════════════════════════════════╝

SECCIÓN 1: VERIFICACIÓN CÓDIGO SOAT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Estado: [CONFIRMADO / NO ENCONTRADO / PARCIAL]
Descripción Oficial: [Del manual SOAT]
Páginas Referencia: [Números de página específicos]
Observaciones: [Notas relevantes del manual]

SECCIÓN 2: CÓDIGOS CUPS RECOMENDADOS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

A) PRINCIPALES (Prioridad Alta):
   • Código: [CUPS]
     Descripción: [Del RAG]
     Justificación: [Por qué es el más apropiado según RAG]

   • Código: [CUPS]
     Descripción: [Del RAG]
     Justificación: [Por qué complementa el principal]

B) ALTERNATIVOS (Considerar según caso):
   • Código: [CUPS] - [Descripción] - [Cuándo usar]
   • Código: [CUPS] - [Descripción] - [Cuándo usar]

SECCIÓN 3: CÓDIGOS CIE-10 RECOMENDADOS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

A) PRINCIPALES:
   • Código: [CIE-10]
     Descripción: [Del RAG]
     Tipo: [Traumatismo/Osteomuscular/Otro]
     Justificación: [Relación directa con procedimiento]

   • Código: [CIE-10]
     Descripción: [Del RAG]
     Tipo: [Traumatismo/Osteomuscular/Otro]
     Justificación: [Diagnóstico complementario]

B) SECUNDARIOS:
   • Código: [CIE-10] - [Descripción] - [Cuándo incluir]

SECCIÓN 4: ANÁLISIS RIESGO DE GLOSA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🔴 RIESGOS ALTOS:
   • [Riesgo específico basado en manual SOAT]
   • [Incoherencia identificada]

🟡 RIESGOS MEDIOS:
   • [Documentación parcial]
   • [Ambigüedad en descripción]

🟢 MITIGACIONES:
   • [Acción específica para prevenir glosa]
   • [Documentación adicional sugerida]

SECCIÓN 5: PERTINENCIA Y RECOMENDACIONES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✓ FUNDAMENTO SOAT:
  [Cita específica del manual con páginas]

✓ COHERENCIA CUPS-CIE10:
  [Explicación de por qué esta combinación es óptima]

✓ ACCIONES RECOMENDADAS:
  1. [Acción concreta]
  2. [Documentación requerida]
  3. [Nota aclaratoria sugerida]

IMPORTANTE:
- USA SOLO los códigos presentes en el contexto RAG
- CITA páginas específicas del manual SOAT
- SÉ ESPECÍFICO y ESTRUCTURADO en cada sección
"""


def _generar_analisis_rag(
    *,
    codigo_soat: str,
    descripcion: str,
    evidencia_soat: list[dict[str, Any]],
    contexto_cups_detallado: str,
    contexto_cie10_detallado: str,
    client_groq: Groq,
    client_gemini,
    llm_router=None,
) -> tuple[str, str, str]:
    try:
        prompt = _construir_prompt_analisis_soat(
            codigo_soat,
            descripcion,
            evidencia_soat,
            contexto_cups_detallado,
            contexto_cie10_detallado,
        )
        if llm_router is not None:
            response = generate_task_text(
                llm_router,
                task=LLMTask.SOAT_CHAT,
                prompt=prompt,
                system_prompt=(
                    "Eres un auditor médico experto. Usa TODA la evidencia RAG proporcionada para hacer "
                    "recomendaciones precisas."
                ),
                metadata={"operation": "legacy_soat_rag_analysis", "codigo_soat": codigo_soat},
                generation_config={"temperature": 0.1},
            )
            analisis_completo = response.content
        elif client_gemini:
            response = client_gemini.generate_content(prompt)
            analisis_completo = response.text
        elif _legacy_llm_enabled() and client_groq is not None:
            resp = client_groq.chat.completions.create(
                model=config.GROQ_MODEL_SOAT,
                temperature=0.1,
                messages=[
                    {
                        "role": "system",
                        "content": "Eres un auditor médico experto. Usa TODA la evidencia RAG proporcionada para hacer recomendaciones precisas.",
                    },
                    {"role": "user", "content": prompt},
                ],
            )
            analisis_completo = resp.choices[0].message.content
        else:
            raise RuntimeError("No hay capacidad Gemini configurada para el análisis SOAT.")

        analisis_completo = str(analisis_completo or "")

        recomendaciones_glosa = ""
        pertinencia_codigos = ""
        if "ANÁLISIS DE RIESGO DE GLOSA:" in analisis_completo:
            glosa_section = analisis_completo.split("ANÁLISIS DE RIESGO DE GLOSA:")[1]
            recomendaciones_glosa = (
                glosa_section.split("**5.")[0].strip() if "**5." in glosa_section else glosa_section.strip()
            )
        if "PERTINENCIA Y JUSTIFICACIÓN:" in analisis_completo:
            pertinencia_codigos = analisis_completo.split("PERTINENCIA Y JUSTIFICACIÓN:")[1].strip()
        return analisis_completo, recomendaciones_glosa, pertinencia_codigos
    except Exception:
        return (
            "No fue posible completar el análisis SOAT con Gemini.",
            "No se pudo generar análisis de glosa",
            "No se pudo generar análisis de pertinencia",
        )


def _generar_nota_aclaratoria_soat(
    descripcion: str,
    propuestas_cups: list[dict[str, Any]],
    propuestas_cie10: list[dict[str, str]],
    client_groq: Groq,
    client_gemini,
    llm_router=None,
) -> str:
    try:
        nota_prompt = f"""
Basándote en el análisis RAG del procedimiento "{descripcion}" y los códigos identificados, redacta una NOTA ACLARATORIA ESPECÍFICA para el posoperatorio inmediato.

Contexto de códigos identificados:
- CUPS principales: {[c["codigo_cups"] for c in propuestas_cups[:3]]}
- CIE-10 principales: {[c["codigo"] for c in propuestas_cie10[:3]]}

La nota debe:
1. Usar terminología médica precisa del contexto RAG
2. Justificar técnicamente el procedimiento realizado
3. Ser concisa pero completa (2-3 líneas)
4. Prevenir glosas específicas identificadas en el análisis

Formato: Nota directa lista para insertar en el expediente.
"""
        if llm_router is not None:
            resp_nota = generate_task_text(
                llm_router,
                task=LLMTask.GLOSA_NOTE_GENERATION,
                prompt=nota_prompt,
                system_prompt="Redacta notas aclaratorias médicas precisas y técnicas.",
                metadata={"operation": "legacy_soat_note_generation", "descripcion": descripcion[:200]},
                generation_config={"temperature": 0.0},
            )
            return resp_nota.content
        if client_gemini:
            resp_nota = client_gemini.generate_content(nota_prompt)
            return resp_nota.text
        if not _legacy_llm_enabled() or client_groq is None:
            return "No se pudo generar nota aclaratoria"
        resp_nota = client_groq.chat.completions.create(
            model=config.GROQ_MODEL_SOAT,
            temperature=0.0,
            messages=[
                {"role": "system", "content": "Redacta notas aclaratorias médicas precisas y técnicas."},
                {"role": "user", "content": nota_prompt},
            ],
        )
        return resp_nota.choices[0].message.content or "No se pudo generar nota aclaratoria"
    except Exception:
        return "No se pudo generar nota aclaratoria"


def _seleccionar_codigos_rag(
    propuestas_cups: list[dict[str, Any]],
    propuestas_cie10: list[dict[str, str]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, str]], list[dict[str, str]]]:
    cups_seleccionados = propuestas_cups[:4] if len(propuestas_cups) >= 3 else propuestas_cups[:2]
    cups_alternativas = propuestas_cups[4:10] if len(propuestas_cups) > 4 else propuestas_cups[2:6]
    cie10_seleccionados = propuestas_cie10[:5] if len(propuestas_cie10) >= 4 else propuestas_cie10[:3]
    cie10_alternativas = propuestas_cie10[5:12] if len(propuestas_cie10) > 5 else propuestas_cie10[3:8]
    return cups_seleccionados, cups_alternativas, cie10_seleccionados, cie10_alternativas


def _agente_codificar_desde_soat_item(
    item: dict[str, str],
    soat_retriever,
    retriever,
    cups_retriever,
    client_groq: Groq,
    client_gemini,
    *,
    llm_router=None,
):
    codigo_soat = (item.get("codigo_soat") or "").strip()
    descripcion = (item.get("descripcion") or "").strip()
    consulta = f"{codigo_soat} {descripcion}".strip()
    evidence_bundle = _collect_soat_reasoning_context(
        codigo_soat=codigo_soat,
        descripcion_soat=descripcion,
        evidencia_query=consulta,
        cie10_query_text=descripcion,
        cups_query_text=descripcion,
        soat_retriever=soat_retriever,
        retriever=retriever,
        cups_retriever=cups_retriever,
    )
    validation_model = _resolve_soat_validation_model(
        llm_router=llm_router,
        metadata={"operation": "soat_agent_reasoning", "codigo_soat": codigo_soat},
        codigo_soat=codigo_soat,
        descripcion_soat=descripcion,
        diagnosticos_prequx="",
        procedimientos=descripcion,
        hallazgos="",
        descripcion_proc=descripcion,
        justificacion="",
        diagnosticos_postqx="",
        evidence_bundle=evidence_bundle,
    )
    return _render_agent_soat_result(item=item, validation_model=validation_model)


def _extraer_seccion_documento(documento: str, seccion: str) -> str:
    """Extrae una sección específica del documento quirúrgico."""
    try:
        return extract_quirurgico_text_section(documento, seccion)
    except Exception:
        return ""


async def _validar_codigo_soat_con_documento(
    codigo_soat: str,
    descripcion_soat: str,
    diagnosticos_prequx: str,
    procedimientos: str,
    hallazgos: str,
    descripcion_proc: str,
    justificacion: str,
    diagnosticos_postqx: str,
    soat_retriever,
    retriever,
    cups_retriever,
    client_groq: Groq,
    client_gemini,
    *,
    llm_router=None,
) -> dict[str, Any]:
    """Valida un código SOAT específico contra la documentación quirúrgica usando Gemini con contexto RAG mejorado."""
    texto_diagnosticos = f"{diagnosticos_prequx} {diagnosticos_postqx} {hallazgos}".strip()
    texto_procedimientos = f"{procedimientos} {descripcion_proc}".strip()
    evidence_bundle = _collect_soat_reasoning_context(
        codigo_soat=codigo_soat,
        descripcion_soat=descripcion_soat,
        evidencia_query=f"{codigo_soat} {descripcion_soat}".strip(),
        cie10_query_text=texto_diagnosticos,
        cups_query_text=texto_procedimientos,
        soat_retriever=soat_retriever,
        retriever=retriever,
        cups_retriever=cups_retriever,
    )
    validation_model = _resolve_soat_validation_model(
        llm_router=llm_router,
        metadata={"operation": "soat_validate_invoice", "codigo_soat": codigo_soat},
        codigo_soat=codigo_soat,
        descripcion_soat=descripcion_soat,
        diagnosticos_prequx=diagnosticos_prequx,
        procedimientos=procedimientos,
        hallazgos=hallazgos,
        descripcion_proc=descripcion_proc,
        justificacion=justificacion,
        diagnosticos_postqx=diagnosticos_postqx,
        evidence_bundle=evidence_bundle,
    )
    return _render_invoice_validation_result(validation_model)


def _generar_resumen_validacion(
    resultados: list[dict[str, Any]], documento_quirurgico: str
) -> dict[str, Any]:
    """Genera un resumen ejecutivo de la validación de toda la factura."""
    total_codigos = len(resultados)
    justificados = sum(1 for r in resultados if "JUSTIFICADO" in r.get("validacion_resultado", ""))

    riesgos_altos = []
    recomendaciones_generales = []

    for resultado in resultados:
        codigo = resultado.get("codigo_soat", "")
        if "NO JUSTIFICADO" in resultado.get("validacion_resultado", ""):
            riesgos_altos.append(f"Código {codigo}: Falta justificación documental")

        if "PARCIALMENTE JUSTIFICADO" in resultado.get("validacion_resultado", ""):
            riesgos_altos.append(f"Código {codigo}: Justificación parcial - requiere documentación adicional")

    if not riesgos_altos:
        recomendaciones_generales.append("✅ Todos los códigos están adecuadamente justificados")
    else:
        recomendaciones_generales.append("⚠️ Se identificaron códigos con riesgo de glosa")
        recomendaciones_generales.append("📝 Revisar documentación quirúrgica para códigos señalados")

    return {
        "total_codigos_validados": total_codigos,
        "codigos_justificados": justificados,
        "porcentaje_justificacion": round((justificados / total_codigos) * 100, 2)
        if total_codigos > 0
        else 0,
        "riesgos_identificados": riesgos_altos,
        "recomendaciones_generales": recomendaciones_generales,
        "estado_general": "APROBADO" if justificados == total_codigos else "REQUIERE REVISIÓN",
    }


def _parsear_respuesta_gemini_codigos(respuesta: str) -> dict[str, Any]:
    """
    Parsea la respuesta estructurada de Gemini para extraer códigos CIE-10 y CUPS.
    """
    resultado = {
        "cie10_principal": None,
        "cie10_secundarios": [],
        "cups_principal": None,
        "cups_alternativos": [],
        "analisis_clinico": "",
    }

    # Extraer CIE-10 PRINCIPAL
    patron_cie10_principal = r"PRINCIPAL:\s*•\s*Código:\s*([A-Z]\d{1,3}(?:\.\d{1,2})?)\s*Descripción:\s*([^\n]+)\s*Justificación:\s*([^\n]+)"
    match = re.search(patron_cie10_principal, respuesta, re.IGNORECASE | re.DOTALL)
    if match:
        resultado["cie10_principal"] = {
            "codigo": match.group(1).strip(),
            "descripcion": match.group(2).strip(),
            "justificacion": match.group(3).strip(),
        }

    # Extraer CIE-10 SECUNDARIOS
    seccion_cie10 = re.search(r"SECUNDARIOS:(.*?)(?=CÓDIGOS CUPS|$)", respuesta, re.IGNORECASE | re.DOTALL)
    if seccion_cie10:
        items = re.findall(
            r"•\s*Código:\s*([A-Z]\d{1,3}(?:\.\d{1,2})?)\s*Descripción:\s*([^\n]+)\s*Justificación:\s*([^\n]+)",
            seccion_cie10.group(1),
            re.IGNORECASE,
        )
        for item in items:
            resultado["cie10_secundarios"].append(
                {"codigo": item[0].strip(), "descripcion": item[1].strip(), "justificacion": item[2].strip()}
            )

    # Extraer CUPS PRINCIPAL
    patron_cups_principal = r"CÓDIGOS CUPS RECOMENDADOS:.*?PRINCIPAL:\s*•\s*Código:\s*(\d{6})\s*Descripción:\s*([^\n]+)\s*Justificación:\s*([^\n]+)"
    match_cups = re.search(patron_cups_principal, respuesta, re.IGNORECASE | re.DOTALL)
    if match_cups:
        resultado["cups_principal"] = {
            "codigo": match_cups.group(1).strip(),
            "descripcion": match_cups.group(2).strip(),
            "justificacion": match_cups.group(3).strip(),
        }

    # Extraer CUPS ALTERNATIVOS
    seccion_cups = re.search(
        r"ALTERNATIVOS:(.*?)(?=ANÁLISIS CLÍNICO|$)", respuesta, re.IGNORECASE | re.DOTALL
    )
    if seccion_cups:
        items_cups = re.findall(
            r"•\s*Código:\s*(\d{6})\s*Descripción:\s*([^\n]+)\s*Justificación:\s*([^\n]+)",
            seccion_cups.group(1),
            re.IGNORECASE,
        )
        for item in items_cups:
            resultado["cups_alternativos"].append(
                {"codigo": item[0].strip(), "descripcion": item[1].strip(), "justificacion": item[2].strip()}
            )

    # Extraer análisis clínico completo
    analisis_match = re.search(r"ANÁLISIS CLÍNICO:(.*?)$", respuesta, re.IGNORECASE | re.DOTALL)
    if analisis_match:
        resultado["analisis_clinico"] = analisis_match.group(1).strip()

    return resultado


def _build_gemini_soat_prompt(codigo_soat: str, descripcion: str) -> str:
    return f"""
Eres un especialista en codificación médica en Colombia con conocimiento experto en:
- Sistema CIE-10 (Clasificación Internacional de Enfermedades)
- Sistema CUPS (Clasificación Única de Procedimientos en Salud)
- Manual SOAT 2024 y codificación de accidentes de tránsito

Se te proporciona una descripción de un procedimiento o diagnóstico del sistema SOAT.

╔══════════════════════════════════════════════════════════════╗
║ INFORMACIÓN DEL PROCEDIMIENTO/DIAGNÓSTICO                    ║
╚══════════════════════════════════════════════════════════════╝

Código SOAT: {codigo_soat if codigo_soat else "No especificado"}
Descripción: {descripcion}

╔══════════════════════════════════════════════════════════════╗
║ TU TAREA                                                      ║
╚══════════════════════════════════════════════════════════════╝

Genera los códigos médicos correspondientes siguiendo ESTRICTAMENTE este formato:

CÓDIGOS CIE-10 RECOMENDADOS:
----------------------------
PRINCIPAL:
• Código: [CIE-10]
  Descripción: [Descripción completa oficial CIE-10]
  Justificación: [Por qué este código es el más apropiado]

SECUNDARIOS:
• Código: [CIE-10]
  Descripción: [Descripción completa oficial CIE-10]
  Justificación: [Relación con el diagnóstico/procedimiento]

• Código: [CIE-10]
  Descripción: [Descripción completa oficial CIE-10]
  Justificación: [Relación con el diagnóstico/procedimiento]

CÓDIGOS CUPS RECOMENDADOS:
----------------------------
PRINCIPAL:
• Código: [6 dígitos CUPS]
  Descripción: [Descripción completa oficial CUPS]
  Justificación: [Por qué este procedimiento es el indicado]

ALTERNATIVOS:
• Código: [6 dígitos CUPS]
  Descripción: [Descripción completa oficial CUPS]
  Justificación: [Cuándo considerar este procedimiento]

• Código: [6 dígitos CUPS]
  Descripción: [Descripción completa oficial CUPS]
  Justificación: [Cuándo considerar este procedimiento]

ANÁLISIS CLÍNICO:
----------------------------
Coherencia diagnóstico-procedimiento:
[Explicación de por qué los códigos CIE-10 y CUPS propuestos son coherentes entre sí]

Consideraciones SOAT:
[Aspectos específicos relevantes para facturación SOAT]

Prevención de glosas:
[Recomendaciones para documentar adecuadamente estos códigos]

IMPORTANTE:
- Usa SOLO códigos reales y oficiales de CIE-10 y CUPS
- Los códigos CUPS deben tener exactamente 6 dígitos
- Los códigos CIE-10 deben seguir el formato estándar (letra + números + punto si aplica)
- Proporciona al menos 1 código principal de cada tipo
- Sé específico y preciso en las justificaciones
"""


def generar_codigos_desde_soat_con_gemini(
    descripciones_soat: list[dict[str, str]],
    *,
    client_gemini,
    llm_router=None,
    cache_repository=None,
    username: str = "",
) -> list[dict[str, str]]:
    if not descripciones_soat or not isinstance(descripciones_soat, list):
        raise ValueError("Se requiere una lista de descripciones SOAT")
    if not client_gemini and llm_router is None:
        raise ValueError("No hay capacidad LLM configurada para generar códigos SOAT.")

    resultados = []
    for item in descripciones_soat:
        descripcion = str(item.get("descripcion") or "").strip()
        codigo_soat = str(item.get("codigo_soat") or "").strip()

        if not descripcion:
            resultados.append(
                {
                    "codigo_soat": codigo_soat,
                    "descripcion": descripcion,
                    "error": "Descripción vacía",
                }
            )
            continue

        structured_code: SoatGeneratedCodeStructured | None = None
        cache_context = _resolve_soat_cache_context(
            llm_router=llm_router,
            task=LLMTask.SOAT_CODE_GENERATION,
            username=username,
        )
        fingerprint = None
        if cache_repository is not None and cache_context is not None:
            fingerprint = build_llm_task_fingerprint(
                task=LLMTask.SOAT_CODE_GENERATION.value,
                provider=cache_context["provider"],
                model=cache_context["model"],
                cache_version=getattr(cache_repository, "cache_version", "v1"),
                prompt_version=_SOAT_CODE_GENERATION_PROMPT_VERSION,
                schema_version=str(SoatGeneratedCodeStructured.model_fields["schema_version"].default),
                payload={"codigo_soat": codigo_soat, "descripcion": descripcion},
            )
            cached = cache_repository.get(
                username=cache_context["username"],
                task=LLMTask.SOAT_CODE_GENERATION.value,
                fingerprint=fingerprint,
            )
            if cached and isinstance(cached.get("payload"), dict):
                try:
                    structured_code = SoatGeneratedCodeStructured.model_validate(cached["payload"])
                except Exception:
                    structured_code = None
        try:
            if structured_code is None:
                structured_code = _generate_structured_soat_response(
                    llm_router=llm_router,
                    task=LLMTask.SOAT_CODE_GENERATION,
                    prompt=_build_code_generation_structured_prompt(codigo_soat, descripcion),
                    output_model=SoatGeneratedCodeStructured,
                    output_kind=LLMOutputKind.STRUCTURED_OBJECT,
                    metadata={"operation": "soat_code_generation", "codigo_soat": codigo_soat},
                )
        except Exception:
            logger.exception("Error generando codificación SOAT estructurada")
            structured_code = None

        if structured_code is None:
            structured_code = _build_fallback_generated_code_structured(
                codigo_soat=codigo_soat,
                descripcion=descripcion,
                error="No se pudo generar codificación estructurada SOAT.",
            )
        elif (
            cache_repository is not None
            and cache_context is not None
            and fingerprint is not None
            and not structured_code.error
        ):
            cache_repository.upsert(
                username=cache_context["username"],
                task=LLMTask.SOAT_CODE_GENERATION.value,
                fingerprint=fingerprint,
                provider=cache_context["provider"],
                model=cache_context["model"],
                payload=structured_code.model_dump(by_alias=True, exclude_none=True),
                metadata={
                    "prompt_version": _SOAT_CODE_GENERATION_PROMPT_VERSION,
                    "schema_version": structured_code.schema_version,
                },
            )

        resultados.append(
            _render_generated_code_payload(
                codigo_soat=codigo_soat,
                descripcion=descripcion,
                structured_code=structured_code,
            )
        )
    return resultados
