from __future__ import annotations

import logging
import re
from datetime import date, datetime
from typing import Any

from groq import Groq

from app.config import config
from app.llm import LLMOutputKind, LLMTask, ModelSelectionPolicy
from app.llm.models import LLMResolvedRoute
from app.llm.providers import GeminiAdapter, GroqAdapter
from app.llm.routing import DefaultModelRouter, DefaultModelSelectionPolicy
from app.llm.schemas import (
    DocumentoQuirurgicoStructured,
    FacturaStructured,
    HistoriaClinicaStructured,
    normalize_factura_date_value,
)
from app.services.clinical_document_projection import (
    get_structured_document_model,
    render_document_analysis_html,
)
from app.services.clinical_structured_extraction import ClinicalStructuredExtractionService
from app.services.factura_html_legacy import parse_factura_html_to_json
from app.services.historia_antecedentes import (
    antecedentes_to_flat_list,
    consolidate_antecedentes,
    extract_historia_antecedentes_structured,
)
from app.services.historia_html_legacy import (
    extract_historia_antecedentes_from_html,
    extract_historia_medicamentos_from_html,
    extract_historia_metadatos_from_html,
    extract_historia_nombre_resumen_from_html,
    extract_historia_procedimientos_from_html,
)
from app.services.historia_medicamentos import (
    consolidate_medication_items,
    extract_valle_salud_medications,
    medication_from_legacy,
    normalize_medication_key,
)
from app.services.historia_summary import (
    extract_historia_antecedentes_text,
    extract_historia_edad_text,
    extract_historia_fecha_ingreso_text,
    extract_historia_fecha_nacimiento_text,
    extract_historia_medicamentos_text,
    extract_historia_motivo_text,
    extract_historia_prestador_text,
    extract_historia_sexo_text,
    resolve_historia_resumen,
)
from app.services.patient_name_extraction import extract_patient_name_from_html
from app.services.quirurgico_html_legacy import (
    extract_quirurgico_diagnosticos_from_html,
    extract_quirurgico_procedimientos_from_html,
    extract_quirurgico_sections_from_html,
)


_MEDICAMENTO_GENERIC_CODES = {"2801"}
_MEDICAMENTO_CODE_PATTERN = re.compile(r"^[A-Z0-9]{2,12}$", re.IGNORECASE)
_PLACEHOLDER_TOKEN_PATTERN = re.compile(
    r"^\[(?:dato|desc|valor|med|dosis|cant|unit|total|cups|tipo|dias|tarifa|nombre|observaci[oó]n)\]$",
    re.IGNORECASE,
)
_PLACEHOLDER_INLINE_PATTERN = re.compile(
    r"\[(?:dato|desc|valor|med|dosis|cant|unit|total|cups|tipo|dias|tarifa|nombre|observaci[oó]n)\]",
    re.IGNORECASE,
)
_ITEM_KEYWORDS = {
    "insumo_material": [
        "sutura",
        "curacion",
        "curación",
        "gasa",
        "vendaje",
        "ferula",
        "férula",
        "yeso",
        "algodon",
        "algodón",
        "apósito",
        "aposito",
        "material",
        "aguja",
    ],
    "equipo_dispositivo": [
        "cateter",
        "catéter",
        "abocat",
        "buretrol",
        "equipo",
        "venoclisis",
        "jeringa",
        "sonda",
        "canula",
        "cánula",
        "dispositivo",
    ],
    "solucion": [
        "solucion",
        "solución",
        "cloruro de sodio",
        "solucion salina",
        "solución salina",
        "lactato de ringer",
        "dextrosa",
    ],
}
logger = logging.getLogger(__name__)
_LOW_QUALITY_TEXT_VALUES = {
    "",
    "...",
    "[...]",
    "no especificado",
    "no disponible",
    "sin informacion",
    "sin información",
    "descripcion estructurada",
    "descripción estructurada",
    "hallazgos estructurados",
    "resumen estructurado",
}
_LOW_QUALITY_MOTIVO_SNIPPETS = {"motivo de consulta", "consulta externa", "valoracion medica", "valoración médica"}
_LOW_QUALITY_PROVIDER_SNIPPETS = {"ips", "prestador", "institucion", "institución"}
_CIVIL_STATUS_PATTERN = re.compile(
    r"\b(estado civil|solter[oa]|casad[oa]|uni[oó]n libre|viud[oa]|divorciad[oa])\b",
    re.IGNORECASE,
)
_SYMPTOM_ONLY_PATTERN = re.compile(
    r"^\s*(dolor|fiebre|tos|mareo|n[aá]usea|vomito|v[oó]mito|cefalea|diarrea|"
    r"malestar|sangrado|edema|disnea|ardor|prurito|inflamaci[oó]n)\b",
    re.IGNORECASE,
)


class _ForcedProviderPolicy:
    def __init__(self, *, provider: str, model: str) -> None:
        self._provider = provider
        self._model = model

    def resolve(self, task: LLMTask, metadata: dict[str, Any] | None = None) -> LLMResolvedRoute:
        _ = metadata
        return LLMResolvedRoute(
            task=task,
            output_kind=LLMOutputKind.STRUCTURED_OBJECT,
            provider=self._provider,
            model=self._model,
        )


def _doc_structured(value):
    return get_structured_document_model(value if isinstance(value, dict) else None)


def _doc_html(value) -> str:
    if isinstance(value, dict):
        return render_document_analysis_html(value)
    return str(value or "")


def _source_analysis_html(value) -> str:
    if isinstance(value, dict):
        return str(value.get("analisis_html") or "").strip()
    return str(value or "")


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


def _clean_clinical_key(value: Any) -> str:
    return re.sub(r"[^a-z0-9]+", "", _clean_clinical_text(value).lower())


