"""Name detection by structural frame, with role inference (R-3).

No watchlist. Names are found by the structures that surround them in legal
documents; the Title-Case (or all-caps) run sitting in the frame's slot is captured
as the name. Because detection is anchored to frames, running prose with many
capitalized tokens but no frame yields few or no findings, which is what keeps the
clean-prose false-positive rate low.

Roles use the closed vocabulary from R-3: petitioner, respondent, testator, affiant,
attorney, notary, witness, party, unknown. Litigation labels with no dedicated slot
(plaintiff, defendant, appointee) map to "party".

Confidence (R-17, informed by conflicts.ts tiers): a unique, unambiguous frame such as
a testamentary or affiant clause scores high; an honorific or a bare "In re" scores
medium.
"""
from __future__ import annotations

import re
from typing import Iterator

from .base import Match

# A name token: Title-Case word, an initial like "Q.", a hyphenated name, or ALL CAPS.
_CAP = r"[A-Z][A-Za-z'’.\-]+|[A-Z]\.|[A-Z]{2,}"
# A name run: 1..6 such tokens separated by single whitespace (incl. newlines).
NAME = rf"(?:{_CAP})(?:[ \t]+(?:{_CAP})){{0,5}}"

ENTITY_SUFFIXES = {
    "llc", "l.l.c.", "inc", "inc.", "corp", "corp.", "corporation", "company",
    "co", "co.", "trust", "bank", "llp", "ltd", "ltd.", "foundation",
    "association", "partners", "group", "n.a.", "p.c.", "pllc",
}
# Slots that are role words or boilerplate, never a name.
STOP_SLOTS = {
    "petitioner", "respondent", "plaintiff", "defendant", "state", "people",
    "commonwealth", "united states", "esq", "esq.", "notary public", "notary",
    "witness", "witnesses", "the", "estate", "court", "respectfully submitted",
}

ROLE_PETITIONER = "petitioner"
ROLE_RESPONDENT = "respondent"
ROLE_TESTATOR = "testator"
ROLE_AFFIANT = "affiant"
ROLE_ATTORNEY = "attorney"
ROLE_NOTARY = "notary"
ROLE_WITNESS = "witness"
ROLE_PARTY = "party"
ROLE_UNKNOWN = "unknown"


def _clean(slot: str) -> str:
    return slot.strip().strip(",;:").strip()


def _is_stopword_slot(slot: str) -> bool:
    s = _clean(slot).lower()
    if not s or s in STOP_SLOTS:
        return True
    tokens = s.split()
    # All tokens are stop words / connectors -> not a name.
    return all(t in STOP_SLOTS or t in {"of", "and", "the", "&"} for t in tokens)


def _name_type(slot: str) -> str:
    last = _clean(slot).split()[-1].lower() if _clean(slot) else ""
    return "entity_name" if last in ENTITY_SUFFIXES else "person_name"


def _emit(text: str, m: re.Match, group: int, detector: str, role: str,
          confidence: float) -> Iterator[Match]:
    raw_full = m.group(group)
    if raw_full is None:
        return
    # Trim trailing connectors/whitespace from the captured run.
    raw = raw_full
    trimmed = raw.rstrip()
    while trimmed and trimmed.split()[-1].lower() in {"and", "of", "the", "&"}:
        trimmed = trimmed.rsplit(" ", 1)[0].rstrip()
    if not trimmed or _is_stopword_slot(trimmed):
        return
    start = m.start(group)
    end = start + len(trimmed)
    yield Match(
        detector=detector,
        type=_name_type(trimmed),
        start=start,
        end=end,
        raw=trimmed,
        preview=trimmed,
        role=role,
        confidence=confidence,
        detail={"frame": detector.split(".", 2)[-1]},
    )


# --- Frame patterns. Keywords are case-insensitive via (?i:...); the NAME slot
# stays case-sensitive so lowercase prose is not captured. ---

_MARRIAGE = re.compile(rf"(?i:in re(?:\s+the)?\s+marriage of)\s*:?\s*({NAME})"
                       rf"(?:\s+(?i:and)\s+({NAME}))?")
_INRE = re.compile(rf"(?i:in re)\s*:?\s+({NAME})")
_VS_FULL = re.compile(
    rf"({NAME})\s*,\s*(?i:plaintiff|petitioner)\s*,?\s*"
    rf"(?i:v\.?|vs\.?|versus)\s+({NAME})\s*,\s*(?i:defendant|respondent)"
)
_STATE_V = re.compile(
    rf"(?i:state|the people|people|commonwealth|united states)\s+"
    rf"(?i:v\.?|vs\.?|versus)\s+({NAME})"
)
_ROLE_TRAILING = re.compile(rf"({NAME})\s*,\s*(?P<role>(?i:petitioner|respondent))\b")

_TESTATOR = re.compile(rf"(?i:\bI),\s*({NAME})\s*,\s*(?i:being of sound mind|"
                       rf"a resident|do hereby|declare|being of legal age)")
_APPOINT = re.compile(rf"(?i:I (?:hereby )?(?:appoint|nominate|designate))\s+({NAME})\s+"
                      rf"(?i:as\s+(?:my\s+)?(?:personal representative|executor|"
                      rf"executrix|trustee|successor trustee|agent|attorney-in-fact|"
                      rf"guardian|conservator))")
_APPEARED = re.compile(rf"(?i:personally appeared)\s+({NAME})")
_SWORN = re.compile(rf"({NAME})\s*,\s*(?i:being first duly sworn|"
                    rf"being duly sworn|after being duly sworn)")

