"""Shared and specialized patient-name extractors.

`extract_patient_name_from_text()` is the stable general-purpose extractor used
across the application. Feature-specific heuristics must not keep mutating this
function, because it is shared by unrelated flows.

When a document family needs a different reading strategy, expose a new public
extractor for that context and let the caller choose it explicitly.
"""

from __future__ import annotations

import re


_INVALID_NAME_PHRASES = {
    "telefono",
    "teléfono",
    "fecha egreso",
    "tipo y n",
    "masculino",
    "femenino",
    "paciente",
    "usuario",
}
_INVALID_NAME_TOKENS = {
    "telefono",
    "teléfono",
    "fecha",
    "egreso",
    "tipo",
    "paciente",
    "usuario",
    "masculino",
    "femenino",
    "sexo",
    "identificacion",
    "identificación",
    "traido",
    "traído",
    "personal",
    "paramedico",
    "paramédico",
    "ingresa",
    "ingreso",
    "refiere",
    "presenta",
    "acompanado",
    "acompañado",
    "ambulancia",
    "triage",
    "urgencias",
    "consulta",
    "dolor",
    "trauma",
}
_TRAILING_LABEL_TOKENS = {
    "no",
    "caso",
    "edad",
    "sexo",
    "identificacion",
    "identificación",
}
_NAME_JOINERS = {"de", "del", "la", "las", "los", "y"}
_ADMIN_LABEL_SPLIT_PATTERN = re.compile(
    r"(?:hora|fecha|identificaci[oó]n|sexo|edad|servicio|tipo y n[°o]|telefonos?|tel[eé]fono|convenio|admisi[oó]n|consecutivo|page\s+\d+|diagn[oó]stico|evoluci[oó]n)",
    re.IGNORECASE,
)
_PREFAC_COMPANY_TOKENS = {
    "inversiones",
    "medicas",
    "médicas",
    "salud",
    "hospital",
    "clinica",
    "clínica",
    "ips",
    "sas",
}
_TEXT_PATTERNS = (
    re.compile(
        r"paciente\s*:\s*(?:cc|ti|ce|dni|nit)?\s*[-:]?\s*\d{5,12}\s*-\s*([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80})",
        re.IGNORECASE,
    ),
    re.compile(
        r"(?:nombre(?:\s+del)?\s+paciente|nombre\s+paciente)\s*[:#-]?\s*([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80})",
        re.IGNORECASE,
    ),
    re.compile(
        r"([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80})\s*Nombre(?:\s+del)?\s+Paciente",
        re.IGNORECASE,
    ),
    re.compile(
        r"paciente\s*:\s*([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80})",
        re.IGNORECASE,
    ),
)
_NAME_ONLY_LINE_PATTERN = re.compile(r"^[A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80}$")
_PATIENT_LABEL_LINE_PATTERN = re.compile(r"^paciente(?:\s*[:#-]\s*|\s*$)(.*)$", re.IGNORECASE)


def _normalize_spaces(value: str) -> str:
    return re.sub(r"\s+", " ", str(value or "")).strip()


def sanitize_patient_name_candidate(value: str) -> str:
    raw = _normalize_spaces(value).strip(" :#-\n\r\t")
    if not raw:
        return ""

    normalized = raw.lower()
    normalized = re.sub(r"[^a-z0-9áéíóúñ ]+", " ", normalized)
    normalized = re.sub(r"\s+", " ", normalized).strip()
    if not normalized:
        return ""

    if normalized in _INVALID_NAME_PHRASES:
        return ""
    if any(
        phrase in normalized
        for phrase in ("fecha egreso", "tipo y n", "telefono", "teléfono")
    ):
        return ""

    tokens = re.findall(r"[a-záéíóúñ]+", normalized)
    if len(tokens) < 2:
        return ""

    while len(tokens) >= 2 and tokens[-1] in _TRAILING_LABEL_TOKENS:
        tokens.pop()
    if len(tokens) < 2:
        return ""

    non_joiner_tokens = [token for token in tokens if token not in _NAME_JOINERS]
    if len(non_joiner_tokens) < 2:
        return ""
    if len(non_joiner_tokens) > 5:
        return ""
    if any(token in _INVALID_NAME_TOKENS for token in non_joiner_tokens):
        return ""

    return " ".join(token.upper() for token in tokens[:6]).strip()