def _is_low_quality_clinical_text(
    value: Any,
    *,
    min_len: int = 0,
    generic_values: set[str] | None = None,
) -> bool:
    text = _clean_clinical_text(value)
    normalized = text.lower()
    if not text:
        return True
    if len(text) < min_len:
        return True
    if normalized in _LOW_QUALITY_TEXT_VALUES:
        return True
    return bool(generic_values and normalized in generic_values)


def _prefer_clinical_value(
    preferred: Any,
    fallback: Any,
    *,
    min_preferred_len: int = 0,
    generic_values: set[str] | None = None,
) -> str:
    preferred_text = _clean_clinical_text(preferred)
    fallback_text = _clean_clinical_text(fallback)
    if _is_low_quality_clinical_text(
        preferred_text,
        min_len=min_preferred_len,
        generic_values=generic_values,
    ) and fallback_text:
        return fallback_text
    return preferred_text or fallback_text


def _has_placeholder_medicamento(item: dict[str, str]) -> bool:
    nombre = _sanitizar_placeholder(item.get("medicamento"))
    texto_original = _sanitizar_placeholder(item.get("texto_original"))
    return not nombre and not texto_original


def _is_meaningful_medicamento_canonico(item: dict[str, str]) -> bool:
    nombre = _sanitizar_placeholder(item.get("medicamento"))
    texto_original = _sanitizar_placeholder(item.get("texto_original"))
    dosis = _sanitizar_placeholder(item.get("dosis"))
    cantidad = _sanitizar_placeholder(item.get("cantidad"))
    if _has_placeholder_medicamento(item) and not dosis and not cantidad:
        return False
    if nombre.lower() == "no especificado" and not dosis and not cantidad:
        return False
    return bool(nombre or texto_original or dosis or cantidad)


def _merge_medicamentos_canonicos(*groups: list[dict[str, str]]) -> list[dict[str, str]]:
    merged: list[dict[str, str]] = []
    seen: set[str] = set()
    for group in groups:
        for item in group:
            if not isinstance(item, dict) or not _is_meaningful_medicamento_canonico(item):
                continue
            key = "|".join(
                [
                    _clean_clinical_key(item.get("codigo")),
                    _clean_clinical_key(item.get("medicamento")),
                    _clean_clinical_key(item.get("dosis")),
                    _clean_clinical_key(item.get("cantidad")),
                    _clean_clinical_key(item.get("tipo_uso")),
                    _clean_clinical_key(item.get("fuente")),
                ]
            )
            fallback_key = "|".join(
                [
                    _clean_clinical_key(item.get("medicamento")),
                    _clean_clinical_key(item.get("dosis")),
                    _clean_clinical_key(item.get("cantidad")),
                ]
            )
            candidate_key = key if key.strip("|") else fallback_key
            if not candidate_key or candidate_key in seen:
                continue
            seen.add(candidate_key)
            merged.append(item)
    return merged


def _is_low_quality_provider(value: Any, *, case_number: Any = "", patient_id: Any = "") -> bool:
    text = _clean_clinical_text(value)
    normalized = text.lower()
    if _is_low_quality_clinical_text(text, min_len=3):
        return True
    if normalized in _LOW_QUALITY_PROVIDER_SNIPPETS:
        return True
    if re.search(r"x(?:[\s.*_-]*x){3,}", text, re.IGNORECASE):
        return True
    candidate_key = _clean_clinical_key(text)
    return candidate_key in {
        _clean_clinical_key(case_number),
        _clean_clinical_key(patient_id),
    }


def _is_low_quality_motivo_consulta(value: Any) -> bool:
    text = _clean_clinical_text(value)
    normalized = text.lower()
    if _is_low_quality_clinical_text(text, min_len=6):
        return True
    return normalized in _LOW_QUALITY_MOTIVO_SNIPPETS or bool(_CIVIL_STATUS_PATTERN.search(text))


def _parse_historia_date(value: Any) -> date | None:
    normalized = normalize_factura_date_value(value, allow_time=True)
    if not normalized:
        return None
    try:
        if "T" in normalized:
            return datetime.fromisoformat(normalized.replace("Z", "+00:00")).date()
        return date.fromisoformat(normalized)
    except ValueError:
        return None


def _resolve_historia_edad(value: Any, *, raw_text: str, fecha_nacimiento: Any, fecha_ingreso: Any) -> str:
    explicit = _clean_clinical_text(value) or _clean_clinical_text(extract_historia_edad_text(raw_text))
    if explicit:
        return explicit
    birth_date = _parse_historia_date(fecha_nacimiento)
    admission_date = _parse_historia_date(fecha_ingreso)
    if not birth_date or not admission_date:
        return ""
    years = admission_date.year - birth_date.year
    if (admission_date.month, admission_date.day) < (birth_date.month, birth_date.day):
        years -= 1
    return f"{years} años" if years >= 0 else ""


def _clean_historia_text_list(values: Any) -> list[str]:
    if isinstance(values, str):
        values = re.split(r"\n|;", values)
    if not isinstance(values, list):
        return []
    result: list[str] = []
    seen: set[str] = set()
    for item in values:
        text = _clean_clinical_text(item)
        if not text or text.lower() == "no especificado":
            continue
        key = _clean_clinical_key(text)
        if key in seen:
            continue
        seen.add(key)
        result.append(text)
    return result


def _extract_raw_historia_medicamentos(raw_text: str) -> list[dict[str, str]]:
    section = extract_historia_medicamentos_text(raw_text)
    lines = _clean_historia_text_list(section)
    return [normalizar_medicamento_canonico(line, fuente="historia_clinica") for line in lines]