_HONORIFIC = re.compile(rf"\b(?:Mr|Mrs|Ms|Dr|Hon|Prof|Rev)\.\s+({NAME})")
_ESQ = re.compile(rf"({NAME})\s*,\s*(?i:esquire|esq)\.?")
_BAR_NO = re.compile(rf"({NAME})\s*,?\s*(?i:(?:mo(?:\.|uri)?\s*)?bar(?:\s*(?:no\.?|#|"
                     rf"number))?)\s*:?\s*\d", re.IGNORECASE)
_ATTORNEY_FOR = re.compile(rf"(?i:attorney for)\s+({NAME})")

_NOTARY = re.compile(rf"({NAME})\s*,\s*(?i:notary public)")
_WITNESS = re.compile(rf"(?i:witness(?:eth)?|attesting witness)\s*:?\s*({NAME})")


def _f_marriage(text: str) -> Iterator[Match]:
    for m in _MARRIAGE.finditer(text):
        yield from _emit(text, m, 1, "name.frame.caption", ROLE_PETITIONER, 0.85)
        if m.group(2):
            yield from _emit(text, m, 2, "name.frame.caption", ROLE_RESPONDENT, 0.85)


def _f_inre(text: str) -> Iterator[Match]:
    for m in _INRE.finditer(text):
        # Skip the marriage form, which the marriage pattern already handled.
        if re.match(r"(?i:the\s+)?marriage", text[m.start(1):m.start(1) + 12]):
            continue
        yield from _emit(text, m, 1, "name.frame.caption", ROLE_PARTY, 0.6)


def _f_vs_full(text: str) -> Iterator[Match]:
    for m in _VS_FULL.finditer(text):
        r1 = ROLE_PETITIONER if re.search(r"(?i:petitioner)", m.group(0)) else ROLE_PARTY
        yield from _emit(text, m, 1, "name.frame.caption", r1, 0.85)
        r2 = ROLE_RESPONDENT if re.search(r"(?i:respondent)", m.group(0)) else ROLE_PARTY
        yield from _emit(text, m, 2, "name.frame.caption", r2, 0.85)


def _f_state_v(text: str) -> Iterator[Match]:
    for m in _STATE_V.finditer(text):
        yield from _emit(text, m, 1, "name.frame.caption", ROLE_PARTY, 0.8)


def _f_role_trailing(text: str) -> Iterator[Match]:
    for m in _ROLE_TRAILING.finditer(text):
        role = m.group("role").lower()
        yield from _emit(text, m, 1, "name.frame.caption", role, 0.8)


def _f_testator(text: str) -> Iterator[Match]:
    for m in _TESTATOR.finditer(text):
        yield from _emit(text, m, 1, "name.frame.testamentary", ROLE_TESTATOR, 0.9)


def _f_appoint(text: str) -> Iterator[Match]:
    for m in _APPOINT.finditer(text):
        yield from _emit(text, m, 1, "name.frame.testamentary", ROLE_PARTY, 0.8)


def _f_appeared(text: str) -> Iterator[Match]:
    for m in _APPEARED.finditer(text):
        yield from _emit(text, m, 1, "name.frame.affiant", ROLE_AFFIANT, 0.9)


def _f_sworn(text: str) -> Iterator[Match]:
    for m in _SWORN.finditer(text):
        yield from _emit(text, m, 1, "name.frame.affiant", ROLE_AFFIANT, 0.85)


def _f_honorific(text: str) -> Iterator[Match]:
    for m in _HONORIFIC.finditer(text):
        yield from _emit(text, m, 1, "name.frame.salutation", ROLE_PARTY, 0.6)


def _f_esq(text: str) -> Iterator[Match]:
    for m in _ESQ.finditer(text):
        yield from _emit(text, m, 1, "name.frame.attorney", ROLE_ATTORNEY, 0.85)


def _f_bar_no(text: str) -> Iterator[Match]:
    for m in _BAR_NO.finditer(text):
        yield from _emit(text, m, 1, "name.frame.attorney", ROLE_ATTORNEY, 0.8)


def _f_attorney_for(text: str) -> Iterator[Match]:
    for m in _ATTORNEY_FOR.finditer(text):
        # The slot may be a role word ("Attorney for Petitioner"); _emit drops those.
        yield from _emit(text, m, 1, "name.frame.attorney", ROLE_PARTY, 0.55)


def _f_notary(text: str) -> Iterator[Match]:
    for m in _NOTARY.finditer(text):
        yield from _emit(text, m, 1, "name.frame.notary", ROLE_NOTARY, 0.85)


def _f_witness(text: str) -> Iterator[Match]:
    for m in _WITNESS.finditer(text):
        yield from _emit(text, m, 1, "name.frame.witness", ROLE_WITNESS, 0.75)


ALL_FRAMES = [
    _f_marriage, _f_inre, _f_vs_full, _f_state_v, _f_role_trailing,
    _f_testator, _f_appoint, _f_appeared, _f_sworn,
    _f_honorific, _f_esq, _f_bar_no, _f_attorney_for, _f_notary, _f_witness,
]


def detect_all(text: str) -> list[Match]:
    out: list[Match] = []
    seen: set[tuple[int, int]] = set()
    for frame in ALL_FRAMES:
        for m in frame(text):
            key = (m.start, m.end)
            if key in seen:
                continue
            seen.add(key)
            out.append(m)
    return out
