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,
)
_POSTOPERATIVE_TOKEN_PATTERN = re.compile(
    r"(?<![\w-])(?P<term>POP|POSTOP|POST-OP|POSTOPERATORIO|POSOPERATORIO)(?![\w-])",
    re.IGNORECASE,
)
_PELVIC_PROLAPSE_PATTERN = re.compile(
    r"\bprolapso\s+(?:de\s+)?[oó]rganos\s+p[eé]lvicos\b",
    re.IGNORECASE,
)
_PROCEDURE_CUE_PATTERN = re.compile(
    r"\b(?:se\s+realiz[aoó]|se\s+practic[aoó]|realizad[aoó]|practicad[aoó]|procedimiento|"
    r"cirug[ií]a|intervenci[oó]n|operad[aoó]|status\s+post)\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 _source_line(value: str) -> str:
    """Preserve the source wording while only trimming page/line boundaries."""
    return str(value or "").strip()


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]] = []
    line_index = 0
    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,
                        "line_index": line_index,
                        "excerpt": line[:280],
                    }
                )
            line_index += 1
    return items


def _line_records(text: str) -> list[dict[str, Any]]:
    records: list[dict[str, Any]] = []
    block_index = 0
    section = ""
    line_index = 0
    for page, page_text in enumerate(re.split(r"\f", str(text or "")), start=1):
        for raw_line in page_text.splitlines():
            source = _source_line(raw_line)
            normalized = _normalize_ascii(source)
            if not source:
                block_index += 1
                section = ""
                records.append(
                    {
                        "line_index": line_index,
                        "page": page,
                        "source": source,
                        "normalized": normalized,
                        "section": section,
                        "block_index": block_index,
                    }
                )
                line_index += 1
                continue
            if any(token in normalized for token in _PROCEDURE_HEADER_TOKENS):
                section = (
                    "procedimientos_antecedentes"
                    if "antecedent" in normalized
                    else "procedimientos"
                )
                block_index += 1
            elif any(token in normalized for token in _MEDICATION_HEADER_TOKENS):
                section = "medicamentos"
                block_index += 1
            elif re.search(r"\bevoluci[oó]n\b", normalized):
                section = "evolucion"
                block_index += 1
            elif any(token in normalized for token in _SECTION_STOP_TOKENS):
                section = ""
                block_index += 1
            records.append(
                {
                    "line_index": line_index,
                    "page": page,
                    "source": source,
                    "normalized": normalized,
                    "section": section,
                    "block_index": block_index,
                }
            )
            line_index += 1
    return records


def _extract_postoperative_signals(
    records: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    signals: list[dict[str, Any]] = []
    for record in records:
        source = str(record["source"])
        if not source or _PELVIC_PROLAPSE_PATTERN.search(source):
            continue
        for match in _POSTOPERATIVE_TOKEN_PATTERN.finditer(source):
            signals.append(
                {
                    "term": match.group("term"),
                    "meaning": "postoperatorio",
                    "page": int(record["page"]),
                    "line_index": int(record["line_index"]),
                    "excerpt": source[:280],
                    "section": str(record["section"] or ""),
                    "block_index": int(record["block_index"]),
                    "correlation_status": "pendiente_revision",
                    "correlation_reason": "Señal contextual pendiente de procedimiento explícito cercano.",
                }
            )
    return signals


def _extract_procedure_contexts(
    records: list[dict[str, Any]],
    inline_cups: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    contexts: list[dict[str, Any]] = []
    inline_by_line: dict[int, list[dict[str, Any]]] = {}
    for item in inline_cups:
        line_index = int(item["line_index"]) if item.get("line_index") is not None else -1
        inline_by_line.setdefault(line_index, []).append(item)
    for record in records:
        source = str(record["source"])
        if not source or str(record["section"] or "") not in {"procedimientos", "evolucion"}:
            continue
        if any(token in record["normalized"] for token in _PROCEDURE_HEADER_TOKENS):
            continue
        if not (
            record["section"] == "procedimientos"
            or _PROCEDURE_CUE_PATTERN.search(source)
            or _POSTOPERATIVE_TOKEN_PATTERN.search(source)
        ):
            continue
        parsed = inline_by_line.get(int(record["line_index"]), [])
        if parsed:
            for item in parsed:
                contexts.append(
                    {
                        "codigo_cups": str(item.get("codigo_cups") or ""),
                        "description": str(item.get("procedimiento") or ""),
                        "page": int(record["page"]),
                        "line_index": int(record["line_index"]),
                        "excerpt": source[:280],
                        "section": str(record["section"] or ""),
                        "block_index": int(record["block_index"]),
                        "historical": False,
                    }
                )
            continue
        description = source
        pop_match = _POSTOPERATIVE_TOKEN_PATTERN.search(source)
        if pop_match:
            description = re.sub(
                r"^.*?\b(?:de|del|por|tras|posterior\s+a)\b\s*",
                "",
                source[pop_match.end() :],
                count=1,
                flags=re.IGNORECASE,
            ).strip(" :-–—") or source
        contexts.append(
            {
                "codigo_cups": "",
                "description": description[:280],
                "page": int(record["page"]),
                "line_index": int(record["line_index"]),
                "excerpt": source[:280],
                "section": str(record["section"] or ""),
                "block_index": int(record["block_index"]),
                "historical": bool(re.search(r"\bantecedent", record["normalized"])),
            }
        )
    return _unique_inline_items(contexts)


def resolve_postoperative_procedure(
    signal: Mapping[str, Any],
    procedure_contexts: Iterable[Mapping[str, Any]],
) -> dict[str, Any] | None:
    """Resolve one POP signal only against the closest non-historical context."""
    signal_section = str(signal.get("section") or "")
    signal_block = int(signal["block_index"]) if signal.get("block_index") is not None else -1
    signal_line = int(signal["line_index"]) if signal.get("line_index") is not None else -1
    candidates = [
        dict(candidate)
        for candidate in procedure_contexts
        if not bool(candidate.get("historical"))
        and (
            str(candidate.get("section") or "") == signal_section
            or (
                signal_section == "evolucion"
                and (
                    int(candidate["block_index"])
                    if candidate.get("block_index") is not None
                    else -2
                ) == signal_block
            )
        )
    ]
    if not candidates:
        return None
    distances = [
        abs(
            (int(candidate["line_index"]) if candidate.get("line_index") is not None else -1)
            - signal_line
        )
        for candidate in candidates
    ]
    minimum = min(distances)
    closest = [
        candidate
        for candidate, distance in zip(candidates, distances, strict=True)
        if distance == minimum
    ]
    if len(closest) != 1:
        return None
    return closest[0]


@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)
    postoperative_signals: list[dict[str, Any]] = field(default_factory=list)
    procedure_contexts: list[dict[str, Any]] = 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)
        )
        records = _line_records(text)
        for item in inline_cups:
            matching = next(
                (
                    record
                    for record in records
                    if record["source"].startswith(str(item.get("excerpt") or ""))
                ),
                None,
            )
            if matching is not None:
                item["line_index"] = int(matching["line_index"])
                item["section"] = str(matching["section"] or "")
                item["block_index"] = int(matching["block_index"])
        postoperative_signals = _extract_postoperative_signals(records)
        procedure_contexts = _extract_procedure_contexts(records, inline_cups)

        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),
            postoperative_signals=postoperative_signals,
            procedure_contexts=procedure_contexts,
            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,
    )
