from __future__ import annotations

import re
import unicodedata
from collections.abc import Iterable
from difflib import SequenceMatcher
from typing import Any

from app.llm.schemas import AntecedenteCategoria, AntecedenteClinico, AntecedenteEstado


_DATE = r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}"
_TABLE_ROW_PATTERN = re.compile(
    rf"^\s*(?P<date>{_DATE})\s+(?P<label>[A-ZÁÉÍÓÚÑÜ ]{{3,30}}?)\s{{2,}}(?P<value>.+?)\s*$",
    re.IGNORECASE,
)
_SECTION_START_PATTERN = re.compile(
    r"^\s*(?:ANTECEDENTES(?:\s+(?:PERSONALES|PERONSLES|CL[IÍ]NICOS|M[EÉ]DICOS))?|"
    r"FECHA\s+TIPO\s+DESCRIPCI[OÓ]N\s+ANTECEDENTES)\s*:?\s*$",
    re.IGNORECASE,
)
_SECTION_END_PATTERN = re.compile(
    r"^\s*(?:EXAMEN\s+F[IÍ]SICO|REVISI[OÓ]N\s+POR\s+SISTEMAS|HALLAZGOS|DIAGN[OÓ]STICOS|"
    r"AN[AÁ]LISIS(?:\s+Y\s+PLAN)?|PLAN|EVOLUCI[OÓ]N|ORDENES\s+M[EÉ]DICAS|PARACL[IÍ]NICOS)\s*:?.*$",
    re.IGNORECASE,
)
_DENIAL_PATTERN = re.compile(
    r"^(?:NIEGA|NEGATIV[OA]S?|NO\s+(?:CONOCID[OA]S?|REFIERE|APLICA)|SIN\s+ANTECEDENTES?)\.?$",
    re.IGNORECASE,
)
_NOISE_PATTERN = re.compile(r"^[*_.\-–—:/\s]+$")
_LABEL_PATTERN = re.compile(
    r"(?P<label>"
    r"PATOL[OÓ]GICOS?|AL[EÉ]RGICOS?|ALERGIAS?|QUIR[UÚ]RGICOS?|FARMACOL(?:[OÓ])?GICOS?|F[AÁ]RMACOS?|"
    r"T[OÓ]XICOS?|PERSONALES?|FAMILIARES?|SIST[EÉ]MICOS?|GINECO(?:L[OÓ]GICOS?)?|OBST[EÉ]TRICOS?|"
    r"GINECO[- ]?OBST[EÉ]TRICOS?|OTROS?|DIABETES|OBESIDAD|HIPERTENSI[OÓ]N(?:\s+ARTERIA[L]?)?|"
    r"TABAQUISMO|CARDIOPAT[IÍ]A|ASMA|RENAL\s+CR[OÓ]NICO|IVU|ENFERMEDADES\s+INMUNOL[OÓ]GICAS|"
    r"COLAGENESIS|EPOC|ALCOHOL(?:ISMO)?|SPA"
    r")\s*:",
    re.IGNORECASE,
)
_GLOBAL_ALLERGY_PATTERN = re.compile(r"^\s*ALERGIAS?\s*:?(?P<value>.*)$", re.IGNORECASE)
_UNTIMED_ROW_PATTERN = re.compile(
    r"^\s*(?P<label>PATOL[OÓ]GICOS?|AL[EÉ]RGICOS?|QUIR[UÚ]RGICOS?|"
    r"FARMACOL(?:[OÓ])?GICOS?|T[OÓ]XICOS?|PERSONALES?|FAMILIARES?|SIST[EÉ]MICOS?)"
    r"\s*:?\s+(?P<value>\S.+?)\s*$",
    re.IGNORECASE,
)

_CATEGORY_LABELS: tuple[tuple[str, AntecedenteCategoria], ...] = (
    ("alerg", "alergico"),
    ("quirurg", "quirurgico"),
    ("farmac", "farmacologico"),
    ("medicament", "farmacologico"),
    ("toxic", "toxico"),
    ("tabaqu", "toxico"),
    ("alcohol", "toxico"),
    ("spa", "toxico"),
    ("familiar", "familiar"),
    ("personal", "personal"),
    ("sistem", "sistemico"),
    ("gineco", "gineco_obstetrico"),
    ("obstetric", "gineco_obstetrico"),
    ("patolog", "patologico"),
)
_CHECKLIST_PATHOLOGICAL = {
    "diabetes",
    "obesidad",
    "hipertension",
    "hipertension arterial",
    "cardiopatia",
    "asma",
    "renal cronico",
    "ivu",
    "enfermedades inmunologicas",
    "colagenesis",
    "epoc",
}
_GENERIC_DESCRIPTION: dict[AntecedenteCategoria, str] = {
    "alergico": "Alergias",
    "patologico": "Patológicos",
    "quirurgico": "Quirúrgicos",
    "farmacologico": "Farmacológicos",
    "toxico": "Tóxicos",
    "personal": "Personales",
    "familiar": "Familiares",
    "sistemico": "Sistémicos",
    "gineco_obstetrico": "Gineco-obstétricos",
    "otro": "Otros",
}


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


