from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass
from html.parser import HTMLParser
from typing import Any


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


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


def _normalize_label(value: Any) -> str:
    normalized = _normalize_ascii(_normalize_whitespace(value)).lower()
    normalized = re.sub(r"[^a-z0-9\s]+", " ", normalized)
    return _normalize_whitespace(normalized)


@dataclass(frozen=True)
class HistoriaHTMLBlock:
    kind: str
    payload: Any


class _HistoriaHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.blocks: list[HistoriaHTMLBlock] = []
        self._list_depth = 0
        self._in_p = False
        self._p_buffer: list[str] = []
        self._in_li = False
        self._li_buffer: list[str] = []
        self._current_list: list[str] | None = None

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        normalized = tag.lower()
        if normalized in {"ul", "ol"}:
            self._list_depth += 1
            if self._list_depth == 1:
                self._current_list = []
        elif normalized == "li" and self._list_depth:
            self._in_li = True
            self._li_buffer = []
        elif normalized == "p" and not self._list_depth:
            self._in_p = True
            self._p_buffer = []
        elif normalized == "br":
            self._append_text(" ")

    def handle_endtag(self, tag: str) -> None:
        normalized = tag.lower()
        if normalized == "li" and self._in_li:
            text = _normalize_whitespace("".join(self._li_buffer))
            if text and self._current_list is not None:
                self._current_list.append(text)
            self._li_buffer = []
            self._in_li = False
        elif normalized in {"ul", "ol"} and self._list_depth:
            self._list_depth -= 1
            if self._list_depth == 0 and self._current_list is not None:
                self.blocks.append(HistoriaHTMLBlock("list", self._current_list))
                self._current_list = None
        elif normalized == "p" and self._in_p:
            text = _normalize_whitespace("".join(self._p_buffer))
            if text:
                self.blocks.append(HistoriaHTMLBlock("p", text))
            self._p_buffer = []
            self._in_p = False

    def handle_data(self, data: str) -> None:
        self._append_text(data)

    def _append_text(self, text: str) -> None:
        if self._in_li:
            self._li_buffer.append(text)
        elif self._in_p:
            self._p_buffer.append(text)


def _parse_blocks(html: str) -> list[HistoriaHTMLBlock]:
    parser = _HistoriaHTMLParser()
    parser.feed(str(html or ""))
    parser.close()
    return parser.blocks


def _find_next_paragraph(blocks: list[HistoriaHTMLBlock], labels: tuple[str, ...]) -> str:
    normalized_labels = {_normalize_label(label) for label in labels}
    for index, block in enumerate(blocks):
        if block.kind != "p":
            continue
        if _normalize_label(block.payload) not in normalized_labels:
            continue
        for candidate in blocks[index + 1 :]:
            if candidate.kind == "p":
                return _normalize_whitespace(candidate.payload)
            if candidate.kind == "list":
                break
        return ""
    return ""


def _find_list_after(blocks: list[HistoriaHTMLBlock], labels: tuple[str, ...]) -> list[str]:
    normalized_labels = {_normalize_label(label) for label in labels}
    for index, block in enumerate(blocks):
        if block.kind != "p":
            continue
        if _normalize_label(block.payload) not in normalized_labels:
            continue
        for candidate in blocks[index + 1 :]:
            if candidate.kind == "list":
                return [_normalize_whitespace(item) for item in candidate.payload if _normalize_whitespace(item)]
            if candidate.kind == "p":
                break
        return []
    return []


def _find_inline_value(blocks: list[HistoriaHTMLBlock], labels: tuple[str, ...]) -> str:
    for block in blocks:
        texts = block.payload if block.kind == "list" else [block.payload]
        for text in texts:
            raw = _normalize_whitespace(text)
            if not raw:
                continue
            for label in labels:
                match = re.match(rf"^\s*{re.escape(label)}\s*[:\s-]+\s*(.+?)\s*$", raw, re.IGNORECASE)
                if match:
                    value = _normalize_whitespace(match.group(1))
                    if value and value.lower() not in {"no especificado", "n/a", "na", "---"}:
                        return value
    return ""


def _find_field_value(blocks: list[HistoriaHTMLBlock], *labels: str) -> str:
    next_value = _find_next_paragraph(blocks, tuple(labels))
    if next_value and next_value.lower() not in {"no especificado", "n/a", "na", "---"}:
        return next_value
    return _find_inline_value(blocks, tuple(labels))


def extract_historia_metadatos_from_html(html: str) -> dict[str, str]:
    blocks = _parse_blocks(html)
    if not blocks:
        return {}

    metadatos: dict[str, str] = {}
    nombre = _find_field_value(blocks, "Nombre del paciente")
    if nombre:
        metadatos["nombre_paciente"] = nombre

    prestador = _find_field_value(
        blocks,
        "Prestador de servicio",
        "Prestador",
        "Institución médica",
        "Institución",
        "Centro médico",
    )
    if prestador:
        metadatos["prestador_servicio"] = prestador

    caso = _find_field_value(blocks, "Número de caso", "No. de Caso", "No. de caso", "Caso", "N° de caso")
    if caso:
        metadatos["caso"] = caso

    identificacion = _find_field_value(
        blocks,
        "Datos de identificación del paciente",
        "Tipo y número de documento",
        "Documento de identidad",
        "Identificación",
        "CC",
        "Cédula",
    )
    if identificacion:
        metadatos["datos_identificacion_paciente"] = identificacion

    sexo = _find_field_value(blocks, "Sexo", "Género")
    if sexo:
        metadatos["sexo"] = sexo.upper()

    edad = _find_field_value(blocks, "Edad")
    if edad:
        metadatos["edad"] = edad

    fecha_ingreso = _find_field_value(blocks, "Fecha de ingreso", "Fecha ingreso", "Fecha de admisión")
    if fecha_ingreso:
        metadatos["fecha_ingreso"] = fecha_ingreso

    fecha_nacimiento = _find_field_value(blocks, "Fecha de nacimiento", "Fecha nacimiento", "F. nacimiento")
    if fecha_nacimiento:
        metadatos["fecha_nacimiento"] = fecha_nacimiento

    motivo = _find_field_value(blocks, "Motivo de consulta", "Motivo consulta", "Motivo")
    if motivo:
        metadatos["motivo_consulta"] = motivo.upper()

    resumen = extract_historia_resumen_from_html(html)
    if resumen and resumen.lower() != "no especificado":
        metadatos["resumen"] = resumen
    return metadatos


def extract_historia_nombre_resumen_from_html(html: str) -> tuple[str | None, str | None]:
    blocks = _parse_blocks(html)
    if not blocks:
        return None, None
    nombre = _find_field_value(blocks, "Nombre del paciente") or None
    resumen = extract_historia_resumen_from_html(html) or None
    return nombre, resumen


def extract_historia_resumen_from_html(html: str) -> str:
    blocks = _parse_blocks(html)
    if not blocks:
        return ""
    return _find_next_paragraph(blocks, ("Resumen",))


def extract_historia_procedimientos_from_html(html: str) -> list[str]:
    blocks = _parse_blocks(html)
    if not blocks:
        return []
    return _find_list_after(blocks, ("Procedimientos",))


def extract_historia_medicamentos_from_html(html: str) -> list[str]:
    blocks = _parse_blocks(html)
    if not blocks:
        return []
    return _find_list_after(
        blocks,
        (
            "Medicamentos administrados",
            "Medicamentos",
            "Plan farmacológico",
        ),
    )