def _trim_admin_suffix(value: str) -> str:
    raw = _normalize_spaces(value)
    if not raw:
        return ""
    match = _ADMIN_LABEL_SPLIT_PATTERN.search(raw)
    if match and match.start() > 0:
        raw = raw[: match.start()]
    return raw.strip(" :#-\n\r\t")


def _looks_like_company_name(value: str) -> bool:
    tokens = re.findall(r"[a-záéíóúñ]+", _normalize_spaces(value).lower())
    return sum(1 for token in tokens if token in _PREFAC_COMPANY_TOKENS) >= 2


def _looks_like_explicit_name_line(value: str) -> bool:
    return bool(_NAME_ONLY_LINE_PATTERN.fullmatch(_normalize_spaces(value)))


def _extract_prefactura_patient_name(lines: list[str]) -> str:
    for index, line in enumerate(lines):
        if not _PATIENT_LABEL_LINE_PATTERN.match(line):
            continue
        candidates: list[str] = []
        for probe in range(index, min(index + 6, len(lines))):
            raw_candidate = lines[probe]
            if probe > index and (
                re.search(r"total\s+precio|total\s+de\s+la\s+factura|page\s+\d+", raw_candidate, re.IGNORECASE)
                or re.match(r"\d{2}/\d{2}/\d{2}\s+\d{2}:\d{2}", raw_candidate)
            ):
                break
            if probe == index:
                raw_candidate = _PATIENT_LABEL_LINE_PATTERN.sub(r"\1", raw_candidate, count=1)
            candidate = sanitize_patient_name_candidate(_trim_admin_suffix(raw_candidate))
            if not candidate or _looks_like_company_name(candidate):
                continue
            candidates.append(candidate)
        if candidates:
            return max(candidates, key=lambda item: (len(item.split()), len(item)))
    return ""


def _extract_patient_name_from_patient_lines(lines: list[str]) -> str:
    for index, line in enumerate(lines):
        if not line:
            continue

        patient_line = _PATIENT_LABEL_LINE_PATTERN.match(line)
        if patient_line:
            inline_value = _trim_admin_suffix(patient_line.group(1))
            inline_with_id = re.search(
                r"(?:cc|ti|ce|dni|nit)?\s*[-:]?\s*\d{5,12}\s*-\s*([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ ]{5,80})",
                inline_value,
                re.IGNORECASE,
            )
            if inline_with_id:
                inline_value = inline_with_id.group(1)
            candidate = sanitize_patient_name_candidate(inline_value)
            if candidate:
                return candidate
            for probe in range(index + 1, min(index + 6, len(lines))):
                if not _looks_like_explicit_name_line(_trim_admin_suffix(lines[probe])):
                    continue
                candidate = sanitize_patient_name_candidate(_trim_admin_suffix(lines[probe]))
                if candidate:
                    return candidate
    return ""


def _extract_patient_name_from_patterns(text: str) -> str:
    for pattern in _TEXT_PATTERNS:
        match = pattern.search(text)
        if not match:
            continue
        candidate = sanitize_patient_name_candidate(_trim_admin_suffix(match.group(1)))
        if candidate:
            return candidate
    return ""


def _extract_patient_name_from_name_labels(lines: list[str]) -> str:
    for index, line in enumerate(lines):
        if not line:
            continue

        if re.fullmatch(r"(?:nombre(?:\s+del)?\s+paciente|paciente)", line, re.IGNORECASE):
            for probe in range(index + 1, min(index + 4, len(lines))):
                if not _looks_like_explicit_name_line(_trim_admin_suffix(lines[probe])):
                    continue
                candidate = sanitize_patient_name_candidate(lines[probe])
                if candidate:
                    return candidate

        if re.search(r"nombre(?:\s+del)?\s+paciente", line, re.IGNORECASE):
            fragments = re.split(r"[:#-]", line, maxsplit=1)
            if len(fragments) == 2:
                candidate = sanitize_patient_name_candidate(_trim_admin_suffix(fragments[1]))
                if candidate:
                    return candidate
    return ""


