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", "factura"]
RedactedIdentityField = Literal["patient_id", "patient_name"]

_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_EXPLICIT_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,
    ),
)
_ADMISSION_CASE_PATTERN = re.compile(
    r"admisi[oó]n\s*[:#-]?\s*(\d{4,12})",
    re.IGNORECASE,
)
_HISTORIA_CASE_PATTERN = re.compile(
    r"no\.?\s*h\.?\s*c\.?\s*(?:cc|ti|ce|dni)?\s*[-:]?\s*"
    r"(?:x(?:[\s.*_-]*x){3,}|\d{5,12})\s*-\s*(\d{4,12})",
    re.IGNORECASE,
)
_MASKED_PATIENT_ID_PATTERNS = (
    re.compile(r"\b(?:cc|ti|ce|dni)\s*[-:]?\s*x(?:[\s.*_-]*x){3,}", re.IGNORECASE),
    re.compile(
        r"(?:doc\.?\s*id\.?|identificaci[oó]n|documento|c[ée]dula)"
        r"\s*[:#-]?\s*(?:cc|ti|ce|dni)?\s*[-:]?\s*x(?:[\s.*_-]*x){3,}",
        re.IGNORECASE,
    ),
)
_MASKED_PATIENT_NAME_PATTERN = re.compile(
    r"(?:nombre(?:\s+del)?\s+paciente|paciente)\s*[:#-]?\s*"
    r"x(?:[\s.*_-]*x){3,}",
    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 = ""
    redacted_identity_fields: tuple[RedactedIdentityField, ...] = field(default_factory=tuple)
    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 _is_valid_case_number(value: str) -> bool:
    normalized = _normalize_line(value).upper()
    return 4 <= len(normalized) <= 20 and bool(re.search(r"\d", normalized))


def _extract_explicit_case_number(raw_text: str) -> str:
    for pattern in _CASE_EXPLICIT_PATTERNS:
        for match in pattern.finditer(raw_text):
            candidate = _normalize_line(match.group(1)).upper()
            if _is_valid_case_number(candidate):
                return candidate
    return ""


def _extract_case_number(raw_text: str) -> str:
    return _extract_explicit_case_number(raw_text)


def _extract_historia_case_number(raw_text: str) -> str:
    explicit = _extract_explicit_case_number(raw_text)
    if explicit:
        return explicit

    header_lines = [
        _normalize_line(line)
        for line in str(raw_text or "").splitlines()
        if _normalize_line(line)
    ][:30]
    header_text = "\n".join(header_lines)
    for pattern in (_ADMISSION_CASE_PATTERN, _HISTORIA_CASE_PATTERN):
        match = pattern.search(header_text)
        if match and _is_valid_case_number(match.group(1)):
            return _normalize_line(match.group(1)).upper()
    return ""


def _extract_redacted_identity_fields(raw_text: str) -> tuple[RedactedIdentityField, ...]:
    text = str(raw_text or "")
    redacted: list[RedactedIdentityField] = []
    if any(pattern.search(text) for pattern in _MASKED_PATIENT_ID_PATTERNS):
        redacted.append("patient_id")
    if _MASKED_PATIENT_NAME_PATTERN.search(text):
        redacted.append("patient_name")
    return tuple(redacted)


def _identity_evidence(
    *,
    patient_name: str,
    patient_id: str,
    case_number: str,
    redacted_fields: tuple[RedactedIdentityField, ...],
    prefix: str,
) -> tuple[str, ...]:
    return _build_evidence(
        (bool(patient_name), f"{prefix}_patient_name"),
        (bool(patient_id), f"{prefix}_patient_id"),
        (bool(case_number), f"{prefix}_case_number"),
        ("patient_id" in redacted_fields, "identificacion_censurada"),
        ("patient_name" in redacted_fields, "nombre_paciente_censurado"),
    )


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:
        redacted_fields = _extract_redacted_identity_fields(raw_text)
        patient_name = "" if "patient_name" in redacted_fields else extract_patient_name_from_text(raw_text)
        patient_id = "" if "patient_id" in redacted_fields else _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,
            redacted_identity_fields=redacted_fields,
            evidence=_identity_evidence(
                patient_name=patient_name,
                patient_id=patient_id,
                case_number=case_number,
                redacted_fields=redacted_fields,
                prefix="general",
            ),
        )


class HistoriaClinicaDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "historia_clinica"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        redacted_fields = _extract_redacted_identity_fields(raw_text)
        patient_name = (
            ""
            if "patient_name" in redacted_fields
            else extract_historia_clinica_patient_name_from_text(raw_text)
        )
        patient_id = "" if "patient_id" in redacted_fields else _extract_general_patient_id(raw_text)
        case_number = _extract_historia_case_number(raw_text)
        return DocumentIdentityExtractionResult(
            strategy=self.strategy,
            patient_name=patient_name,
            patient_id=patient_id,
            case_number=case_number,
            redacted_identity_fields=redacted_fields,
            evidence=_identity_evidence(
                patient_name=patient_name,
                patient_id=patient_id,
                case_number=case_number,
                redacted_fields=redacted_fields,
                prefix="historia_clinica",
            ),
        )


class PrefacturaDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "prefactura"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        redacted_fields = _extract_redacted_identity_fields(raw_text)
        patient_name = (
            "" if "patient_name" in redacted_fields else extract_prefactura_patient_name_from_text(raw_text)
        )
        patient_id = "" if "patient_id" in redacted_fields else _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,
            redacted_identity_fields=redacted_fields,
            evidence=_identity_evidence(
                patient_name=patient_name,
                patient_id=patient_id,
                case_number=case_number,
                redacted_fields=redacted_fields,
                prefix="prefactura",
            ),
        )


class FacturaDocumentIdentityExtractor:
    strategy: DocumentIdentityStrategy = "factura"

    def extract(self, raw_text: str) -> DocumentIdentityExtractionResult:
        from app.services.factura_deterministic import analyze_factura_text

        analysis = analyze_factura_text(raw_text)
        redacted_fields = _extract_redacted_identity_fields(raw_text)
        patient_name = str(
            analysis.informacion_paciente.get("nombre_completo")
            or analysis.nombre_paciente
            or ""
        ).strip()
        patient_id = str(analysis.informacion_paciente.get("numero_identificacion") or "").strip()
        if "patient_name" in redacted_fields:
            patient_name = ""
        if "patient_id" in redacted_fields:
            patient_id = ""
        case_number = str(analysis.informacion_factura.get("numero_caso") or "").strip()
        warnings: list[str] = []
        if "identity" in analysis.low_confidence_sections:
            warnings.append("factura_identity_low_confidence")
        return DocumentIdentityExtractionResult(
            strategy=self.strategy,
            patient_name=patient_name,
            patient_id=patient_id,
            case_number=case_number,
            redacted_identity_fields=redacted_fields,
            evidence=_identity_evidence(
                patient_name=patient_name,
                patient_id=patient_id,
                case_number=case_number,
                redacted_fields=redacted_fields,
                prefix="factura",
            ),
            warnings=tuple(warnings),
        )


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


def resolve_identity_strategy(document_type: str) -> DocumentIdentityStrategy:
    normalized = _normalize_ascii(document_type)
    if normalized == "historia_clinica":
        return "historia_clinica"
    if normalized == "factura":
        return "factura"
    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 ""))