def _is_symptom_only_diagnostico(value: Any) -> bool:
    text = _clean_clinical_text(value)
    if not text:
        return False
    if re.search(r"\b[A-TV-Z]\d{2}[0-9A-Z]?(?:\.[0-9A-Z]{1,2})?\b", text, re.IGNORECASE):
        return False
    if re.search(r"\b(diagn[oó]stic|impresi[oó]n diagn[oó]stica|dx)\b", text, re.IGNORECASE):
        return False
    return bool(_SYMPTOM_ONLY_PATTERN.search(text)) and len(text.split()) <= 5


def _is_low_quality_qx_section(value: Any, *, min_len: int) -> bool:
    text = _clean_clinical_text(value)
    if _is_low_quality_clinical_text(text, min_len=min_len):
        return True
    return not bool(re.search(r"[a-záéíóúñ]{4,}", text, re.IGNORECASE))


def _merge_historia_metadatos_with_fallback(
    structured_data: dict[str, Any],
    *,
    html_data: dict[str, str],
    raw_text: str = "",
) -> dict[str, Any]:
    merged = dict(structured_data)
    html_data = html_data or {}
    patient_id = html_data.get("datos_identificacion_paciente") or merged.get("datos_identificacion_paciente")
    case_number = html_data.get("caso") or merged.get("caso")

    for field in (
        "nombre_paciente",
        "caso",
        "datos_identificacion_paciente",
        "sexo",
        "edad",
        "fecha_ingreso",
        "fecha_nacimiento",
    ):
        if not _clean_clinical_text(merged.get(field)) and _clean_clinical_text(html_data.get(field)):
            merged[field] = html_data.get(field)

    raw_sexo = extract_historia_sexo_text(raw_text)
    if raw_sexo:
        merged["sexo"] = raw_sexo.upper()

    raw_prestador = extract_historia_prestador_text(raw_text)
    if raw_prestador and (
        not _clean_clinical_text(merged.get("prestador_servicio"))
        or _is_low_quality_provider(
            merged.get("prestador_servicio"),
            case_number=case_number,
            patient_id=patient_id,
        )
    ):
        merged["prestador_servicio"] = raw_prestador

    if _is_low_quality_provider(
        merged.get("prestador_servicio"),
        case_number=case_number,
        patient_id=patient_id,
    ):
        merged["prestador_servicio"] = _prefer_clinical_value(
            html_data.get("prestador_servicio"),
            merged.get("prestador_servicio"),
            min_preferred_len=3,
        )

    raw_fecha_nacimiento = normalize_factura_date_value(
        extract_historia_fecha_nacimiento_text(raw_text),
        allow_time=False,
    )
    structured_fecha_nacimiento = normalize_factura_date_value(
        merged.get("fecha_nacimiento"),
        allow_time=False,
    )
    fecha_nacimiento = raw_fecha_nacimiento or structured_fecha_nacimiento
    if fecha_nacimiento:
        merged["fecha_nacimiento"] = fecha_nacimiento
    else:
        merged.pop("fecha_nacimiento", None)

    raw_fecha_ingreso = normalize_factura_date_value(
        extract_historia_fecha_ingreso_text(raw_text),
        allow_time=True,
    )
    structured_fecha_ingreso = normalize_factura_date_value(
        merged.get("fecha_ingreso"),
        allow_time=True,
    )
    fecha_ingreso = raw_fecha_ingreso or structured_fecha_ingreso
    if fecha_ingreso:
        merged["fecha_ingreso"] = fecha_ingreso
    else:
        merged.pop("fecha_ingreso", None)

    edad = _resolve_historia_edad(
        merged.get("edad"),
        raw_text=raw_text,
        fecha_nacimiento=merged.get("fecha_nacimiento"),
        fecha_ingreso=merged.get("fecha_ingreso"),
    )
    if edad:
        merged["edad"] = edad

    if _is_low_quality_motivo_consulta(merged.get("motivo_consulta")):
        raw_motivo = extract_historia_motivo_text(raw_text)
        merged["motivo_consulta"] = _prefer_clinical_value(
            raw_motivo or html_data.get("motivo_consulta"),
            merged.get("motivo_consulta"),
            min_preferred_len=6,
        )
    if _clean_clinical_text(merged.get("motivo_consulta")):
        merged["motivo_consulta"] = _clean_clinical_text(merged.get("motivo_consulta")).upper()

    antecedentes = _clean_historia_text_list(merged.get("antecedentes"))
    raw_antecedentes = _clean_historia_text_list(extract_historia_antecedentes_text(raw_text))
    html_antecedentes = _clean_historia_text_list(html_data.get("antecedentes"))
    antecedentes = _clean_historia_text_list([*antecedentes, *raw_antecedentes, *html_antecedentes])
    if antecedentes:
        merged["antecedentes"] = antecedentes

    return merged


def _sanitizar_placeholder(value) -> str:
    texto = str(value or "").strip()
    if not texto:
        return ""
    texto_limpio = _PLACEHOLDER_INLINE_PATTERN.sub("", texto).strip(" -:/\t\r\n")
    if _PLACEHOLDER_TOKEN_PATTERN.match(texto) or texto.lower() in {"no disponible", "n/a", "none"}:
        return ""
    return texto_limpio.strip()


def _clasificar_tipo_item_medicamento(nombre: str, fuente: str) -> tuple[str, str]:
    nombre_norm = _sanitizar_placeholder(nombre).lower()
    if not nombre_norm:
        return "desconocido", "sin_nombre"
    for tipo, keywords in _ITEM_KEYWORDS.items():
        if any(keyword in nombre_norm for keyword in keywords):
            return tipo, "heuristica_keyword"
    if fuente == "factura":
        return "medicamento", "fallback_factura"
    return "medicamento", "fallback_clinico"


