from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass

from app.services.document_identity_extraction import extract_document_identity
from app.services.historia_html_legacy import extract_historia_resumen_from_html


_SUMMARY_HEADERS = (
    "resumen clinico",
    "resumen clínico",
    "resumen",
    "enfermedad actual",
)
_MOTIVO_HEADERS = ("motivo de consulta", "motivo consulta", "motivo")
_DIAGNOSTICO_HEADERS = ("diagnosticos", "diagnósticos", "diagnostico", "diagnóstico")
_PROCEDIMIENTO_HEADERS = (
    "procedimientos realizados",
    "procedimientos",
    "procedimiento realizado",
    "descripcion del procedimiento",
    "descripción del procedimiento",
    "procedimiento principal",
)
_MEDICAMENTO_HEADERS = (
    "medicamentos administrados",
    "medicamentos formulados",
    "medicamentos",
    "tratamiento farmacologico",
    "tratamiento farmacológico",
)
_ANTECEDENTE_HEADERS = (
    "antecedentes",
    "antecedentes personales",
    "antecedentes clinicos",
    "antecedentes clínicos",
    "antecedentes quirurgicos",
    "antecedentes quirúrgicos",
)
_PRESTADOR_HEADERS = (
    "erp",
    "eps",
    "pagador",
    "prestador",
    "aseguradora",
    "entidad responsable de pago",
    "entidad",
)
_EDAD_HEADERS = ("edad",)
_SEXO_HEADERS = ("sexo", "genero", "género")
_INSTITUTION_PATTERN = re.compile(
    r"\b(fundaci[oó]n|cl[ií]nica|hospital|inversiones\s+m[eé]dicas|centro\s+m[eé]dico|"
    r"instituci[oó]n|e\.?s\.?e\.?|i\.?p\.?s\.?)\b",
    re.IGNORECASE,
)
_INSTITUTION_EXCLUDED_PATTERN = re.compile(
    r"\b(no\.?\s*historia|admisi[oó]n|paciente|historia\s+cl[ií]nica|fecha|tel[eé]fono|"
    r"direcci[oó]n|impreso\s+por|usuario)\b",
    re.IGNORECASE,
)
_NIT_PATTERN = re.compile(r"\bnit\b", re.IGNORECASE)
_MASKED_VALUE_PATTERN = re.compile(r"x(?:[\s.*_-]*x){3,}", re.IGNORECASE)
_IDENTIFICATION_RESIDUE_PATTERN = re.compile(
    r"\bidentificad[oa]\s+(?:con\s+)?(?:c\.?\s*c\.?|c\.?\s*e\.?|t\.?\s*i\.?|"
    r"c[eé]dula(?:\s+de\s+ciudadan[ií]a)?|documento(?:\s+de\s+identidad)?)\b"
    r"\s*[-–—,:#]?\s*",
    re.IGNORECASE,
)
_AGE_VALUE_PATTERN = re.compile(r"\b\d{1,3}\s*(?:a[nñ]os?|mes(?:es)?)\b", re.IGNORECASE)
_SEX_VALUE_PATTERN = re.compile(r"\b(masculino|femenino|hombre|mujer|otro)\b", re.IGNORECASE)
_HEADER_MATRIX_LABEL_PATTERN = re.compile(
    r"no\.?\s+de\s+caso|nombre\s+del\s+paciente|edad|sexo|identificaci[oó]n|doc\.?\s*id\.?",
    re.IGNORECASE,
)
_FECHA_INGRESO_HEADERS = ("fecha de ingreso", "fecha ingreso", "ingreso")
_HISTORIA_DATE_TOKEN = r"(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})"
_HISTORIA_TIME_TOKEN = r"(?:[01]?\d|2[0-3]):[0-5]\d"
_FECHA_NACIMIENTO_PATTERN = re.compile(
    rf"\b(?:fec(?:ha)?\.?\s*(?:de\s*)?nac(?:im(?:iento)?)?\.?|fecha\s+de\s+nacimiento)"
    rf"\s*[:\-]?\s*(?P<date>{_HISTORIA_DATE_TOKEN})",
    re.IGNORECASE,
)
_FECHA_INGRESO_PATTERN = re.compile(
    rf"\b(?:fec(?:ha)?\.?\s*(?:de\s*)?ing(?:reso)?\.?|fecha\s+de\s+admisi[oó]n)"
    rf"\s*[:\-]?\s*(?P<date>{_HISTORIA_DATE_TOKEN})",
    re.IGNORECASE,
)
_HORA_INGRESO_PATTERN = re.compile(
    rf"\b(?:hora\s+(?:de\s+)?ing(?:reso)?\.?|hora)\s*[:\-]?\s*(?P<time>{_HISTORIA_TIME_TOKEN})",
    re.IGNORECASE,
)
_HORA_AFTER_DATE_PATTERN = re.compile(rf"^\s+(?P<time>{_HISTORIA_TIME_TOKEN})")
_IDENTITY_HEADERS = (
    "nombre del paciente",
    "paciente",
    "documento",
    "identificacion",
    "identificación",
    "caso",
    "fecha de ingreso",
    "fecha ingreso",
    "fecha",
    "eps",
    "aseguradora",
    "pagador",
    "convenio",
)
_ALL_HEADERS = (
    *_SUMMARY_HEADERS,
    *_MOTIVO_HEADERS,
    *_DIAGNOSTICO_HEADERS,
    *_PROCEDIMIENTO_HEADERS,
    *_MEDICAMENTO_HEADERS,
    *_ANTECEDENTE_HEADERS,
    *_IDENTITY_HEADERS,
    "hallazgos",
    "conclusion",
    "conclusión",
    "observaciones",
    "antecedentes",
)
_SECTION_SEPARATOR_PATTERN = re.compile(r"[:\-–—]\s*")
_ADMIN_PATTERN = re.compile(
    r"\b(eps|aseguradora|pagador|convenio|poliza|póliza|afiliaci[oó]n|arl|soat)\b",
    flags=re.IGNORECASE,
)
_ADMIN_SUFFIX_PATTERN = re.compile(
    r"\b(s\.?a\.?|sas|ltda|seguros|comerciales|bolivar|bolívar)\b",
    flags=re.IGNORECASE,
)
_CLINICAL_SIGNAL_PATTERN = re.compile(
    r"\b("
    r"paciente|ingresa|ingreso|consulta|presenta|refiere|evolucion|evolución|"
    r"diagnost|hallaz|proced|quirurg|quirúrg|operator|trauma|fract|dolor|"
    r"manejo|tratamiento|medic|hospital|sintoma|síntoma|se realiza|postoperator"
    r")\b",
    flags=re.IGNORECASE,
)
_SUMMARY_POSITIVE_PATTERN = re.compile(
    r"\b("
    r"diagnost|cie10|cups|proced|quirurg|quirúrg|intervenci[oó]n|manejo|tratamiento|"
    r"medic|administr|orden|formula|evoluci[oó]n|ingres|egres|hallaz|laboratorio|"
    r"imagen|radiograf|tac|resonancia|ecograf|dolor|fract|trauma|herida|infecci[oó]n|"
    r"consulta|presenta|refiere|paciente|hospital"
    r")\b",
    flags=re.IGNORECASE,
)
_SUMMARY_STRONG_PATTERN = re.compile(
    r"\b(diagnost|proced|quirurg|tratamiento|manejo|medic|hallaz|fract|trauma|evoluci[oó]n)\b",
    flags=re.IGNORECASE,
)
_SUMMARY_NEGATIVE_PATTERN = re.compile(
    r"\b("
    r"estado civil|solter[oa]|casad[oa]|uni[oó]n libre|direccion|direcci[oó]n|telefono|tel[eé]fono|"
    r"correo|email|eps|erp|pagador|aseguradora|convenio|nit|factura|poliza|p[oó]liza|"
    r"autorizaci[oó]n|ciudad|departamento"
    r")\b",
    flags=re.IGNORECASE,
)
_SENTENCE_SPLIT_PATTERN = re.compile(r"(?<=[.!?;])\s+|\n+")


