from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass, field
from typing import Literal, Protocol

from app.services.patient_name_extraction import (
    extract_historia_clinica_patient_name_from_text,
    extract_patient_name_from_text,
    extract_prefactura_patient_name_from_text,
)


DocumentIdentityStrategy = Literal["general", "historia_clinica", "prefactura"]

_PATIENT_LINE_ID_PATTERN = re.compile(
    r"paciente\s*:\s*(?:cc|ti|ce|dni|nit)?\s*[-:]?\s*(\d{5,12})",
    re.IGNORECASE,
)
_DOCUMENT_NUMBER_LABEL_PATTERNS = (
    re.compile(r"(?:cc|ti|ce|dni)\s*[-:]?\s*(\d{5,12})", re.IGNORECASE),
    re.compile(r"(?:identificaci[oó]n|documento|c[ée]dula)\D{0,12}(\d{5,12})", re.IGNORECASE),
    re.compile(r"(\d{5,12})\s*(?:identificaci[oó]n|documento)\b", re.IGNORECASE),
)
_PLAIN_DIGITS_PATTERN = re.compile(r"\b(\d{5,12})\b")
_PATIENT_ID_LABEL_PATTERN = re.compile(
    r"(?:identificaci[oó]n|tipo y n[°o]|documento|\bcc\b|\bti\b|\bce\b|\bdni\b|c[ée]dula)",
    re.IGNORECASE,
)
_PATIENT_ID_EXCLUDED_CONTEXT_PATTERN = re.compile(
    r"(?:caso|admisi[oó]n|consecutivo|orden|page\s+\d+|fecha|hora|telefonos?|tel[eé]fono|contacto)",
    re.IGNORECASE,
)
_CASE_INLINE_PATTERNS = (
    re.compile(
        r"(?:caso\s*(?:no\.?)?|no\.?\s*de\s*caso|n[°o]\s*caso)\s*[:#-]?\s*([A-Z0-9-]{4,20})",
        re.IGNORECASE,
    ),
    re.compile(
        r"\b([A-Z0-9-]{4,20})\s*(?:no\.?\s*de\s*caso|n[°o]\s*caso|caso\s*(?:no\.?)?|caso)\b",
        re.IGNORECASE,
    ),
)
_PREFACTURA_CASE_PATTERN = re.compile(
    r"(?:caso\s*(?:no\.?)?|no\.?\s*de\s*caso|n[°o]\s*caso|caso)\s*[:#-]?\s*(?:cm\s*-\s*)?(\d{4,12})",
    re.IGNORECASE,
)
_PREFACTURA_PATIENT_ID_PATTERN = re.compile(
    r"(?:identificaci[oó]n|tipo y n[°o]\s+de documento|paciente\s*:\s*(?:cc|ti|ce|dni))\D{0,20}(\d{5,12})",
    re.IGNORECASE,
)


@dataclass(frozen=True)
class DocumentIdentityExtractionResult:
    strategy: DocumentIdentityStrategy
    patient_name: str = ""
    patient_id: str = ""
    case_number: str = ""
    evidence: tuple[str, ...] = field(default_factory=tuple)
    warnings: tuple[str, ...] = field(default_factory=tuple)


class DocumentIdentityExtractor(Protocol):
    strategy: DocumentIdentityStrategy

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult: ...


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 _build_evidence(*pairs: tuple[bool, str]) -> tuple[str, ...]:
    return tuple(label for present, label in pairs if present)


def _extract_case_number(raw_text: str) -> str:
    for pattern in _CASE_INLINE_PATTERNS:
        match = pattern.search(raw_text)
        if match:
            return _normalize_line(match.group(1)).upper()
    return ""


def _extract_patient_id_from_fragment(fragment: str, *, label_context: bool = False) -> str:
    normalized = _normalize_line(fragment)
    if not normalized:
        return ""
    if _PATIENT_ID_EXCLUDED_CONTEXT_PATTERN.search(normalized) and not (
        label_context
        or _PATIENT_LINE_ID_PATTERN.search(normalized)
        or _PATIENT_ID_LABEL_PATTERN.search(normalized)
    ):
        return ""

    for pattern in _DOCUMENT_NUMBER_LABEL_PATTERNS:
        match = pattern.search(normalized)
        if match:
            return _normalize_line(match.group(1))

    if label_context:
        match = _PLAIN_DIGITS_PATTERN.search(normalized)
        if match and not _PATIENT_ID_EXCLUDED_CONTEXT_PATTERN.search(normalized):
            return _normalize_line(match.group(1))
    return ""