def _separar_nombre_y_dosis_en_texto(texto: str) -> tuple[str, str]:
    limpio = _sanitizar_placeholder(texto)
    if not limpio:
        return "", ""
    match = re.search(r"\d", limpio)
    if not match:
        return limpio, ""
    idx = match.start()
    nombre = limpio[:idx].strip(" -:/")
    dosis = limpio[idx:].strip(" -:/")
    if not nombre:
        return limpio, ""
    return nombre, dosis


def _texto_medicamento(value) -> str:
    return str(value or "").strip()


def _resolver_estado_codigo_medicamento(codigo: str, codigo_tipo: str, fuente: str) -> str:
    codigo_norm = _texto_medicamento(codigo).upper()
    if not codigo_norm:
        return "no_identificado"
    if codigo_norm in _MEDICAMENTO_GENERIC_CODES:
        return "pendiente_validacion"
    if fuente == "factura" and codigo_tipo != "medicamento":
        return "pendiente_validacion"
    return "identificado"


def normalizar_medicamento_canonico(item, fuente: str = "historia_clinica") -> dict[str, Any]:
    """
    Normaliza medicamentos de historia clínica, factura o entrada manual a un shape canónico.
    """
    fuente_norm = _texto_medicamento(fuente) or "desconocida"
    texto_original = ""
    codigo = ""
    medicamento = ""
    dosis = ""
    cantidad = ""
    codigo_tipo = "desconocido"
    codigo_origen = "desconocido"
    codigo_estado = ""
    tipo_item = "desconocido"
    tipo_item_origen = "desconocido"
    tipo_uso = ""

    if item and isinstance(item, dict) and not isinstance(item, list):
        fuente_norm = _texto_medicamento(item.get("fuente")) or fuente_norm
        codigo = _texto_medicamento(
            item.get("codigo_referencia")
            or item.get("codigo_medicamento")
            or item.get("cod_medicamento")
            or item.get("codigo")
        )
        medicamento = _sanitizar_placeholder(
            item.get("medicamento") or item.get("nombre") or item.get("detalle")
        )
        dosis = _sanitizar_placeholder(item.get("dosis"))
        cantidad = _sanitizar_placeholder(item.get("cantidad") or item.get("cant"))
        texto_original = _sanitizar_placeholder(item.get("texto_original"))
        codigo_estado = _texto_medicamento(item.get("codigo_estado"))
        codigo_tipo = _texto_medicamento(item.get("codigo_tipo")) or "desconocido"
        codigo_origen = _texto_medicamento(item.get("codigo_origen")) or "desconocido"
        tipo_item = _texto_medicamento(item.get("tipo_item")) or "desconocido"
        tipo_item_origen = _texto_medicamento(item.get("tipo_item_origen")) or "desconocido"
        tipo_uso = _texto_medicamento(item.get("tipo_uso")) or _texto_medicamento(item.get("uso"))

        if _texto_medicamento(item.get("codigo_medicamento")) or _texto_medicamento(
            item.get("cod_medicamento")
        ):
            codigo_tipo = "medicamento"
            codigo_origen = "codigo_medicamento"
        elif _texto_medicamento(item.get("codigo")):
            codigo_tipo = "facturacion" if fuente_norm == "factura" else "medicamento"
            codigo_origen = "codigo"

        if not texto_original:
            partes = [codigo, cantidad, medicamento, dosis]
            texto_original = " - ".join([parte for parte in partes if parte])
    else:
        texto_original = _sanitizar_placeholder(re.sub(r"<.*?>", "", _texto_medicamento(item)))
        partes = [parte.strip() for parte in re.split(r"\s*-\s*", texto_original) if parte.strip()]
        tiene_codigo = bool(partes and _MEDICAMENTO_CODE_PATTERN.match(partes[0]))

        if len(partes) >= 4 and tiene_codigo:
            codigo = partes[0]
            cantidad = partes[1]
            medicamento = partes[2]
            dosis = " - ".join(partes[3:])
        elif len(partes) >= 3 and tiene_codigo:
            codigo = partes[0]
            medicamento = partes[1]
            dosis = partes[2]
            cantidad = partes[3] if len(partes) > 3 else ""
        elif len(partes) >= 3:
            cantidad = partes[0]
            medicamento = partes[1]
            dosis = " - ".join(partes[2:])
        elif len(partes) == 2:
            if _MEDICAMENTO_CODE_PATTERN.match(partes[0]):
                codigo = partes[0]
                medicamento = partes[1]
            else:
                medicamento = partes[0]
                dosis = partes[1]
        elif len(partes) == 1:
            medicamento, dosis = _separar_nombre_y_dosis_en_texto(partes[0])

        if codigo:
            codigo_tipo = (
                "medicamento" if fuente_norm in {"historia_clinica", "manual", "cache_pdf"} else "desconocido"
            )
            codigo_origen = "texto"

    codigo_estado = codigo_estado or _resolver_estado_codigo_medicamento(codigo, codigo_tipo, fuente_norm)
    tipo_item, tipo_item_origen = (
        (tipo_item, tipo_item_origen)
        if tipo_item and tipo_item != "desconocido"
        else _clasificar_tipo_item_medicamento(medicamento or texto_original, fuente_norm)
    )
    if codigo_estado != "identificado":
        codigo = codigo if codigo_estado == "pendiente_validacion" else ""

    immediate_text = " ".join([texto_original, dosis, tipo_uso]).casefold()
    if not cantidad and re.search(r"\b(?:aplicar|administrar|dar|usar)?\s*ahora\b", immediate_text):
        cantidad = "1"

    canonical = {
        "codigo": codigo,
        "codigo_estado": codigo_estado,
        "codigo_tipo": codigo_tipo,
        "codigo_origen": codigo_origen,
        "medicamento": medicamento or "No especificado",
        "dosis": dosis or "",
        "cantidad": cantidad or "",
        "fuente": fuente_norm,
        "texto_original": texto_original or medicamento or "No especificado",
        "tipo_item": tipo_item,
        "tipo_item_origen": tipo_item_origen,
        "tipo_uso": tipo_uso,
    }
    if isinstance(item, dict):
        raw_estados = item.get("estados") or ([tipo_uso] if tipo_uso else [])
        estados = raw_estados if isinstance(raw_estados, list) else [raw_estados]
        raw_fuentes = item.get("fuentes") or ([fuente_norm] if fuente_norm else [])
        fuentes = raw_fuentes if isinstance(raw_fuentes, list) else [raw_fuentes]
        raw_evidencias = item.get("evidencias") or []
        evidencias = raw_evidencias if isinstance(raw_evidencias, list) else [raw_evidencias]
        canonical.update(
            {
                "key": str(item.get("key") or ""),
                "nombre": str(item.get("nombre") or medicamento or ""),
                "presentacion": str(item.get("presentacion") or ""),
                "posologia": str(item.get("posologia") or ""),
                "estados": estados,
                "codigo_facturacion": str(item.get("codigo_facturacion") or ""),
                "codigo_referencia": str(item.get("codigo_referencia") or ""),
                "fuentes": fuentes,
                "evidencias": evidencias,
                "discrepancia_cantidad": bool(item.get("discrepancia_cantidad")),
                "cantidad_clinica": item.get("cantidad_clinica"),
                "cantidad_facturada": item.get("cantidad_facturada"),
                "unidad_cantidad": str(item.get("unidad_cantidad") or ""),
                "asociacion_factura": str(item.get("asociacion_factura") or "no_evaluable"),
                "candidatos_factura": list(item.get("candidatos_factura") or []),
                "pertinencia": (
                    dict(item["pertinencia"]) if isinstance(item.get("pertinencia"), dict) else None
                ),
            }
        )
        numeric_quantity = item.get("cantidad")
        if isinstance(numeric_quantity, (int, float)):
            canonical["cantidad"] = numeric_quantity
    return canonical