def _extract_patient_name_from_header_matrix(lines: list[str]) -> str:
    for index, line in enumerate(lines[:-1]):
        label = re.search(r"nombre(?:\s+del)?\s+paciente", line, re.IGNORECASE)
        if not label:
            continue
        following_labels = [
            match.start()
            for match in re.finditer(
                r"edad|sexo|identificaci[oó]n|documento|doc\.?\s*id\.?",
                line[label.end() :],
                re.IGNORECASE,
            )
        ]
        end = label.end() + min(following_labels) if following_labels else len(line)
        candidate = sanitize_patient_name_candidate(lines[index + 1][label.start() : end])
        if candidate:
            return candidate
    return ""


def extract_patient_name_from_html(analisis_html: str) -> str:
    if not analisis_html:
        return ""

    patterns = (
        re.compile(
            r"<p><b>Nombre del paciente</b>\s*</p>\s*<p>([^<]+)</p>",
            re.IGNORECASE | re.DOTALL,
        ),
        re.compile(
            r"<p>Nombre del paciente[:\s]*</p>\s*<p>([^<]+)</p>",
            re.IGNORECASE | re.DOTALL,
        ),
        re.compile(
            r"<b>Nombre del paciente</b>\s*:?\s*([A-Za-zÀ-ÿ\u00f1\u00d1\s]+)(?:</p>|<br|$)",
            re.IGNORECASE,
        ),
        re.compile(
            r"<p>.*?Nombre.*?paciente.*?:?\s*([A-Za-zÀ-ÿ\u00f1\u00d1\s]{3,50}?)(?:</p>|<br|$)",
            re.IGNORECASE,
        ),
    )
    for pattern in patterns:
        match = pattern.search(analisis_html)
        if not match:
            continue
        candidate = sanitize_patient_name_candidate(match.group(1))
        if candidate:
            return candidate

    lines = analisis_html.splitlines()
    for index, line in enumerate(lines):
        if not re.search(r"nombre.*paciente", line, re.IGNORECASE):
            continue
        for probe in range(index, min(index + 4, len(lines))):
            candidate_match = re.search(
                r">([A-Za-zÀ-ÿ\u00f1\u00d1\s]{5,50})<",
                lines[probe],
            )
            if not candidate_match:
                continue
            candidate = sanitize_patient_name_candidate(candidate_match.group(1))
            if candidate:
                return candidate
    return ""


def extract_historia_clinica_patient_name_from_text(text: str) -> str:
    if not text:
        return ""

    raw_lines = [line.rstrip() for line in text.splitlines()]
    lines = [_normalize_spaces(line) for line in raw_lines]
    extracted = _extract_patient_name_from_patient_lines(lines)
    if extracted:
        return extracted

    extracted = _extract_patient_name_from_header_matrix(raw_lines)
    if extracted:
        return extracted

    extracted = _extract_patient_name_from_patterns(text)
    if extracted:
        return extracted

    return _extract_patient_name_from_name_labels(lines)


def extract_prefactura_patient_name_from_text(text: str) -> str:
    if not text:
        return ""

    lines = [_normalize_spaces(line) for line in text.splitlines()]
    prefactura_name = _extract_prefactura_patient_name(lines)
    if prefactura_name:
        return prefactura_name
    return extract_historia_clinica_patient_name_from_text(text)


def extract_patient_name_from_text(text: str) -> str:
    """Stable shared extractor for generic document reading.

    Keep this behavior backward compatible. New feature-specific identity
    heuristics must live in dedicated extractors and be selected explicitly by
    the caller.
    """

    if not text:
        return ""

    if re.search(r"prefactura|extracto de cuenta", text, re.IGNORECASE):
        extracted = extract_prefactura_patient_name_from_text(text)
        if extracted:
            return extracted

    return extract_historia_clinica_patient_name_from_text(text)
