from __future__ import annotations

import re
import unicodedata
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field
from typing import Any

from app.services.document_identity_extraction import extract_document_identity, resolve_identity_strategy


_DATE_PATTERN = re.compile(r"\b(?:\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b")
_CIE10_INLINE_PATTERN = re.compile(
    r"^\s*(?P<codigo>[A-TV-Z]\d{2}[0-9A-Z]?(?:\.[0-9A-Z]{1,2})?)\s*(?:[-:–—]\s*|\s+)(?P<descripcion>.+)$",
    re.IGNORECASE,
)
_CUPS_INLINE_PATTERN = re.compile(
    r"^\s*(?P<codigo>\d{6})\s*(?:[-:–—]\s*|\s+)(?P<descripcion>.+)$",
    re.IGNORECASE,
)
_PROCEDURE_HEADER_TOKENS = (
    "procedimientos realizados",
    "procedimientos",
    "procedimiento realizado",
    "descripcion del procedimiento",
    "descripción del procedimiento",
    "procedimiento principal",
)
_MEDICATION_HEADER_TOKENS = (
    "medicamentos administrados",
    "medicamentos formulados",
    "medicamentos",
    "tratamiento farmacologico",
    "tratamiento farmacológico",
)
_SECTION_STOP_TOKENS = (
    "diagnostico",
    "diagnóstico",
    "hallazgo",
    "hallazgos",
    "interpretacion",
    "interpretación",
    "resumen",
    "analisis",
    "análisis",
    "observaciones",
    "fecha",
)
_MEDICATION_DETAIL_PATTERN = re.compile(
    r"\b(?:mg|mcg|g|ml|ampolla|ampollas|tableta|tabletas|capsula|cápsula|vo|iv|im|sc)\b",
    re.IGNORECASE,
)


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 _unique_texts(values: list[str]) -> list[str]:
    seen: set[str] = set()
    items: list[str] = []
    for raw in values:
        value = _normalize_line(raw)
        key = value.casefold()
        if not value or key in seen:
            continue
        seen.add(key)
        items.append(value)
    return items


def parse_inline_cie10_entry(text: str) -> dict[str, str] | None:
    match = _CIE10_INLINE_PATTERN.match(_normalize_line(text))
    if not match:
        return None
    return {
        "codigo": match.group("codigo").upper(),
        "descripcion": _normalize_line(match.group("descripcion")),
    }


def parse_inline_cups_entry(text: str) -> dict[str, str] | None:
    match = _CUPS_INLINE_PATTERN.match(_normalize_line(text))
    if not match:
        return None
    return {
        "codigo_cups": match.group("codigo"),
        "procedimiento": _normalize_line(match.group("descripcion")),
    }


def _compact_text(raw_text: str) -> str:
    lines = [_normalize_line(line) for line in str(raw_text or "").splitlines()]
    return "\n".join(_unique_texts([line for line in lines if line]))


def _collect_section_lines(lines: list[str], header_tokens: tuple[str, ...]) -> list[str]:
    collected: list[str] = []
    in_section = False
    for line in lines:
        normalized = _normalize_ascii(line)
        if any(token in normalized for token in header_tokens):
            in_section = True
            continue
        if not in_section:
            continue
        if any(token in normalized for token in _SECTION_STOP_TOKENS):
            in_section = False
            continue
        if not line:
            in_section = False
            continue
        collected.append(line)
    return _unique_texts(collected)


def _extract_medication_candidates(lines: list[str]) -> list[str]:
    explicit = _collect_section_lines(lines, _MEDICATION_HEADER_TOKENS)
    if explicit:
        return explicit
    return _unique_texts([line for line in lines if _MEDICATION_DETAIL_PATTERN.search(line)])


def _extract_procedure_candidates(lines: list[str]) -> list[str]:
    explicit = _collect_section_lines(lines, _PROCEDURE_HEADER_TOKENS)
    inline = [line for line in lines if parse_inline_cups_entry(line)]
    return _unique_texts(explicit + inline)


def _inline_items_with_page(
    text: str,
    parser,
) -> list[dict[str, str | int]]:
    items: list[dict[str, str | int]] = []
    for page_number, page_text in enumerate(re.split(r"\f", str(text or "")), start=1):
        for raw_line in page_text.splitlines():
            line = _normalize_line(raw_line)
            parsed = parser(line)
            if parsed:
                items.append({**parsed, "page": page_number, "excerpt": line[:280]})
    return items


@dataclass(frozen=True)
class DeterministicSignalSnapshot:
    document_type: str
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    dates: list[str] = field(default_factory=list)
    inline_cie10: list[dict[str, Any]] = field(default_factory=list)
    inline_cups: list[dict[str, Any]] = field(default_factory=list)
    medications: list[str] = field(default_factory=list)
    procedures: list[str] = field(default_factory=list)
    compacted_text: str = ""
    should_skip_historia_summary: bool = False

    @property
    def has_inline_cie10(self) -> bool:
        return bool(self.inline_cie10)

    @property
    def has_inline_cups(self) -> bool:
        return bool(self.inline_cups)

    @property
    def has_structured_enough_metadata(self) -> bool:
        return bool(self.patient_name and (self.patient_id or self.case_number or self.dates))


class DeterministicSignalExtractor:
    def extract(
        self,
        raw_text: str,
        document_type: str,
        *,
        summary_threshold: int | None = None,
        max_chars: int | None = None,
    ) -> DeterministicSignalSnapshot:
        text = str(raw_text or "")
        identity = extract_document_identity(text, strategy=resolve_identity_strategy(document_type))
        compacted_text = _compact_text(text)
        lines = [line for line in compacted_text.splitlines() if line]
        inline_cie10 = _unique_inline_items(
            _inline_items_with_page(text, parse_inline_cie10_entry)
        )
        inline_cups = _unique_inline_items(
            _inline_items_with_page(text, parse_inline_cups_entry)
        )

        skip_historia_summary = False
        if (
            str(document_type or "").strip() == "historia_clinica"
            and summary_threshold
            and len(text) > int(summary_threshold)
            and max_chars
            and len(compacted_text) <= int(max_chars)
        ):
            skip_historia_summary = True

        return DeterministicSignalSnapshot(
            document_type=str(document_type or "").strip() or "generico",
            patient_name=identity.patient_name,
            patient_id=identity.patient_id,
            case_number=identity.case_number,
            dates=_unique_texts(_DATE_PATTERN.findall(text)),
            inline_cie10=inline_cie10,
            inline_cups=inline_cups,
            medications=_extract_medication_candidates(lines),
            procedures=_extract_procedure_candidates(lines),
            compacted_text=compacted_text,
            should_skip_historia_summary=skip_historia_summary,
        )


def _unique_inline_items(
    items: Iterable[Mapping[str, Any] | None],
) -> list[dict[str, Any]]:
    seen: set[tuple[tuple[str, str], ...]] = set()
    unique_items: list[dict[str, Any]] = []
    for item in items:
        if not item:
            continue
        normalized = tuple(sorted((key, _normalize_line(value)) for key, value in item.items()))
        if normalized in seen:
            continue
        seen.add(normalized)
        unique_items.append(dict(item))
    return unique_items


def extract_deterministic_signals(
    raw_text: str,
    document_type: str,
    *,
    summary_threshold: int | None = None,
    max_chars: int | None = None,
) -> DeterministicSignalSnapshot:
    return DeterministicSignalExtractor().extract(
        raw_text,
        document_type,
        summary_threshold=summary_threshold,
        max_chars=max_chars,
    )