def normalizar_lista_medicamentos(items, fuente: str = "historia_clinica") -> list[dict[str, Any]]:
    if not isinstance(items, list):
        return []
    return [normalizar_medicamento_canonico(item, fuente=fuente) for item in items if item]


def formatear_medicamento_canonico(medicamento: dict[str, Any]) -> str:
    if medicamento.get("estados") or medicamento.get("posologia") or medicamento.get("presentacion"):
        nombre = _texto_medicamento(medicamento.get("nombre") or medicamento.get("medicamento"))
        posologia = _texto_medicamento(medicamento.get("posologia"))
        dosis = _texto_medicamento(medicamento.get("dosis"))
        cantidad = _texto_medicamento(medicamento.get("cantidad"))
        estados = " · ".join(
            str(value).replace("_", " ").capitalize()
            for value in medicamento.get("estados", [])
            if str(value).strip()
        )
        return " - ".join(part for part in (nombre, posologia, dosis, cantidad, estados) if part)
    tipo_item = _texto_medicamento(medicamento.get("tipo_item"))
    nombre = _texto_medicamento(medicamento.get("medicamento"))
    cantidad = _sanitizar_placeholder(medicamento.get("cantidad"))
    dosis = _sanitizar_placeholder(medicamento.get("dosis"))
    tipo_uso = _texto_medicamento(medicamento.get("tipo_uso"))
    if tipo_item == "insumo_material":
        principal = f"INSUMO / MATERIAL - {nombre}" if nombre else "INSUMO / MATERIAL"
    elif tipo_item == "equipo_dispositivo":
        principal = f"EQUIPO / DISPOSITIVO - {nombre}" if nombre else "EQUIPO / DISPOSITIVO"
    elif tipo_item == "solucion":
        codigo_visible = (
            medicamento.get("codigo")
            if medicamento.get("codigo_estado") == "identificado" and medicamento.get("codigo")
            else "Código no identificado"
        )
        principal = f"{codigo_visible} - {nombre}" if nombre else codigo_visible
    else:
        codigo_visible = (
            medicamento.get("codigo")
            if medicamento.get("codigo_estado") == "identificado" and medicamento.get("codigo")
            else "Código no identificado"
        )
        principal = f"{codigo_visible} - {nombre}" if nombre else codigo_visible
    return " - ".join([parte for parte in [principal, tipo_uso, cantidad, dosis] if parte])


def _build_clinical_processing_router(
    client_groq: Groq | None,
    client_gemini=None,
    *,
    force_provider: str | None = None,
) -> DefaultModelRouter:
    policy: ModelSelectionPolicy = DefaultModelSelectionPolicy()
    forced = str(force_provider or "").strip().lower()
    if forced == "gemini":
        policy = _ForcedProviderPolicy(provider="gemini", model=config.GEMINI_MODEL_EXTRACT)
    elif forced == "groq" and config.LEGACY_LLM_PROVIDERS_ENABLED:
        policy = _ForcedProviderPolicy(provider="groq", model=config.GROQ_MODEL_CLINICAL)
    providers: dict[str, Any] = {
        "gemini": GeminiAdapter(
            api_key=config.GEMINI_API_KEY,
            default_model=config.GEMINI_MODEL_DEFAULT,
            service="clinical_processing_gemini_provider",
            client=client_gemini,
        ),
    }
    if config.LEGACY_LLM_PROVIDERS_ENABLED:
        providers["groq"] = GroqAdapter(
            api_key=config.GROQ_API_KEY,
            default_model=config.GROQ_MODEL_DEFAULT,
            service="clinical_processing_groq_provider",
            client=client_groq,
        )
    return DefaultModelRouter(
        providers=providers,
        policy=policy,
        service="clinical_processing_router",
    )


