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()
    return normalized.replace("del la", "de la")


_FACTURA_PROCEDIMIENTO_INVALID_VALUES = {
    "",
    "[desc]",
    "codigo",
    "codigo cups",
    "descripcion",
    "no especificado",
    "procedimiento",
}


def _clean_cell(value: Any) -> str:
    return _normalize_whitespace(value)


def is_valid_factura_procedimiento(codigo: Any, descripcion: Any) -> bool:
    codigo_normalizado = _normalize_heading(codigo)
    descripcion_normalizada = _normalize_heading(descripcion)
    if not descripcion_normalizada or descripcion_normalizada in _FACTURA_PROCEDIMIENTO_INVALID_VALUES:
        return False
    return not (codigo_normalizado and codigo_normalizado in _FACTURA_PROCEDIMIENTO_INVALID_VALUES)


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


class _FacturaHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.blocks: list[FacturaHTMLBlock] = []
        self._table_depth = 0
        self._list_depth = 0
        self._in_p = False
        self._p_buffer: list[str] = []
        self._in_td = False
        self._td_buffer: list[str] = []
        self._current_row: list[str] = []
        self._current_table: list[list[str]] | None = None
        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 == "table":
            self._table_depth += 1
            if self._table_depth == 1:
                self._current_table = []
        elif normalized == "tr" and self._table_depth:
            self._current_row = []
        elif normalized in {"td", "th"} and self._table_depth:
            self._in_td = True
            self._td_buffer = []
        elif 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._table_depth 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 in {"td", "th"} and self._in_td:
            text = _clean_cell("".join(self._td_buffer))
            self._current_row.append(text)
            self._td_buffer = []
            self._in_td = False
        elif normalized == "tr" and self._table_depth and self._current_table is not None:
            if any(_clean_cell(cell) for cell in self._current_row):
                self._current_table.append([_clean_cell(cell) for cell in self._current_row])
            self._current_row = []
        elif normalized == "table" and self._table_depth:
            self._table_depth -= 1
            if self._table_depth == 0 and self._current_table is not None:
                self.blocks.append(FacturaHTMLBlock("table", self._current_table))
                self._current_table = None
        elif normalized == "li" and self._in_li:
            text = _clean_cell("".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(FacturaHTMLBlock("list", self._current_list))
                self._current_list = None
        elif normalized == "p" and self._in_p:
            text = _clean_cell("".join(self._p_buffer))
            if text:
                self.blocks.append(FacturaHTMLBlock("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_td:
            self._td_buffer.append(text)
        elif self._in_li:
            self._li_buffer.append(text)
        elif self._in_p:
            self._p_buffer.append(text)


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


def _find_next_payload(blocks: list[FacturaHTMLBlock], heading: str, kind: str) -> Any:
    normalized_target = _normalize_heading(heading)
    for index, block in enumerate(blocks):
        if block.kind != "p":
            continue
        if _normalize_heading(block.payload) != normalized_target:
            continue
        for candidate in blocks[index + 1 :]:
            if candidate.kind == kind:
                return candidate.payload
            if candidate.kind == "p":
                break
        return None
    return None


def _find_text_after(blocks: list[FacturaHTMLBlock], heading: str) -> str:
    payload = _find_next_payload(blocks, heading, "p")
    return _clean_cell(payload) if isinstance(payload, str) else ""


def _find_table_after(blocks: list[FacturaHTMLBlock], heading: str) -> list[list[str]]:
    payload = _find_next_payload(blocks, heading, "table")
    return payload if isinstance(payload, list) else []


def _find_list_after(blocks: list[FacturaHTMLBlock], heading: str) -> list[str]:
    payload = _find_next_payload(blocks, heading, "list")
    return payload if isinstance(payload, list) else []


def _table_to_mapping(rows: list[list[str]]) -> dict[str, str]:
    mapping: dict[str, str] = {}
    for row in rows:
        if len(row) < 2:
            continue
        key = _normalize_heading(row[0])
        value = _clean_cell(row[1])
        if key and value:
            mapping[key] = value
    return mapping


def _table_rows_to_dicts(rows: list[list[str]], columns: list[str]) -> list[dict[str, str]]:
    results: list[dict[str, str]] = []
    for row in rows:
        if len(row) < len(columns):
            continue
        trimmed = [_clean_cell(cell) for cell in row[: len(columns)]]
        if not any(trimmed):
            continue
        results.append({key: value for key, value in zip(columns, trimmed, strict=False)})
    return results


def _lookup(mapping: dict[str, str], *labels: str) -> str:
    for label in labels:
        value = mapping.get(_normalize_heading(label), "")
        if value:
            return value
    return ""


def parse_factura_html_to_json(html: str) -> dict[str, Any]:
    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": [],
    }

    blocks = _parse_blocks(html)
    if not blocks:
        return resultado

    resultado["nombre_paciente"] = _find_text_after(blocks, "Nombre del paciente")

    proveedor = _table_to_mapping(_find_table_after(blocks, "1. INFORMACIÓN DEL PROVEEDOR"))
    resultado["proveedor"] = {
        "nombre_institucion": _lookup(proveedor, "Nombre de la institución médica"),
        "nit": _lookup(proveedor, "NIT"),
        "direccion": _lookup(proveedor, "Dirección"),
        "ciudad_departamento": _lookup(proveedor, "Ciudad y departamento"),
    }

    factura = _table_to_mapping(_find_table_after(blocks, "2. INFORMACIÓN DE LA FACTURA"))
    resultado["informacion_factura"] = {
        "numero_factura": _lookup(factura, "Número de factura"),
        "prefijo": _lookup(factura, "Prefijo"),
        "fecha_emision": _lookup(factura, "Fecha de emisión"),
        "numero_caso": _lookup(factura, "Número de caso"),
        "fecha_vencimiento": _lookup(factura, "Fecha de vencimiento"),
    }

    pagador = _table_to_mapping(_find_table_after(blocks, "3. INFORMACIÓN DEL PAGADOR"))
    resultado["pagador"] = {
        "aseguradora_eps": _lookup(pagador, "Aseguradora/EPS"),
        "nit_pagador": _lookup(pagador, "NIT del pagador"),
        "tipo_convenio": _lookup(pagador, "Tipo de convenio"),
        "numero_autorizacion": _lookup(pagador, "Número de autorización"),
    }

    paciente = _table_to_mapping(_find_table_after(blocks, "4. INFORMACIÓN DEL PACIENTE"))
    resultado["informacion_paciente"] = {
        "nombre_completo": _lookup(paciente, "Nombre completo"),
        "numero_identificacion": _lookup(paciente, "Número de identificación (CC)"),
        "fecha_ingreso": _lookup(paciente, "Fecha de ingreso"),
        "fecha_egreso": _lookup(paciente, "Fecha de egreso"),
        "estancia_hospitalaria": _lookup(paciente, "Estancia hospitalaria"),
    }

    resultado["servicios_procedimientos"]["procedimientos_quirurgicos"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Procedimientos quirúrgicos"),
        ["concepto", "codigo_cups", "descripcion", "cantidad", "valor_unitario", "total"],
    )
    resultado["servicios_procedimientos"]["examenes_laboratorio"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Exámenes de laboratorio"),
        ["prueba", "descripcion", "cantidad", "valor_unitario", "total"],
    )
    resultado["servicios_procedimientos"]["imagenologia"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Imagenología") or _find_table_after(blocks, "Imagenologia"),
        ["estudio", "descripcion", "cantidad", "valor_unitario", "total"],
    )
    resultado["servicios_procedimientos"]["hospitalizacion"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Hospitalización") or _find_table_after(blocks, "Hospitalizacion"),
        ["habitacion", "dias", "tarifa", "total"],
    )
    resultado["servicios_procedimientos"]["honorarios_medicos"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Honorarios médicos") or _find_table_after(blocks, "Honorarios medicos"),
        ["rol", "profesional", "cantidad", "valor_unitario", "total"],
    )
    resultado["servicios_procedimientos"]["medicamentos"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Medicamentos"),
        ["medicamento", "dosis", "cantidad", "valor_unitario", "total"],
    )
    resultado["servicios_procedimientos"]["otros_servicios"] = _table_rows_to_dicts(
        _find_table_after(blocks, "Otros servicios"),
        ["codigo_facturacion", "codigo_referencia", "descripcion", "cantidad", "valor_unitario", "total"],
    )

    financiero = _table_to_mapping(_find_table_after(blocks, "6. ANÁLISIS FINANCIERO"))
    resultado["analisis_financiero"] = {
        "total_servicios": _lookup(financiero, "Total de servicios"),
        "descuentos": _lookup(financiero, "Descuentos"),
        "copagos": _lookup(financiero, "Copagos"),
        "valor_total_factura": _lookup(financiero, "Valor total de la factura"),
        "valor_en_letras": _lookup(financiero, "Valor en letras"),
    }
    resultado["observaciones"] = _find_list_after(blocks, "7. OBSERVACIONES IMPORTANTES")
    return resultado


def extract_factura_procedimientos_from_html(html: str) -> list[dict[str, str]]:
    factura_json = parse_factura_html_to_json(html)
    procedimientos = factura_json.get("servicios_procedimientos", {}).get("procedimientos_quirurgicos", [])
    results: list[dict[str, str]] = []
    for item in procedimientos:
        if not isinstance(item, dict):
            continue
        codigo = _normalize_whitespace(item.get("codigo_cups"))
        descripcion = _normalize_whitespace(item.get("descripcion") or item.get("concepto"))
        if not is_valid_factura_procedimiento(codigo, descripcion):
            continue
        results.append(
            {
                "codigo_soat": codigo,
                "descripcion": descripcion,
            }
        )
    return results