@dataclass(frozen=True)
class _ScoredSentence:
    index: int
    text: str
    score: int


@dataclass(frozen=True)
class HistoriaSummaryPolicy:
    """Contrato determinista de extensión para resúmenes nuevos de HC."""

    source_words: int
    source_characters: int = 0
    threshold_characters: int = 12000
    target_ratio: float = 0.10
    long_min_words: int = 350
    max_words: int = 700
    version: str = "v1"

    @property
    def is_long(self) -> bool:
        return self.source_characters >= self.threshold_characters or (
            self.source_characters <= 0 and self.source_words >= self.threshold_characters // 5
        )

    @property
    def target_words(self) -> int:
        if not self.is_long:
            return 0
        return max(self.long_min_words, min(self.max_words, round(self.source_words * self.target_ratio)))

    @property
    def acceptable_min_words(self) -> int:
        if not self.is_long:
            return 0
        return max(self.long_min_words, round(self.target_words * 0.85))

    @property
    def acceptable_max_words(self) -> int:
        if not self.is_long:
            return self.max_words
        return min(self.max_words, round(self.target_words * 1.10))


def calculate_historia_summary_policy(
    source_text: str,
    *,
    threshold_characters: int = 12000,
    target_ratio: float = 0.10,
    long_min_words: int = 350,
    max_words: int = 700,
) -> HistoriaSummaryPolicy:
    return HistoriaSummaryPolicy(
        source_words=len(str(source_text or "").split()),
        source_characters=len(str(source_text or "")),
        threshold_characters=max(1, int(threshold_characters)),
        target_ratio=max(0.01, float(target_ratio)),
        long_min_words=max(1, int(long_min_words)),
        max_words=max(1, int(max_words)),
    )


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


