"""Entity-preserving MT — #000056 §9 lever (the validated one).

The fan-out delta (EN 85% → ES+sandwich 71%, −14pp) showed the
fixable slice of the LOST set is opus-mt *translating or garbling
proper nouns*: ``New London``→"nuevo Londres", ``Tarsus``→"Tarso",
``Boltzmann``→"perntzmann", ``the play Hamlet``→"the game village".
The standard MT fix: mask protected spans with sentinels MarianMT
copies verbatim, translate, restore the originals.

Detection is deliberately **deterministic and hand-rolled** (five-step:
no NER model until a model earns it):

1. quoted spans — verbatim idioms/titles (``"winter is coming"``);
2. Capitalised multi-word runs (with lowercase connectors) and
   mid-sentence Capitalised tokens — ``New London``, ``Paul of
   Tarsus``, ``Eiffel Tower``, ``Boltzmann``, ``Hamlet``.

Scope honesty: mask/restore fixes the *garble/translate* class. It
does NOT solve exonym/transliteration (``Egipto``→corpus "Egypt",
``Segunda Guerra Mundial``→"World War II") — that needs a gazetteer
and is out of v1 (documented in #000056 §9).
"""
from __future__ import annotations

import re

# Sentinels: short, all-caps, alnum, no spaces — MarianMT copies these
# through untouched (verified by test). Bracketing letters make an
# accidental in-vocab collision astronomically unlikely.
_PH = "ZQX{0}XQZ"
_PH_RE = re.compile(r"ZQX(\d+)XQZ")

_QUOTED = re.compile(r"[\"“”«»']([^\"“”«»']{2,60})[\"“”«»']")
# A proper-noun run: a Capitalised word, optionally chained through
# lowercase connectors (of/de/the/von/…) to more Capitalised words.
_CAP_RUN = re.compile(
    r"\b[A-ZÀ-Þ][\wÀ-ÿ'’.-]*"
    r"(?:\s+(?:of|de|del|la|le|van|von|der|the|y|and|&)\s+[A-ZÀ-Þ][\wÀ-ÿ'’.-]*"
    r"|\s+[A-ZÀ-Þ][\wÀ-ÿ'’.-]*)*"
)
# Leading-token guard: a sentence-initial Capitalised word that is just
# an ordinary first word (wh/function) is NOT an entity.
_NOT_ENTITY = frozenset(
    "what who when where why how which whose is are was were do does did "
    "the a an this that these those tell name list describe explain give "
    "qué quién cuándo dónde cómo cuál cuáles por qué el la los las un una "
    "что кто когда где почему как какой".split()
)


def protect_spans(text: str) -> list[tuple[int, int]]:
    """Char ranges to keep verbatim through MT, longest-first, merged."""
    spans: list[tuple[int, int]] = []
    for m in _QUOTED.finditer(text):
        spans.append((m.start(), m.end()))
    for m in _CAP_RUN.finditer(text):
        s, e = m.start(), m.end()
        tok = text[s:e]
        if tok.lower() in _NOT_ENTITY:
            continue
        # Single leading-Capitalised ordinary word at pos 0 → skip.
        if s == 0 and " " not in tok and tok.lower() in _NOT_ENTITY:
            continue
        spans.append((s, e))
    spans.sort(key=lambda p: (p[0], -(p[1] - p[0])))
    merged: list[tuple[int, int]] = []
    for s, e in spans:
        if merged and s < merged[-1][1]:
            continue  # overlapped by an earlier (longer) span
        merged.append((s, e))
    return merged


def mask(text: str) -> tuple[str, dict[str, str]]:
    spans = protect_spans(text)
    if not spans:
        return text, {}
    out, mapping, last, i = [], {}, 0, 0
    for s, e in spans:
        out.append(text[last:s])
        ph = _PH.format(i)
        mapping[ph] = text[s:e]
        out.append(ph)
        last = e
        i += 1
    out.append(text[last:])
    return "".join(out), mapping


def restore(text: str, mapping: dict[str, str]) -> str:
    if not mapping:
        return text

    def _sub(m: re.Match) -> str:
        ph = m.group(0)
        return mapping.get(ph, ph)

    # MarianMT may alter spacing/case around a copied sentinel; match
    # the numeric core so ``ZQX 0 XQZ`` / ``zqx0xqz`` still restore.
    return re.sub(r"[Zz][Qq][Xx]\s*(\d+)\s*[Xx][Qq][Zz]",
                  lambda m: mapping.get(_PH.format(int(m.group(1))), m.group(0)),
                  text)


class MaskedTranslator:
    """Wraps any ``Translator``: mask protected spans → delegate →
    restore. Same protocol, so the sandwich is engine-agnostic."""

    def __init__(self, inner):
        self._inner = inner
        self.engine_id = f"{getattr(inner, 'engine_id', 'mt')}+entmask-v1"
        self.manifest_hash = f"{getattr(inner, 'manifest_hash', '')}:entmask-v1"

    @property
    def available(self) -> bool:
        return getattr(self._inner, "available", False)

    def translate(self, text: str, src: str, tgt: str) -> str:
        masked, mapping = mask(text)
        out = self._inner.translate(masked, src, tgt)
        if not getattr(self._inner, "available", False):
            return text  # graceful-degrade like the inner engine
        return restore(out, mapping)