def _procesar_documento_estructurado(
    texto: str,
    *,
    document_type: str,
    client_groq: Groq | None,
    client_gemini=None,
    force_provider: str | None = None,
) -> str:
    router = _build_clinical_processing_router(
        client_groq,
        client_gemini,
        force_provider=force_provider,
    )
    return ClinicalStructuredExtractionService(llm_router=router).extract(
        raw_text=texto,
        document_type=document_type,
    ).rendered_html


def extraer_nombre_paciente(analisis_html) -> str:
    """Extrae el nombre del paciente del HTML de análisis"""
    model = _doc_structured(analisis_html)
    if model and str(model.patient_name or "").strip():
        return str(model.patient_name).strip()
    logger.debug("Analizando HTML para extraer nombre del paciente")
    nombre = extract_patient_name_from_html(_doc_html(analisis_html))
    if not nombre:
        logger.warning("No se pudo extraer el nombre del paciente desde analisis_html")
        return "desconocido"

    logger.info("Nombre del paciente extraído correctamente")
    return nombre


def procesar_laboratorio(texto: str, client_groq: Groq, client_gemini=None) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        return _procesar_documento_estructurado(
            texto,
            document_type="laboratorio",
            client_groq=client_groq,
            client_gemini=client_gemini,
        )
    except Exception as e:
        return f"Error procesando laboratorio: {str(e)}"


def procesar_radiologia(texto: str, client_groq: Groq, client_gemini=None) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        return _procesar_documento_estructurado(
            texto,
            document_type="radiologia",
            client_groq=client_groq,
            client_gemini=client_gemini,
        )
    except Exception as e:
        return f"Error procesando radiología: {str(e)}"


def procesar_prescripcion(texto: str, client_groq: Groq, client_gemini=None) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        return _procesar_documento_estructurado(
            texto,
            document_type="prescripcion",
            client_groq=client_groq,
            client_gemini=client_gemini,
        )
    except Exception as e:
        return f"Error procesando prescripción: {str(e)}"


def procesar_documento_quirurgico(texto: str, client_groq: Groq, client_gemini=None) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        return _procesar_documento_estructurado(
            texto,
            document_type="quirurgico",
            client_groq=client_groq,
            client_gemini=client_gemini,
        )
    except Exception as e:
        return f"Error procesando documento quirúrgico: {str(e)}"


def procesar_documento_generico(
    texto: str,
    tipo_documento: str,
    client_groq: Groq,
    client_gemini=None,
    force_provider: str | None = None,
) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        normalized_type = str(tipo_documento or "generico").strip() or "generico"
        return _procesar_documento_estructurado(
            texto,
            document_type=normalized_type,
            client_groq=client_groq,
            client_gemini=client_gemini,
            force_provider=force_provider,
        )
    except Exception as e:
        return f"Error procesando documento {tipo_documento}: {str(e)}"


def extraer_procedimientos_quirurgicos(analisis_html):
    """
    Extrae los procedimientos quirúrgicos del HTML analizado.
    Busca específicamente la sección 'PROCEDIMIENTOS REALIZADOS'.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, DocumentoQuirurgicoStructured):
        return [
            f"{item.codigo} {item.descripcion}".strip() if item.codigo else item.descripcion
            for item in model.procedimientos
            if str(item.descripcion or "").strip()
        ]
    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return extract_quirurgico_procedimientos_from_html(analisis_html)


def extraer_diagnosticos_quirurgicos(analisis_html):
    """
    Extrae los diagnósticos del documento quirúrgico (Prequirúrgicos y Postquirúrgicos).
    Busca en la sección '3. DIAGNÓSTICOS'.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, DocumentoQuirurgicoStructured):
        return [
            f"{item.codigo} - {item.descripcion}".strip() if item.codigo else item.descripcion
            for item in model.diagnosticos
            if str(item.descripcion or "").strip()
        ]
    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return extract_quirurgico_diagnosticos_from_html(analisis_html)


def procesar_factura(texto: str, client_groq: Groq, client_gemini=None) -> str:
    """Fachada legacy que retorna HTML derivado desde extracción estructurada."""
    try:
        return _procesar_documento_estructurado(
            texto,
            document_type="factura",
            client_groq=client_groq,
            client_gemini=client_gemini,
        )
    except Exception as e:
        return f"Error procesando factura: {str(e)}"


