"""Seed-propagation (V-3): deterministic spread of frame-found names.

A name anchored by a frame ("Robert Paulson" in a caption) is a seed. Each seed and its
significant tokens are string-matched across the full document text, so bare first- or
last-name repeats elsewhere are flagged without any model call. Matching is case-sensitive
and whole-word, and any occurrence already covered by a frame match is skipped, which keeps
the sweep from re-flagging the seed itself or firing on lowercase common words.

This sits between the deterministic frames and the model pass. It does NOT narrow the text
given to the model (V-3): the model still reads the full document for the irreducible cases
(a name in no frame anywhere). Propagation only adds coverage for names already seeded.
"""
from __future__ import annotations

from typing import Iterable, Optional
import re

from .detectors.base import Match

# Capitalized tokens that are never propagated as a name on their own.
_TOKEN_STOP = {
    "the", "of", "and", "for", "esq", "estate", "trust", "court", "county",
    "petitioner", "respondent", "plaintiff", "defendant", "state", "will",
    "testament", "affidavit", "witness", "notary", "public", "bar", "no",
    "dear", "mr", "mrs", "ms", "dr", "attorney", "respectfully", "submitted",
}


def _seed_terms(seeds: Iterable[Match]) -> dict[str, tuple[str, Optional[str]]]:
    """Map each propagatable term (full name + significant tokens) to (type, role)."""
    terms: dict[str, tuple[str, Optional[str]]] = {}
    for m in seeds:
        full = m.raw.strip()
        if full:
            terms.setdefault(full, (m.type, m.role))
        for tok in full.split():
            t = tok.strip(",.;:'\"’")
            if (len(t) >= 3 and t[0].isupper() and t.lower() not in _TOKEN_STOP
                    and any(c.isalpha() for c in t)):
                terms.setdefault(t, (m.type, m.role))
    return terms


def propagate_seeds(text: str, seeds: list[Match]) -> list[Match]:
    """Return propagated name Matches for bare seed-token repeats not already covered."""
    placed: set[tuple[int, int]] = {(m.start, m.end) for m in seeds}
    out: list[Match] = []
    # Longest terms first so a full-name occurrence claims its span before its tokens do.
    for term, (ftype, role) in sorted(_seed_terms(seeds).items(),
                                      key=lambda kv: -len(kv[0])):
        for mt in re.finditer(rf"\b{re.escape(term)}\b", text):  # case-sensitive
            span = (mt.start(), mt.end())
            if any(not (span[1] <= a or span[0] >= b) for a, b in placed):
                continue
            placed.add(span)
            out.append(Match(
                detector="name.propagated", type=ftype, start=span[0], end=span[1],
                raw=term, preview=term, role=role, confidence=0.55,
                detail={"propagated": True},
            ))
    return out