def _normalize_ascii(value: str) -> str:
    normalized = unicodedata.normalize("NFKD", str(value or ""))
    return "".join(char for char in normalized if not unicodedata.combining(char)).lower()


def _token_matches_line(normalized_line: str, token: str) -> bool:
    return (
        normalized_line == token
        or normalized_line.startswith(f"{token}:")
        or normalized_line.startswith(f"{token} -")
    )


def _extract_inline_section_value(line: str, token: str) -> str:
    normalized = _normalize_ascii(line)
    position = normalized.find(token)
    if position < 0:
        return ""
    original_suffix = line[position + len(token) :]
    original_suffix = _SECTION_SEPARATOR_PATTERN.sub("", original_suffix, count=1)
    return _normalize_line(original_suffix)


def _is_header_line(normalized_line: str) -> bool:
    if not normalized_line:
        return False
    return any(_token_matches_line(normalized_line, token) for token in _ALL_HEADERS)


def _collect_unique(values: list[str]) -> list[str]:
    result: list[str] = []
    seen: set[str] = set()
    for raw in values:
        value = _normalize_line(raw)
        if not value:
            continue
        key = value.casefold()
        if key in seen:
            continue
        seen.add(key)
        result.append(value)
    return result


def extract_historia_section_text(raw_text: str, headers: tuple[str, ...], *, max_lines: int = 6) -> str:
    lines = [_normalize_line(line) for line in str(raw_text or "").splitlines()]
    collected: list[str] = []
    in_section = False
    for line in lines:
        normalized = _normalize_ascii(line)
        if any(_token_matches_line(normalized, token) for token in headers):
            in_section = True
            for token in headers:
                if _token_matches_line(normalized, token):
                    inline = _extract_inline_section_value(line, token)
                    if inline:
                        collected.append(inline)
                    break
            continue
        if not in_section:
            continue
        if not line:
            break
        if _is_header_line(normalized):
            break
        collected.append(line)
        if len(collected) >= max_lines:
            break
    return "\n".join(_collect_unique(collected))


def extract_historia_motivo_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _MOTIVO_HEADERS, max_lines=3)


def extract_historia_resumen_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _SUMMARY_HEADERS, max_lines=8)


def extract_historia_diagnosticos_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _DIAGNOSTICO_HEADERS, max_lines=8)