def extraer_factura_json(analisis_html) -> dict:
    """
    Extrae los datos de la factura del HTML y los estructura en formato JSON.
    Retorna un diccionario con todas las secciones de la factura.
    """
    resultado = {
        "nombre_paciente": "",
        "proveedor": {},
        "informacion_factura": {},
        "pagador": {},
        "informacion_paciente": {},
        "lineas_canonicas": [],
        "servicios_procedimientos": {
            "procedimientos_quirurgicos": [],
            "examenes_laboratorio": [],
            "imagenologia": [],
            "hospitalizacion": [],
            "honorarios_medicos": [],
            "medicamentos": [],
            "otros_servicios": [],
        },
        "analisis_financiero": {},
        "observaciones": [],
    }

    if isinstance(analisis_html, dict):
        factura_json = analisis_html.get("factura_json")
        if isinstance(factura_json, dict):
            return {
                "nombre_paciente": str(
                    analisis_html.get("nombre_paciente")
                    or factura_json.get("informacion_paciente", {}).get("nombre_completo")
                    or ""
                ).strip(),
                "proveedor": dict(factura_json.get("proveedor") or {}),
                "informacion_factura": dict(factura_json.get("informacion_factura") or {}),
                "pagador": dict(factura_json.get("pagador") or {}),
                "informacion_paciente": dict(factura_json.get("informacion_paciente") or {}),
                "lineas_canonicas": list(factura_json.get("lineas_canonicas") or []),
                "servicios_procedimientos": {
                    "procedimientos_quirurgicos": list(
                        (factura_json.get("servicios_procedimientos") or {}).get("procedimientos_quirurgicos") or []
                    ),
                    "examenes_laboratorio": list(
                        (factura_json.get("servicios_procedimientos") or {}).get("examenes_laboratorio") or []
                    ),
                    "imagenologia": list((factura_json.get("servicios_procedimientos") or {}).get("imagenologia") or []),
                    "hospitalizacion": list(
                        (factura_json.get("servicios_procedimientos") or {}).get("hospitalizacion") or []
                    ),
                    "honorarios_medicos": list(
                        (factura_json.get("servicios_procedimientos") or {}).get("honorarios_medicos") or []
                    ),
                    "medicamentos": list((factura_json.get("servicios_procedimientos") or {}).get("medicamentos") or []),
                    "otros_servicios": list(
                        (factura_json.get("servicios_procedimientos") or {}).get("otros_servicios") or []
                    ),
                },
                "analisis_financiero": dict(factura_json.get("analisis_financiero") or {}),
                "observaciones": [
                    str(item).strip()
                    for item in (factura_json.get("observaciones") or [])
                    if str(item).strip()
                ],
            }

    model = _doc_structured(analisis_html)
    if isinstance(model, FacturaStructured):
        return {
            "nombre_paciente": model.patient_name,
            "proveedor": model.proveedor.model_dump(exclude_none=True),
            "informacion_factura": model.datos_factura.model_dump(exclude_none=True),
            "pagador": model.pagador.model_dump(exclude_none=True),
            "informacion_paciente": model.paciente.model_dump(exclude_none=True),
            "lineas_canonicas": [line.model_dump(exclude_none=True) for line in model.lineas_canonicas],
            "servicios_procedimientos": model.servicios_procedimientos.model_dump(exclude_none=True),
            "analisis_financiero": model.resumen_financiero.model_dump(
                exclude_none=True,
                exclude={"observaciones_importantes"},
            ),
            "observaciones": [model.resumen_financiero.observaciones_importantes]
            if model.resumen_financiero.observaciones_importantes
            else [],
        }

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return resultado
    return parse_factura_html_to_json(analisis_html)


def extraer_procedimientos_para_soat(analisis_html):
    # Reusar extraer_procedimientos_quirurgicos ya extrae <li>...
    procedimientos = extraer_procedimientos_quirurgicos(analisis_html)
    # Si cada item incluye código CUPS al inicio, removerlo dejando descripción limpia
    limpias = []
    for p in procedimientos:
        # Remueve prefijo tipo "123456 Descripción" si existe
        m = re.match(r"^\d{6}\s+(.+)$", p)
        limpias.append(m.group(1).strip() if m else p)
    return limpias


def extraer_metadatos_historia(analisis_html) -> dict:
    """
    Extrae metadatos estructurados de la historia clínica.
    Retorna un diccionario con todos los campos relevantes.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        document_html = _source_analysis_html(analisis_html)
        raw_text = str(analisis_html.get("descripcion") or "") if isinstance(analisis_html, dict) else ""
        structured_data = {
            "nombre_paciente": model.patient_name,
            "prestador_servicio": model.prestador_servicio,
            "caso": model.numero_caso,
            "datos_identificacion_paciente": model.identificacion_paciente,
            "sexo": model.sexo.upper() if model.sexo else None,
            "edad": model.edad,
            "fecha_ingreso": model.fecha_ingreso,
            "fecha_nacimiento": model.fecha_nacimiento,
            "motivo_consulta": model.motivo_consulta.upper() if model.motivo_consulta else None,
            "resumen": resolve_historia_resumen(
                model.resumen_clinico,
                raw_text=raw_text,
                analisis_html=document_html,
            ),
        }
        return _merge_historia_metadatos_with_fallback(
            structured_data,
            html_data=extract_historia_metadatos_from_html(document_html) if document_html else {},
            raw_text=raw_text,
        )

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return {}
    return extract_historia_metadatos_from_html(analisis_html)


def extraer_nombre_y_resumen_historia(analisis_html):
    """Extrae solo el nombre del paciente y el resumen de la historia clínica."""
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        document_html = _source_analysis_html(analisis_html)
        raw_text = str(analisis_html.get("descripcion") or "") if isinstance(analisis_html, dict) else ""
        return (
            model.patient_name,
            resolve_historia_resumen(
                model.resumen_clinico,
                raw_text=raw_text,
                analisis_html=document_html,
            ),
        )

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return None, None
    return extract_historia_nombre_resumen_from_html(analisis_html)


def extraer_procedimientos_historia(analisis_html):
    """
    Extrae los procedimientos de la historia clínica.
    Busca la sección 'Procedimientos' y retorna lista de procedimientos.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        return [
            f"{item.codigo} {item.descripcion}".strip() if item.codigo else item.descripcion
            for item in model.procedimientos
            if str(item.descripcion or "").strip()
            and not re.search(r"\bincapacidad\b", str(item.descripcion), flags=re.IGNORECASE)
        ]

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return [
        item
        for item in extract_historia_procedimientos_from_html(analisis_html)
        if not re.search(r"\bincapacidad\b", str(item), flags=re.IGNORECASE)
    ]


