from __future__ import annotations

import logging
import re
from typing import Any

from groq import Groq

from app.config import config
from app.llm import LLMOutputKind, LLMTask
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,
)
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_html_legacy import (
    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_summary import 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"}


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

    def resolve(self, task: LLMTask) -> LLMResolvedRoute:
        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("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
    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


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],
) -> 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)

    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,
        )

    if _is_low_quality_motivo_consulta(merged.get("motivo_consulta")):
        merged["motivo_consulta"] = _prefer_clinical_value(
            html_data.get("motivo_consulta"),
            merged.get("motivo_consulta"),
            min_preferred_len=6,
        )

    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, str]:
    """
    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"

    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_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"

        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 ""

    return {
        "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,
    }


def normalizar_lista_medicamentos(items, fuente: str = "historia_clinica") -> list[dict[str, str]]:
    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, str]) -> str:
    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"))
    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, cantidad, dosis] if parte])


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

    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()
        ]

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return extract_historia_procedimientos_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.
    """
    model = _doc_structured(analisis_html)
    if isinstance(model, HistoriaClinicaStructured):
        structured_medicamentos = [
            normalizar_medicamento_canonico(
                {
                    "codigo_medicamento": item.codigo or "",
                    "medicamento": item.nombre,
                    "dosis": " - ".join(
                        [part for part in [item.dosis, item.via, item.frecuencia] if str(part or "").strip()]
                    ),
                    "cantidad": item.cantidad or "",
                },
                fuente="historia_clinica",
            )
            for item in model.medicamentos
        ]
        document_html = _source_analysis_html(analisis_html)
        legacy_medicamentos = [
            normalizar_medicamento_canonico(item_texto, fuente="historia_clinica")
            for item_texto in extract_historia_medicamentos_from_html(document_html)
            if item_texto
        ]
        return _merge_medicamentos_canonicos(structured_medicamentos, legacy_medicamentos)

    analisis_html = _doc_html(analisis_html)
    if not analisis_html:
        return []
    return _merge_medicamentos_canonicos(
        [
        normalizar_medicamento_canonico(item_texto, fuente="historia_clinica")
        for item_texto in extract_historia_medicamentos_from_html(analisis_html)
        if item_texto
        ]
    )


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)
