"""Cross-language retrieval guard — Ticket #000001 §7, Phase 0.

Deterministic, no model. Detects a query that is not in the corpus
language via a high-precision signal (inverted punctuation ``¿``/``¡``
or a non-ASCII Latin letter), strips source-language function-word
noise from the *retrieval* token set so it cannot drive an OR-mode
full-corpus FTS5 scan, and fails closed to UNGROUNDED before
retrieval/LLM when no groundable content token survives.

Bright line (CLAUDE.md "soft hash vs hard hash" / ticket §7.1): this
is a **retrieval-side** soft guard. It never touches the verifier or
``audit_mode``, and never enters ``question_hash`` (the user's
question is preserved) or ``verifier_policy_hash`` (verifier
byte-identical). The enabling *policy flag* does move
``governance_policy_hash`` — like every policy flag, since that hash
covers the whole policy dict (keys.py:182) — which correctly
partitions the cache by guard state. The English path is
**byte-identical by construction**: ``_NON_ENGLISH_SIGNAL_RE``
cannot match a pure-ASCII
query without inverted punctuation, so ``guard()`` returns ``None``
and the caller does nothing.

Scope is es-v1 only. Other languages (and the MT retrieval route)
are Phase 1, gated on the MT-provider decision (ticket §7.5).

The token regex / English stopword set are imported from
``arborist.search.fts5`` so "content token" here means exactly what
the retrieval path means (same ASCII regex, same stopwords, same
``len > 1`` rule). A blanket short-token heuristic is deliberately
NOT used — it would regress #000053 / #000054 (``AI``/``ML``/``CPU``/
``GPU`` are load-bearing 2-3-char tokens). The es stoppack is gated
behind the non-English signal precisely so ``un``→UN / ``la``→LA /
``de``→De collisions can never reach an English query.
"""
from __future__ import annotations

import re
from dataclasses import dataclass

from arborist.search.fts5 import _FTS5_STOPWORDS, _FTS5_TOKEN_RE

# Inverted punctuation (¿ ¡) or any non-ASCII byte (á é í ó ú ñ ü ç …).
# Pure-ASCII English questions never match → guard() returns None.
_NON_ENGLISH_SIGNAL_RE = re.compile(r"[¿¡]|[^\x00-\x7F]")

# Spanish v1 function words, ASCII-folded / accent-truncated to match
# what _FTS5_TOKEN_RE actually emits (``¿Qué`` → "Qu"; ``qué`` →
# "qu"/"que"). Length-1 forms (a o y) are already dropped by the
# ``len > 1`` rule in fts5._query_tokens; not listed.
_ES_STOPWORDS = frozenset(
    """
    qu que es el la lo los las le les un una uno unos unas
    de del al en su sus se con por para
    """.split()
)


@dataclass(frozen=True)
class CrossLanguageDecision:
    """Result of the deterministic guard. ``None`` (not this class) is
    returned for English — see ``guard``."""

    dropped: tuple[str, ...]         # es function tokens stripped from retrieval
    content_tokens: tuple[str, ...]  # surviving non-stopword tokens
    fail_closed: bool                # True → nothing groundable; reject pre-retrieval

    @property
    def reason(self) -> str:
        if self.fail_closed:
            return (
                "cross-language query: no corpus-language content token "
                "survived the source-language function-word filter "
                "(es-v1). Fail closed before retrieval/LLM."
            )
        return (
            "cross-language query: source-language function words stripped "
            f"from the retrieval set ({', '.join(self.dropped) or 'none'}); "
            "retrieval runs on surviving content tokens only."
        )


def _fts_tokens(text: str) -> list[str]:
    """Exactly fts5._query_tokens' tokenization (ASCII regex, English
    stopwords, len > 1) so 'content token' matches the retrieval path."""
    raw = _FTS5_TOKEN_RE.findall(text or "")
    return [t for t in raw if t.lower() not in _FTS5_STOPWORDS and len(t) > 1]


def guard(question: str) -> CrossLanguageDecision | None:
    """``None`` when the query looks like English (caller does nothing —
    byte-identical path). Otherwise a decision the caller acts on
    (ticket §7.3)."""
    if not _NON_ENGLISH_SIGNAL_RE.search(question or ""):
        return None
    toks = _fts_tokens(question)
    dropped = tuple(t for t in toks if t.lower() in _ES_STOPWORDS)
    content = tuple(t for t in toks if t.lower() not in _ES_STOPWORDS)
    return CrossLanguageDecision(
        dropped=dropped,
        content_tokens=content,
        fail_closed=(len(content) == 0),
    )


def strip_for_retrieval(question: str, decision: CrossLanguageDecision) -> str:
    """Retrieval-only query string with es function words removed. The
    LLM, verifier, and ``question_hash`` never see this — only the FTS5
    / title / phrase routes do. Downstream applies its own English
    stopword filter, so returning raw-minus-es is sufficient and
    order-preserving."""
    raw = _FTS5_TOKEN_RE.findall(question or "")
    kept = [t for t in raw if t.lower() not in _ES_STOPWORDS]
    return " ".join(kept)