def extraer_antecedentes_historia_estructurados(analisis_html) -> list[dict[str, Any]]:
    """Extrae y consolida antecedentes categorizados con evidencia y estado clínico."""

    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        initial: list[Any] = list(model.antecedentes_estructurados or [])
        initial.extend(
            {
                "categoria": "quirurgico",
                "descripcion": item.descripcion,
                "estado": "presente",
            }
            for item in model.procedimientos_antecedentes
            if str(item.descripcion or "").strip()
        )
        raw_text = str(analisis_html.get("descripcion") or "") if isinstance(analisis_html, dict) else ""
        raw_structured = extract_historia_antecedentes_structured(raw_text)
        structured = consolidate_antecedentes([*initial, *raw_structured])
        known_keys = {_clean_clinical_key(item.descripcion) for item in structured}
        compatibility_descriptions = list(model.antecedentes or [])
        if not raw_structured:
            compatibility_descriptions.extend(
                _clean_historia_text_list(extract_historia_antecedentes_text(raw_text))
            )
        missing_compatibility = [
            description
            for description in compatibility_descriptions
            if _clean_clinical_key(description) not in known_keys
        ]
        if missing_compatibility:
            structured = consolidate_antecedentes(
                [*structured]
                + [
                    {
                        "categoria": "otro",
                        "descripcion": description,
                        "estado": "presente",
                    }
                    for description in missing_compatibility
                ]
            )
        return [item.model_dump(mode="json", exclude_none=True) for item in structured]

    legacy_items = extract_historia_antecedentes_from_html(_doc_html(analisis_html))
    return [
        {
            "categoria": "otro",
            "descripcion": item,
            "estado": "presente",
            "discordante": False,
        }
        for item in _clean_historia_text_list(legacy_items)
    ]


def extraer_antecedentes_historia(analisis_html) -> list[str]:
    """
    Extrae antecedentes clínicos y procedimientos históricos sin mezclarlos con procedimientos actuales.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        structured = extraer_antecedentes_historia_estructurados(analisis_html)
        flat = antecedentes_to_flat_list(structured)
        if flat:
            return _clean_historia_text_list([*model.antecedentes, *flat])

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return _clean_historia_text_list(extract_historia_antecedentes_from_html(analisis_html))


def extraer_medicamentos_historia(analisis_html):
    """
    Extrae los medicamentos de la historia clínica y los devuelve en formato canónico.
    """
    raw_text = str(analisis_html.get("descripcion") or "") if isinstance(analisis_html, dict) else ""
    deterministic = extract_valle_salud_medications(raw_text)
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        structured_medicamentos = [
            medication_from_legacy(
                {
                    "codigo_referencia": item.codigo or "",
                    "nombre": item.nombre,
                    "dosis": item.dosis or "",
                    "posologia": item.posologia or "",
                    "via": item.via or "",
                    "frecuencia": item.frecuencia or "",
                    "duracion": item.duracion or "",
                    "cantidad": item.cantidad or "",
                    "tipo_uso": item.tipo_uso or "",
                },
                fuente="historia_clinica",
            )
            for item in model.medicamentos
        ]
        structured_medicamentos = [item for item in structured_medicamentos if item]
        if deterministic:
            deterministic_tokens = {
                normalize_medication_key(item.get("nombre")).split(" ")[0]
                for item in deterministic
                if str(item.get("nombre") or "").strip()
            }
            missing_structured = [
                item
                for item in structured_medicamentos
                if normalize_medication_key(item.get("nombre")).split(" ")[0] not in deterministic_tokens
            ]
            return consolidate_medication_items([*deterministic, *missing_structured])
        document_html = _source_analysis_html(analisis_html)
        if structured_medicamentos:
            return consolidate_medication_items(structured_medicamentos)
        legacy_medicamentos = [
            medication_from_legacy(item_texto, fuente="historia_clinica")
            for item_texto in extract_historia_medicamentos_from_html(document_html)
            if item_texto
        ]
        raw_medicamentos = [
            medication_from_legacy(item_texto, fuente="historia_clinica")
            for item_texto in _clean_historia_text_list(extract_historia_medicamentos_text(raw_text))
            if item_texto
        ]
        return consolidate_medication_items(
            item for item in [*raw_medicamentos, *legacy_medicamentos] if item
        )

    if deterministic:
        return deterministic
    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    legacy = [
        medication_from_legacy(item_texto, fuente="historia_clinica")
        for item_texto in extract_historia_medicamentos_from_html(analisis_html)
        if item_texto
    ]
    return consolidate_medication_items(item for item in legacy if item)


def extraer_secciones_quirurgicas(analisis_html):
    """Extrae las secciones 5 (Hallazgos) y 6 (Descripción) del documento quirúrgico."""
    model = _doc_structured(analisis_html)
    if isinstance(model, DocumentoQuirurgicoStructured):
        document_html = _source_analysis_html(analisis_html)
        legacy_hallazgos, legacy_descripcion = (
            extract_quirurgico_sections_from_html(document_html) if document_html else ("", "")
        )
        hallazgos_structured = _clean_clinical_text(model.hallazgos)
        hallazgos_legacy = _clean_clinical_text(legacy_hallazgos)
        descripcion_structured = _clean_clinical_text(
            model.descripcion_procedimiento or model.procedimiento_principal
        )
        descripcion_legacy = _clean_clinical_text(legacy_descripcion)

        hallazgos = hallazgos_structured
        if _is_low_quality_qx_section(hallazgos_structured, min_len=20) and not _is_low_quality_qx_section(
            hallazgos_legacy, min_len=20
        ):
            hallazgos = hallazgos_legacy

        descripcion = descripcion_structured
        if _is_low_quality_qx_section(
            descripcion_structured, min_len=40
        ) and not _is_low_quality_qx_section(descripcion_legacy, min_len=40):
            descripcion = descripcion_legacy
        return hallazgos, descripcion

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return None, None
    return extract_quirurgico_sections_from_html(analisis_html)
