from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass

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


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
            if len(sentence.split()) > 42:
                sentence = " ".join(sentence.split()[:42]).rstrip(" ,;:")
            key = _normalize_ascii(sentence)
            if key in seen:
                continue
            seen.add(key)
            sentences.append(sentence)
    return 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 = 115,
) -> 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)
    words_used = 0
    output: list[str] = []
    for item in selected:
        words = item.text.split()
        if output and words_used + len(words) > max_words:
            continue
        output.append(item.text)
        words_used += len(words)
    return _normalize_line(" ".join(output))


def resolve_historia_resumen(
    candidate: str | None,
    *,
    raw_text: str = "",
    analisis_html: str = "",
) -> str:
    normalized_candidate = _normalize_line(candidate or "")
    if is_valid_historia_resumen(normalized_candidate) and len(normalized_candidate.split()) <= 115:
        return normalized_candidate

    weighted = build_weighted_historia_summary(
        candidate=candidate,
        raw_text=raw_text,
        analisis_html=analisis_html,
    )
    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 _normalize_line(raw_summary)

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

    return "No especificado"


def choose_historia_resumen(
    candidate: str | None,
    *,
    detailed_candidate: str = "",
    raw_text: str = "",
    analisis_html: str = "",
) -> str:
    resolved_candidate = resolve_historia_resumen(
        candidate,
        raw_text=raw_text,
        analisis_html=analisis_html,
    )
    resolved_detailed = resolve_historia_resumen(
        detailed_candidate,
        raw_text=raw_text,
        analisis_html=analisis_html,
    )
    candidate_words = len(_normalize_line(resolved_candidate).split())
    detailed_words = len(_normalize_line(resolved_detailed).split())
    if (
        is_valid_historia_resumen(resolved_detailed)
        and detailed_words >= candidate_words + 12
        and (
            _normalize_line(resolved_candidate) == _normalize_line(resolved_candidate).upper()
            or candidate_words <= 35
        )
    ):
        return _normalize_line(resolved_detailed)
    if (
        is_valid_historia_resumen(resolved_detailed)
        and _historia_resumen_quality(resolved_detailed) >= _historia_resumen_quality(resolved_candidate) + 12
    ):
        return _normalize_line(resolved_detailed)
    return _normalize_line(resolved_candidate)