def _clean(value: Any) -> str:
    text = re.sub(r"[*_]+", " ", str(value or ""))
    return re.sub(r"\s+", " ", text).strip(" ,.;:-/\t\r\n")


def _category(label: str) -> AntecedenteCategoria:
    normalized = _ascii(label)
    if normalized in _CHECKLIST_PATHOLOGICAL:
        return "patologico"
    for token, category in _CATEGORY_LABELS:
        if token in normalized:
            return category
    return "otro"


def _is_category_label(label: str) -> bool:
    normalized = _ascii(label)
    return normalized not in _CHECKLIST_PATHOLOGICAL and normalized not in {
        "tabaquismo",
        "alcohol",
        "alcoholismo",
        "spa",
    }


def _state(value: str) -> AntecedenteEstado:
    return "negado" if _DENIAL_PATTERN.fullmatch(_clean(value)) else "presente"


def _description_for_denial(label: str, category: AntecedenteCategoria) -> str:
    return _GENERIC_DESCRIPTION[category] if _is_category_label(label) else _clean(label).capitalize()


def _split_positive_values(value: str, category: AntecedenteCategoria) -> list[str]:
    cleaned = _clean(value)
    if category == "alergico":
        cleaned = re.sub(r"^AL[EÉ]RGIC[OA]\s+A(?:\s+LA)?\s+", "", cleaned, flags=re.IGNORECASE)
    if not cleaned:
        return []
    separator = r"\s*[,;]\s*"
    if category == "alergico":
        separator = r"\s*(?:[,;]|\s+-\s+|\s+Y\s+)\s*"
    if category not in {"alergico", "patologico", "quirurgico", "sistemico", "personal"}:
        return [cleaned]
    return [
        part for part in (_clean(item) for item in re.split(separator, cleaned, flags=re.IGNORECASE)) if part
    ]


def _make_items(
    *,
    label: str,
    value: str,
    date: str | None,
    page: int,
    evidence: str,
) -> list[AntecedenteClinico]:
    category = _category(label)
    state = _state(value)
    if state == "negado":
        descriptions = [_description_for_denial(label, category)]
    else:
        descriptions = _split_positive_values(value, category)
    return [
        AntecedenteClinico(
            categoria=category,
            descripcion=description,
            estado=state,
            fecha=date,
            pagina=page,
            evidencia=_clean(evidence)[:240] or None,
        )
        for description in descriptions
        if description and not _NOISE_PATTERN.fullmatch(description)
    ]


def _parse_labeled_segment(segment: str, *, page: int, date: str | None = None) -> list[AntecedenteClinico]:
    matches = list(_LABEL_PATTERN.finditer(segment))
    items: list[AntecedenteClinico] = []
    for index, match in enumerate(matches):
        value_end = matches[index + 1].start() if index + 1 < len(matches) else len(segment)
        value = _clean(segment[match.end() : value_end])
        if not value:
            continue
        items.extend(
            _make_items(
                label=match.group("label"),
                value=value,
                date=date,
                page=page,
                evidence=segment,
            )
        )
    return items


def _column_segments(line: str) -> list[tuple[int, str]]:
    return [
        (match.start(), match.group().strip())
        for match in re.finditer(r"\S(?:.*?\S)?(?=\s{8,}|$)", line.rstrip())
        if match.group().strip()
    ]


def _column_key(position: int) -> int:
    return 0 if position < 55 else 1


def _append_continuation(item: AntecedenteClinico, continuation: str, evidence: str) -> AntecedenteClinico:
    return item.model_copy(
        update={
            "descripcion": _clean(f"{item.descripcion} {continuation}"),
            "evidencia": _clean(f"{item.evidencia or ''} {evidence}")[:240] or None,
        }
    )