def _extract_general_patient_id(raw_text: str) -> str:
    lines = [_normalize_line(line) for line in str(raw_text or "").splitlines() if _normalize_line(line)]
    for index, line in enumerate(lines[:20]):
        patient_line_match = _PATIENT_LINE_ID_PATTERN.search(line)
        if patient_line_match:
            return _normalize_line(patient_line_match.group(1))

        if not _PATIENT_ID_LABEL_PATTERN.search(line):
            continue

        candidate = _extract_patient_id_from_fragment(line)
        if candidate:
            return candidate

        for probe in range(index + 1, min(index + 4, len(lines))):
            candidate = _extract_patient_id_from_fragment(lines[probe], label_context=True)
            if candidate:
                return candidate
    return ""


def _extract_prefactura_patient_id(raw_text: str) -> str:
    match = _PREFACTURA_PATIENT_ID_PATTERN.search(str(raw_text or ""))
    if match:
        return _normalize_line(match.group(1))
    return _extract_general_patient_id(raw_text)


def _extract_prefactura_case_number(raw_text: str) -> str:
    match = _PREFACTURA_CASE_PATTERN.search(str(raw_text or ""))
    if match:
        return _normalize_line(match.group(1)).upper()
    return _extract_case_number(raw_text)


class GeneralDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "general"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        patient_name = extract_patient_name_from_text(raw_text)
        patient_id = _extract_general_patient_id(raw_text)
        case_number = _extract_case_number(raw_text)
        return DocumentIdentityExtractionResult(
            strategy=self.strategy,
            patient_name=patient_name,
            patient_id=patient_id,
            case_number=case_number,
            evidence=_build_evidence(
                (bool(patient_name), "general_patient_name"),
                (bool(patient_id), "general_patient_id"),
                (bool(case_number), "general_case_number"),
            ),
        )


class HistoriaClinicaDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "historia_clinica"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        patient_name = extract_historia_clinica_patient_name_from_text(raw_text)
        patient_id = _extract_general_patient_id(raw_text)
        case_number = _extract_case_number(raw_text)
        return DocumentIdentityExtractionResult(
            strategy=self.strategy,
            patient_name=patient_name,
            patient_id=patient_id,
            case_number=case_number,
            evidence=_build_evidence(
                (bool(patient_name), "historia_clinica_patient_name"),
                (bool(patient_id), "historia_clinica_patient_id"),
                (bool(case_number), "historia_clinica_case_number"),
            ),
        )


class PrefacturaDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "prefactura"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        patient_name = extract_prefactura_patient_name_from_text(raw_text)
        patient_id = _extract_prefactura_patient_id(raw_text)
        case_number = _extract_prefactura_case_number(raw_text)
        return DocumentIdentityExtractionResult(
            strategy=self.strategy,
            patient_name=patient_name,
            patient_id=patient_id,
            case_number=case_number,
            evidence=_build_evidence(
                (bool(patient_name), "prefactura_patient_name"),
                (bool(patient_id), "prefactura_patient_id"),
                (bool(case_number), "prefactura_case_number"),
            ),
        )


_EXTRACTORS: dict[DocumentIdentityStrategy, DocumentIdentityExtractor] = {
    "general": GeneralDocumentIdentityExtractor(),
    "historia_clinica": HistoriaClinicaDocumentIdentityExtractor(),
    "prefactura": PrefacturaDocumentIdentityExtractor(),
}


def resolve_identity_strategy(document_type: str) -> DocumentIdentityStrategy:
    normalized = _normalize_ascii(document_type)
    if normalized == "historia_clinica":
        return "historia_clinica"
    if normalized == "prefactura":
        return "prefactura"
    return "general"


def extract_document_identity(
    raw_text: str,
    *,
    strategy: DocumentIdentityStrategy = "general",
) -> DocumentIdentityExtractionResult:
    return _EXTRACTORS[strategy].extract(str(raw_text or ""))