def extract_historia_procedimientos_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _PROCEDIMIENTO_HEADERS, max_lines=8)


def extract_historia_medicamentos_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _MEDICAMENTO_HEADERS, max_lines=8)


def extract_historia_antecedentes_text(raw_text: str) -> str:
    return extract_historia_section_text(raw_text, _ANTECEDENTE_HEADERS, max_lines=10)


def extract_historia_labeled_value(raw_text: str, headers: tuple[str, ...], *, max_len: int = 160) -> str:
    for line in [_normalize_line(line) for line in str(raw_text or "").splitlines()]:
        normalized = _normalize_ascii(line)
        for token in headers:
            if not _token_matches_line(normalized, token):
                continue
            value = _extract_inline_section_value(line, token)
            if value and len(value) <= max_len:
                return value
    return ""


def _header_lines(raw_text: str, *, limit: int = 45) -> list[str]:
    return [line.rstrip() for line in str(raw_text or "").splitlines() if line.strip()][:limit]


def _is_institution_candidate(value: str) -> bool:
    normalized = _normalize_line(value)
    if not normalized or len(normalized) > 120:
        return False
    if _INSTITUTION_EXCLUDED_PATTERN.search(normalized) or _MASKED_VALUE_PATTERN.search(normalized):
        return False
    if not _INSTITUTION_PATTERN.search(normalized):
        return False
    digit_count = sum(character.isdigit() for character in normalized)
    return digit_count <= max(3, len(normalized) // 5)


def extract_historia_institution_text(raw_text: str) -> str:
    lines = _header_lines(raw_text)
    nit_indexes = [index for index, line in enumerate(lines) if _NIT_PATTERN.search(line)]
    for nit_index in nit_indexes:
        start = max(0, nit_index - 4)
        candidates = [line for line in lines[start:nit_index] if _is_institution_candidate(line)]
        if candidates:
            return _normalize_line(candidates[-1])
    for line in lines[:20]:
        if _is_institution_candidate(line):
            return _normalize_line(line)
    return ""


def _extract_header_pattern_value(raw_text: str, pattern: re.Pattern[str]) -> str:
    for line in _header_lines(raw_text):
        match = pattern.search(line)
        if match:
            return _normalize_line(match.group(1))
    return ""


def _extract_value_below_matrix_label(
    raw_text: str,
    *,
    label: str,
    value_pattern: re.Pattern[str],
) -> str:
    lines = _header_lines(raw_text)
    for index, line in enumerate(lines[:-1]):
        labels = list(_HEADER_MATRIX_LABEL_PATTERN.finditer(line))
        for label_index, match in enumerate(labels):
            if _normalize_ascii(match.group(0)) != label:
                continue
            end = labels[label_index + 1].start() if label_index + 1 < len(labels) else len(line)
            value_column = lines[index + 1][match.start() : end]
            value_match = value_pattern.search(value_column)
            if value_match:
                return _normalize_line(value_match.group(0))
    return ""


def extract_historia_prestador_text(raw_text: str) -> str:
    institution = extract_historia_institution_text(raw_text)
    if institution:
        return institution
    return extract_historia_labeled_value(raw_text, _PRESTADOR_HEADERS)


def extract_historia_edad_text(raw_text: str) -> str:
    age_pattern = re.compile(
        rf"\bedad\b\s*[:#.-]?\s*({_AGE_VALUE_PATTERN.pattern})",
        re.IGNORECASE,
    )
    return (
        _extract_header_pattern_value(raw_text, age_pattern)
        or _extract_value_below_matrix_label(
            raw_text,
            label="edad",
            value_pattern=_AGE_VALUE_PATTERN,
        )
        or extract_historia_labeled_value(raw_text, _EDAD_HEADERS, max_len=40)
    )


def extract_historia_sexo_text(raw_text: str) -> str:
    labels = "|".join(re.escape(_normalize_ascii(label)) for label in _SEXO_HEADERS)
    sex_pattern = re.compile(
        rf"\b(?:{labels})\b\s*[:#.-]?\s*({_SEX_VALUE_PATTERN.pattern})",
        re.IGNORECASE,
    )
    return _extract_header_pattern_value(raw_text, sex_pattern) or _extract_value_below_matrix_label(
        raw_text,
        label="sexo",
        value_pattern=_SEX_VALUE_PATTERN,
    )


def extract_historia_fecha_nacimiento_text(raw_text: str) -> str:
    for line in str(raw_text or "").splitlines():
        match = _FECHA_NACIMIENTO_PATTERN.search(line)
        if match:
            return match.group("date")
    return ""


def extract_historia_fecha_ingreso_text(raw_text: str) -> str:
    for line in str(raw_text or "").splitlines():
        match = _FECHA_INGRESO_PATTERN.search(line)
        if not match:
            continue
        value = match.group("date")
        time_match = _HORA_INGRESO_PATTERN.search(line, match.end())
        if not time_match:
            time_match = _HORA_AFTER_DATE_PATTERN.search(line[match.end() :])
        if time_match:
            value = f"{value} {time_match.group('time')}"
        return value
    return extract_historia_labeled_value(raw_text, _FECHA_INGRESO_HEADERS, max_len=80)


def is_valid_historia_resumen(text: str) -> bool:
    normalized = _normalize_line(text)
    if not normalized or normalized.casefold() == "no especificado":
        return False
    word_count = len(normalized.split())
    has_clinical_signal = bool(_CLINICAL_SIGNAL_PATTERN.search(normalized))
    looks_administrative = bool(_ADMIN_PATTERN.search(normalized)) or (
        word_count <= 8 and bool(_ADMIN_SUFFIX_PATTERN.search(normalized))
    )
    if looks_administrative and not has_clinical_signal:
        return False
    if word_count <= 1 and not has_clinical_signal:
        return False
    return not (
        word_count <= 10 and normalized.isupper() and looks_administrative and not has_clinical_signal
    )


def _historia_resumen_quality(text: str) -> int:
    normalized = _normalize_line(text)
    if not is_valid_historia_resumen(normalized):
        return -100
    words = normalized.split()
    word_score = min(len(words), 80)
    punctuation_score = 10 if any(mark in normalized for mark in ".;:") else 0
    casing_score = 10 if normalized != normalized.upper() else 0
    clinical_signal_score = min(30, len(_CLINICAL_SIGNAL_PATTERN.findall(normalized)) * 3)
    return word_score + punctuation_score + casing_score + clinical_signal_score


def _split_summary_sentences(*values: str) -> list[str]:
    sentences: list[str] = []
    seen: set[str] = set()
    for value in values:
        for part in _SENTENCE_SPLIT_PATTERN.split(str(value or "")):
            sentence = _normalize_line(part)
            if not sentence:
                continue
            key = _normalize_ascii(sentence)
            if key in seen:
                continue
            seen.add(key)
            sentences.append(sentence)
    return sentences


def sanitize_historia_summary(
    text: str,
    *,
    raw_text: str = "",
    patient_name: str = "",
    patient_id: str = "",
    case_number: str = "",
) -> str:
    """Elimina identidad redundante sin alterar edad, sexo ni contenido clínico."""
    normalized = str(text or "").strip()
    identity = extract_document_identity(raw_text, strategy="historia_clinica")
    values = [
        patient_name,
        patient_id,
        case_number,
        identity.patient_name,
        identity.patient_id,
        identity.case_number,
    ]
    for value in sorted({str(item).strip() for item in values if str(item).strip()}, key=len, reverse=True):
        normalized = re.sub(re.escape(value), "", normalized, flags=re.IGNORECASE)
    # Tras retirar el número, expresiones como "identificado con CC -" no
    # conservan información clínica y dejan un fragmento identificatorio.
    normalized = _IDENTIFICATION_RESIDUE_PATTERN.sub("", normalized)
    normalized = re.sub(
        r"\b(?:nombre\s+del\s+paciente|documento|identificaci[oó]n|n(?:ú|u)mero\s+de\s+caso|caso)\s*[:#-]?\s*",
        "",
        normalized,
        flags=re.IGNORECASE,
    )
    normalized = re.sub(r"[ \t]{2,}", " ", normalized)
    normalized = re.sub(r"\s+([,;:])", r"\1", normalized)
    normalized = re.sub(r" *\n *", "\n", normalized)
    normalized = re.sub(r"\n{3,}", "\n\n", normalized)
    return normalized.strip(" \n,;:")


def _complete_sentence_selection(sentences: list[str], *, policy: HistoriaSummaryPolicy) -> str:
    if not sentences:
        return ""
    total_words = sum(len(item.split()) for item in sentences)
    selected_sentences = sentences[:5] if not policy.is_long else sentences
    if not policy.is_long:
        return "\n\n".join(selected_sentences)
    if policy.acceptable_min_words <= total_words <= policy.max_words:
        selected_sentences = sentences
    selected: list[str] = []
    words_used = 0
    for sentence in sentences:
        sentence_words = len(sentence.split())
        if selected and words_used + sentence_words > policy.max_words:
            break
        if not selected and sentence_words > policy.max_words:
            continue
        selected.append(sentence)
        words_used += sentence_words
    selected_sentences = selected
    if not selected_sentences:
        return ""
    # El consolidado puede traer sus propios párrafos; la selección no debe
    # inventar saltos que alteren el contrato textual de respuestas existentes.
    return " ".join(selected_sentences)


def _score_historia_sentence(sentence: str) -> int:
    """Calcula la ponderación clínica de una frase candidata del resumen.

    La puntuación favorece frases que describen el episodio actual y que aportan
    evidencia útil para auditoría: diagnósticos, procedimientos, tratamientos,
    medicamentos, evolución, hallazgos, ayudas diagnósticas, códigos CIE-10/CUPS
    o contexto temporal del ingreso/egreso. También favorece frases de longitud
    media porque suelen ser más legibles y menos propensas a contener ruido.

    La puntuación penaliza frases administrativas o poco clínicas, por ejemplo
    datos de contacto, pagador, convenio, factura, estado civil, frases demasiado
    cortas, frases excesivamente largas y rótulos en mayúscula que suelen venir
    de encabezados OCR. El valor resultante se usa para ordenar candidatos en
    `build_weighted_historia_summary`; solo frases con puntaje positivo pasan al
    resumen ponderado.

    Args:
        sentence: Frase candidata ya segmentada desde el resumen estructurado,
            el texto fuente o el HTML legado.

    Returns:
        Puntuación entera de relevancia clínica. Valores positivos indican que la
        frase es candidata para el resumen; valores negativos o cero indican baja
        utilidad clínica o alta probabilidad de ruido administrativo.
    """
    normalized = _normalize_line(sentence)
    if not normalized:
        return -100
    words = normalized.split()
    score = 0
    score += min(12, len(_SUMMARY_POSITIVE_PATTERN.findall(normalized)) * 3)
    score += min(12, len(_SUMMARY_STRONG_PATTERN.findall(normalized)) * 4)
    if re.search(r"\b[A-TV-Z]\d{2}[0-9A-Z]?(?:\.[0-9A-Z]{1,2})?\b", normalized, re.IGNORECASE):
        score += 8
    if re.search(r"\b\d{4,6}\b", normalized):
        score += 4
    if 7 <= len(words) <= 32:
        score += 8
    elif len(words) < 4:
        score -= 12
    elif len(words) > 42:
        score -= 8
    if _SUMMARY_NEGATIVE_PATTERN.search(normalized):
        score -= 12
    if normalized.isupper() and len(words) <= 12:
        score -= 6
    return score


def build_weighted_historia_summary(
    *,
    candidate: str | None = None,
    raw_text: str = "",
    analisis_html: str = "",
    min_sentences: int = 3,
    max_sentences: int = 5,
    max_words: int = 700,
    policy: HistoriaSummaryPolicy | None = None,
) -> str:
    raw_summary = extract_historia_resumen_text(raw_text)
    html_summary = extract_historia_resumen_from_html(analisis_html or "")
    source_candidates = [str(candidate or ""), raw_summary, html_summary]
    sentences = _split_summary_sentences(*source_candidates)
    if not sentences:
        sentences = _split_summary_sentences(raw_text)
    scored = [
        _ScoredSentence(index=index, text=sentence, score=_score_historia_sentence(sentence))
        for index, sentence in enumerate(sentences)
    ]
    scored = [item for item in scored if item.score > 0 and is_valid_historia_resumen(item.text)]
    if not scored:
        return ""
    selected = sorted(
        sorted(scored, key=lambda item: item.score, reverse=True)[:max_sentences], key=lambda item: item.index
    )
    if len(selected) < min_sentences:
        selected = sorted(scored[:max_sentences], key=lambda item: item.index)
    return _complete_sentence_selection(
        [item.text for item in selected],
        policy=policy
        or HistoriaSummaryPolicy(source_words=0, max_words=max_words, long_min_words=1),
    )


def resolve_historia_resumen(
    candidate: str | None,
    *,
    raw_text: str = "",
    analisis_html: str = "",
    detailed_candidate: str = "",
    policy: HistoriaSummaryPolicy | None = None,
    patient_name: str = "",
    patient_id: str = "",
    case_number: str = "",
) -> str:
    normalized_candidate = sanitize_historia_summary(
        candidate or "", raw_text=raw_text, patient_name=patient_name, patient_id=patient_id, case_number=case_number
    )
    normalized_detailed = sanitize_historia_summary(
        detailed_candidate,
        raw_text=raw_text,
        patient_name=patient_name,
        patient_id=patient_id,
        case_number=case_number,
    )
    effective_policy = policy or calculate_historia_summary_policy(raw_text)
    long_source = normalized_detailed or normalized_candidate
    if effective_policy.is_long and is_valid_historia_resumen(long_source):
        long_source_words = len(long_source.split())
        if (
            effective_policy.acceptable_min_words <= long_source_words <= effective_policy.acceptable_max_words
            and long_source.rstrip().endswith((".", "!", "?"))
        ):
            return long_source
        detailed_sentences = _split_summary_sentences(long_source)
        resolved = _complete_sentence_selection(detailed_sentences, policy=effective_policy)
        if resolved:
            return resolved
    if is_valid_historia_resumen(normalized_candidate) and (
        not effective_policy.is_long or len(normalized_candidate.split()) <= effective_policy.max_words
    ):
        return normalized_candidate

    weighted = build_weighted_historia_summary(
        candidate=candidate,
        raw_text=raw_text,
        analisis_html=analisis_html,
        policy=effective_policy,
        max_words=effective_policy.max_words,
        max_sentences=50 if effective_policy.is_long else 5,
    )
    if weighted:
        return weighted

    if is_valid_historia_resumen(normalized_candidate):
        return normalized_candidate

    raw_summary = extract_historia_resumen_text(raw_text)
    if is_valid_historia_resumen(raw_summary):
        return sanitize_historia_summary(raw_summary, raw_text=raw_text)

    html_summary = _normalize_line(extract_historia_resumen_from_html(analisis_html or ""))
    if is_valid_historia_resumen(html_summary):
        return sanitize_historia_summary(html_summary, raw_text=raw_text)

    return "No especificado"


def choose_historia_resumen(
    candidate: str | None,
    *,
    detailed_candidate: str = "",
    raw_text: str = "",
    analisis_html: str = "",
    policy: HistoriaSummaryPolicy | None = None,
    patient_name: str = "",
    patient_id: str = "",
    case_number: str = "",
) -> str:
    effective_policy = policy or calculate_historia_summary_policy(raw_text)
    resolved_candidate = resolve_historia_resumen(
        candidate,
        raw_text=raw_text,
        analisis_html=analisis_html,
        detailed_candidate=detailed_candidate,
        policy=effective_policy,
        patient_name=patient_name,
        patient_id=patient_id,
        case_number=case_number,
    )
    return sanitize_historia_summary(
        resolved_candidate,
        raw_text=raw_text,
        patient_name=patient_name,
        patient_id=patient_id,
        case_number=case_number,
    )