def _parse_section_lines(lines: list[str], *, page: int) -> list[AntecedenteClinico]:
    items: list[AntecedenteClinico] = []
    pending_by_column: dict[int, int] = {}
    for line in lines:
        table_match = _TABLE_ROW_PATTERN.match(line)
        if table_match:
            created = _make_items(
                label=table_match.group("label"),
                value=table_match.group("value"),
                date=table_match.group("date"),
                page=page,
                evidence=line,
            )
            if created:
                items.extend(created)
                pending_by_column[1] = len(items) - 1
            continue

        untimed_match = _UNTIMED_ROW_PATTERN.match(line)
        if untimed_match and len(list(_LABEL_PATTERN.finditer(line))) <= 1:
            created = _make_items(
                label=untimed_match.group("label"),
                value=untimed_match.group("value"),
                date=None,
                page=page,
                evidence=line,
            )
            if created:
                items.extend(created)
                pending_by_column[0] = len(items) - 1
            continue

        segments = _column_segments(line)
        for position, segment in segments:
            if _SECTION_START_PATTERN.match(segment) or not _clean(segment):
                continue
            created = _parse_labeled_segment(segment, page=page)
            column = _column_key(position)
            if created:
                items.extend(created)
                pending_by_column[column] = len(items) - 1
                continue
            if column not in pending_by_column or re.search(r"\b(?:FECHA|TIPO|DESCRIPCI[OÓ]N)\b", segment):
                continue
            continuation = _clean(segment)
            if not continuation or _NOISE_PATTERN.fullmatch(continuation):
                continue
            item_index = pending_by_column[column]
            if items[item_index].estado == "negado" or ":" in continuation:
                continue
            if re.match(r"^(?:ANESTESIA|EXFUMADOR|FUMADOR|SOBRE?PESO)\b", continuation, re.IGNORECASE):
                previous = items[item_index]
                items.append(
                    previous.model_copy(
                        update={
                            "descripcion": continuation,
                            "evidencia": _clean(line)[:240] or None,
                        }
                    )
                )
                pending_by_column[column] = len(items) - 1
                continue
            items[item_index] = _append_continuation(items[item_index], continuation, line)
    return items


def _section_blocks(page_text: str) -> list[list[str]]:
    blocks: list[list[str]] = []
    current: list[str] | None = None
    for line in page_text.splitlines():
        normalized = line.strip()
        if _SECTION_START_PATTERN.match(normalized):
            if current:
                blocks.append(current)
            current = []
            continue
        if current is None:
            continue
        if _SECTION_END_PATTERN.match(normalized):
            if current:
                blocks.append(current)
            current = None
            continue
        current.append(line)
        if len(current) >= 80:
            blocks.append(current)
            current = None
    if current:
        blocks.append(current)
    return blocks


def _similar(left: AntecedenteClinico, right: AntecedenteClinico) -> bool:
    if left.categoria != right.categoria or left.estado != right.estado:
        return False
    left_key = re.sub(r"\btotal\b", "", _ascii(left.descripcion))
    right_key = re.sub(r"\btotal\b", "", _ascii(right.descripcion))
    for source, target in (("amoxacilina", "amoxicilina"), ("tamslosina", "tamsulosina")):
        left_key = left_key.replace(source, target)
        right_key = right_key.replace(source, target)
    left_key = re.sub(r"[^a-z0-9]+", " ", left_key).strip()
    right_key = re.sub(r"[^a-z0-9]+", " ", right_key).strip()
    if left_key == right_key:
        return True
    if min(len(left_key), len(right_key)) >= 10 and (left_key in right_key or right_key in left_key):
        return True
    return SequenceMatcher(None, left_key, right_key).ratio() >= 0.92


def _prefer_detail(left: AntecedenteClinico, right: AntecedenteClinico) -> AntecedenteClinico:
    preferred = right if len(right.descripcion) > len(left.descripcion) else left
    other = left if preferred is right else right
    updates: dict[str, Any] = {}
    if not preferred.fecha and other.fecha:
        updates["fecha"] = other.fecha
    if not preferred.pagina and other.pagina:
        updates["pagina"] = other.pagina
    if len(str(other.evidencia or "")) > len(str(preferred.evidencia or "")):
        updates["evidencia"] = other.evidencia
    return preferred.model_copy(update=updates)


