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_heading(value: Any) -> str:
    normalized = _normalize_ascii(_normalize_whitespace(value)).lower()
    normalized = re.sub(r"^\d+\s*[.)-]?\s*", "", normalized)
    normalized = re.sub(r"[^a-z0-9\s]+", " ", normalized)
    return _normalize_whitespace(normalized)


def _normalize_text_for_search(value: Any) -> str:
    normalized = _normalize_ascii(str(value or ""))
    normalized = re.sub(r"[^A-Za-z0-9\s]+", " ", normalized)
    return _normalize_whitespace(normalized).upper()


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


class _QuirurgicoHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.blocks: list[QuirurgicoHTMLBlock] = []
        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(QuirurgicoHTMLBlock("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(QuirurgicoHTMLBlock("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)


_HTML_SECTION_ALIASES: dict[str, tuple[str, ...]] = {
    "diagnosticos": ("3. DIAGNÓSTICOS", "DIAGNÓSTICOS"),
    "procedimientos": ("4. PROCEDIMIENTOS REALIZADOS", "PROCEDIMIENTOS REALIZADOS"),
    "hallazgos": ("5. HALLAZGOS QUIRÚRGICOS", "HALLAZGOS QUIRÚRGICOS"),
    "descripcion": ("6. DESCRIPCIÓN DEL PROCEDIMIENTO", "DESCRIPCIÓN DEL PROCEDIMIENTO"),
}
_HTML_SECTION_HEADINGS = {
    _normalize_heading(alias)
    for aliases in _HTML_SECTION_ALIASES.values()
    for alias in aliases
}

_TEXT_SECTION_ALIASES: dict[str, tuple[str, ...]] = {
    "diagnosticos_prequirurgico": ("DIAGNÓSTICOS PREQUIRÚRGICO", "DIAGNOSTICOS PREQUIRURGICO"),
    "procedimientos_realizados": ("PROCEDIMIENTOS REALIZADOS",),
    "hallazgos_quirurgicos": ("HALLAZGOS QUIRÚRGICOS", "HALLAZGOS QUIRURGICOS"),
    "descripcion_procedimiento": ("DESCRIPCIÓN DEL PROCEDIMIENTO", "DESCRIPCION DEL PROCEDIMIENTO"),
    "justificacion_procedimiento": (
        "JUSTIFICACIÓN DEL PROCEDIMIENTO",
        "JUSTIFICACION DEL PROCEDIMIENTO",
    ),
    "complicaciones_manejo": ("COMPLICACIONES Y SU MANEJO",),
    "diagnosticos_postquirurgico": ("DIAGNÓSTICOS POSTQUIRÚRGICO", "DIAGNOSTICOS POSTQUIRURGICO"),
}
_TEXT_SECTION_ORDER = tuple(_TEXT_SECTION_ALIASES)
_TEXT_SECTION_PATTERNS = {
    key: tuple({_normalize_text_for_search(alias) for alias in aliases})
    for key, aliases in _TEXT_SECTION_ALIASES.items()
}


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


def _match_heading(block: QuirurgicoHTMLBlock, aliases: tuple[str, ...]) -> bool:
    if block.kind != "p":
        return False
    normalized_payload = _normalize_heading(block.payload)
    return normalized_payload in {_normalize_heading(alias) for alias in aliases}


def _collect_section_blocks(blocks: list[QuirurgicoHTMLBlock], aliases: tuple[str, ...]) -> list[QuirurgicoHTMLBlock]:
    for index, block in enumerate(blocks):
        if not _match_heading(block, aliases):
            continue
        collected: list[QuirurgicoHTMLBlock] = []
        for candidate in blocks[index + 1 :]:
            if candidate.kind == "p" and _normalize_heading(candidate.payload) in _HTML_SECTION_HEADINGS:
                break
            collected.append(candidate)
        return collected
    return []


def _extract_list_section(html: str, aliases: tuple[str, ...]) -> list[str]:
    blocks = _parse_blocks(html)
    if not blocks:
        return []
    section_blocks = _collect_section_blocks(blocks, aliases)
    results: list[str] = []
    for block in section_blocks:
        if block.kind == "list":
            results.extend(
                [_normalize_whitespace(item) for item in block.payload if _normalize_whitespace(item)]
            )
    if results:
        return results
    return [
        _normalize_whitespace(block.payload)
        for block in section_blocks
        if block.kind == "p" and _normalize_whitespace(block.payload)
    ]


def _extract_text_section(html: str, aliases: tuple[str, ...]) -> str:
    blocks = _parse_blocks(html)
    if not blocks:
        return ""
    section_blocks = _collect_section_blocks(blocks, aliases)
    paragraphs = [
        _normalize_whitespace(block.payload)
        for block in section_blocks
        if block.kind == "p" and _normalize_whitespace(block.payload)
    ]
    return "\n".join(paragraphs).strip()


def _resolve_text_section_key(section: str) -> str | None:
    normalized_section = _normalize_text_for_search(section)
    for key, patterns in _TEXT_SECTION_PATTERNS.items():
        if normalized_section in patterns:
            return key
    return None


def extract_quirurgico_procedimientos_from_html(html: str) -> list[str]:
    return _extract_list_section(html, _HTML_SECTION_ALIASES["procedimientos"])


def extract_quirurgico_diagnosticos_from_html(html: str) -> list[str]:
    return _extract_list_section(html, _HTML_SECTION_ALIASES["diagnosticos"])


def extract_quirurgico_sections_from_html(html: str) -> tuple[str, str]:
    return (
        _extract_text_section(html, _HTML_SECTION_ALIASES["hallazgos"]),
        _extract_text_section(html, _HTML_SECTION_ALIASES["descripcion"]),
    )


def extract_quirurgico_text_section(documento: str, seccion: str) -> str:
    search_text = _normalize_text_for_search(documento)
    target_key = _resolve_text_section_key(seccion)
    if not search_text or not target_key:
        return ""

    matches: list[tuple[int, int, str]] = []
    for key in _TEXT_SECTION_ORDER:
        for pattern in _TEXT_SECTION_PATTERNS[key]:
            matches.extend(
                (match.start(), match.end(), key)
                for match in re.finditer(rf"(?<![A-Z0-9]){re.escape(pattern)}(?![A-Z0-9])", search_text)
            )

    if not matches:
        return ""

    matches.sort(key=lambda item: (item[0], item[1]))
    for index, (start, end, key) in enumerate(matches):
        if key != target_key:
            continue
        next_start = len(search_text)
        for candidate_start, _candidate_end, _candidate_key in matches[index + 1 :]:
            if candidate_start > start:
                next_start = candidate_start
                break
        return _normalize_whitespace(search_text[end:next_start])
    return ""