def consolidate_antecedentes(
    items: Iterable[AntecedenteClinico | dict[str, Any]],
) -> list[AntecedenteClinico]:
    consolidated: list[AntecedenteClinico] = []
    for raw in items:
        try:
            item = raw if isinstance(raw, AntecedenteClinico) else AntecedenteClinico.model_validate(raw)
        except (TypeError, ValueError):
            continue
        description = _clean(item.descripcion)
        if item.categoria == "alergico" and item.estado == "presente":
            description = re.sub(
                r"^AL[EÉ]RGIC[OA]\s+A(?:\s+LA)?\s+",
                "",
                description,
                flags=re.IGNORECASE,
            )
        item = item.model_copy(update={"descripcion": description})
        if not item.descripcion:
            continue
        existing_index = next(
            (index for index, candidate in enumerate(consolidated) if _similar(candidate, item)),
            None,
        )
        if existing_index is None:
            consolidated.append(item)
        else:
            consolidated[existing_index] = _prefer_detail(consolidated[existing_index], item)

    for category in {item.categoria for item in consolidated}:
        positives = [
            item for item in consolidated if item.categoria == category and item.estado == "presente"
        ]
        negatives = [item for item in consolidated if item.categoria == category and item.estado == "negado"]
        generic_key = _ascii(_GENERIC_DESCRIPTION[category])
        conflicting_descriptions = (
            {
                _ascii(negative.descripcion)
                for negative in negatives
                if _ascii(negative.descripcion) == generic_key
            }
            if positives
            else set()
        )
        for positive in positives:
            if any(_ascii(negative.descripcion) == _ascii(positive.descripcion) for negative in negatives):
                conflicting_descriptions.add(_ascii(positive.descripcion))
        if not conflicting_descriptions:
            continue
        for index, item in enumerate(consolidated):
            if item.categoria != category:
                continue
            if (
                generic_key in conflicting_descriptions
                or _ascii(item.descripcion) in conflicting_descriptions
            ):
                consolidated[index] = item.model_copy(update={"discordante": True})

    order = {
        "alergico": 0,
        "patologico": 1,
        "quirurgico": 2,
        "farmacologico": 3,
        "toxico": 4,
        "personal": 5,
        "familiar": 6,
        "sistemico": 7,
        "gineco_obstetrico": 8,
        "otro": 9,
    }
    return sorted(
        consolidated,
        key=lambda item: (item.estado == "negado", order[item.categoria], _ascii(item.descripcion)),
    )


def extract_historia_antecedentes_structured(
    raw_text: str,
    *,
    initial: Iterable[AntecedenteClinico | dict[str, Any]] = (),
) -> list[AntecedenteClinico]:
    items: list[AntecedenteClinico | dict[str, Any]] = list(initial)
    for page_number, page_text in enumerate(re.split(r"\f", str(raw_text or "")), start=1):
        for block in _section_blocks(page_text):
            items.extend(_parse_section_lines(block, page=page_number))

        # Las alergias suelen aparecer antes de la tabla formal de antecedentes.
        for line in page_text.splitlines():
            allergy_match = _GLOBAL_ALLERGY_PATTERN.match(line)
            if not allergy_match:
                continue
            value = _clean(allergy_match.group("value"))
            if value:
                items.extend(
                    _make_items(
                        label="ALERGIAS",
                        value=value,
                        date=None,
                        page=page_number,
                        evidence=line,
                    )
                )
    return consolidate_antecedentes(items)


def antecedentes_to_flat_list(items: Iterable[AntecedenteClinico | dict[str, Any]]) -> list[str]:
    return [item.descripcion for item in consolidate_antecedentes(items) if item.estado == "presente"]


def antecedentes_to_prompt_text(items: Iterable[AntecedenteClinico | dict[str, Any]]) -> str:
    lines: list[str] = []
    for item in consolidate_antecedentes(items):
        date = f" | fecha={item.fecha}" if item.fecha else ""
        lines.append(f"{item.categoria} | {item.estado}{date} | {item.descripcion}")
    return "\n".join(lines)


def group_antecedentes_for_view(items: Iterable[AntecedenteClinico | dict[str, Any]]) -> dict[str, Any]:
    consolidated = consolidate_antecedentes(items)
    labels = {
        "alergico": "Alergias",
        "patologico": "Patológicos",
        "quirurgico": "Quirúrgicos",
        "farmacologico": "Farmacológicos",
        "toxico": "Tóxicos",
        "personal": "Personales",
        "familiar": "Familiares",
        "sistemico": "Sistémicos",
        "gineco_obstetrico": "Gineco-obstétricos",
        "otro": "Otros",
    }
    categories: list[dict[str, Any]] = []
    for category in labels:
        category_items = [
            item.model_dump(mode="json", exclude_none=True)
            for item in consolidated
            if item.categoria == category and item.estado == "presente"
        ]
        if category_items:
            categories.append(
                {
                    "key": category,
                    "label": labels[category],
                    "items": category_items,
                    "discordant": any(item["discordante"] for item in category_items),
                }
            )
    negatives = [
        item.model_dump(mode="json", exclude_none=True) for item in consolidated if item.estado == "negado"
    ]
    return {
        "categories": categories,
        "negatives": negatives,
        "negative_category_count": len({item["categoria"] for item in negatives}),
        "has_discordance": any(item.discordante for item in consolidated),
    }
