"""Dictionary-backed multilingual name detector.

Replaces the purely heuristic positional approach for name detection with
language-specific first-name lexicons.  The lexicons cover the most common
given names across EN/DE/FR/ES/PL/PT/IT/NL and several cross-language names.

Detection strategy
------------------
1. Scan the text for tokens that appear in the first-name lexicon.
2. If a lexicon-matched token is immediately followed by a capitalised token
   (candidate surname), emit both as a NAME entity.
3. If a lexicon-matched token is preceded by a title (Dr., Mr., …) emit the
   title + first + next-cap-word span as NAME.
4. Standalone lexicon tokens (without a following capitalised token) are
   emitted only when a context trigger is nearby (within 60 chars before):
   name-introducing keywords from the multilingual keyword table.
5. All findings are subject to the standard blocklist.

The lexicon is intentionally modest (~400 names) to keep the module fast and
avoid false positives on common nouns that overlap with names (e.g. "May",
"June", "Mark").  Rare or ambiguous names are excluded.
"""
import re
from typing import Dict, FrozenSet, List, Set, Union

from upf.config import UPFConfig

# ---------------------------------------------------------------------------
# Title set (reused from ner.py concept)
# ---------------------------------------------------------------------------
_TITLES: FrozenSet[str] = frozenset({
    "mr", "mrs", "ms", "miss", "dr", "prof", "professor", "sir", "rev",
    "reverend", "capt", "captain", "lt", "lieutenant", "sgt", "sergeant",
    "col", "colonel", "gen", "general", "ceo", "cto", "cfo", "vp",
    "dear", "hello", "hi", "hey",
})

# ---------------------------------------------------------------------------
# Context keywords that boost standalone first-name confidence
# (subset of multilingual.py keywords, condensed)
# ---------------------------------------------------------------------------
_NAME_CONTEXT_RE = re.compile(
    r"(?i)\b(?:name|full\s+name|first\s+name|patient\s+name|vorname|pr[eé]nom|"
    r"nombre|imi[eę]|nome|naam|voornaam|imie|nazwisko|sobrenome|cognome|"
    r"apellido|nachname|patient|dear|hello|hi|hey|sincerely|regards|signed?)\s*[:\-]?\s*$"
)

# ---------------------------------------------------------------------------
# First-name lexicon  (lowercase for fast lookup)
# Names that are also common nouns/adjectives are excluded (May, Mark, etc.)
# ---------------------------------------------------------------------------
_FIRST_NAMES: FrozenSet[str] = frozenset({
    # EN — very common
    "james", "john", "robert", "michael", "william", "david", "richard",
    "joseph", "thomas", "charles", "christopher", "daniel", "matthew",
    "anthony", "donald", "paul", "steven", "andrew", "kenneth", "kevin",
    "brian", "george", "timothy", "ronald", "edward", "jason", "jeffrey",
    "ryan", "jacob", "gary", "nicholas", "eric", "jonathan", "stephen",
    "larry", "justin", "scott", "brandon", "frank", "benjamin", "raymond",
    "samuel", "gregory", "patrick", "alexander", "jack", "dennis", "jerry",
    "tyler", "aaron", "henry", "douglas", "peter", "adam", "nathan",
    "zachary", "walter", "kyle", "harold", "carl", "jeremy", "ethan",
    "sean", "austin", "christian", "arthur", "jesse", "lawrence", "dylan",
    "joe", "bryan", "billy", "bruce", "jordan", "ralph", "roy", "eugene",
    "wayne", "alan", "juan", "louis", "johnny", "dean", "phillip",
    # EN female
    "mary", "patricia", "jennifer", "linda", "barbara", "elizabeth",
    "susan", "jessica", "sarah", "karen", "lisa", "nancy", "betty",
    "margaret", "sandra", "ashley", "dorothy", "kimberly", "emily",
    "donna", "michelle", "carol", "amanda", "melissa", "deborah",
    "stephanie", "rebecca", "sharon", "laura", "cynthia", "kathleen",
    "amy", "angela", "shirley", "anna", "brenda", "pamela", "emma",
    "nicole", "helen", "samantha", "katherine", "christine", "debra",
    "rachel", "carolyn", "janet", "catherine", "maria", "heather",
    "diane", "julie", "joyce", "victoria", "kelly", "christina", "ruth",
    "joan", "virginia", "judith", "evelyn", "hannah", "andrea", "megan",
    "cheryl", "jacqueline", "martha", "madison", "olivia", "gloria",
    "teresa", "sara", "janice", "ann", "alice", "julia", "jean",
    "frances", "grace", "amber", "denise", "danielle", "marilyn", "beverly",
    "isabella", "sophia", "abigail", "brittany", "diana", "audrey",
    "kayla", "alexis", "tiffany", "bianca", "vanessa", "natalie",
    # DE
    "hans", "fritz", "dieter", "günter", "gerhard", "helmut", "walter",
    "werner", "horst", "manfred", "jürgen", "kurt", "otto", "heinrich",
    "karl", "wilhelm", "rolf", "bernd", "frank", "stefan", "thomas",
    "michael", "christian", "andreas", "markus", "tobias", "florian",
    "felix", "luca", "lukas", "leon", "noah", "elias", "ben", "finn",
    "anna", "maria", "monika", "ursula", "petra", "brigitte", "sabine",
    "helga", "ingrid", "renate", "karin", "claudia", "gabriele", "ute",
    "ilse", "hannelore", "inge", "lena", "lea", "mia", "emma", "hannah",
    "sophie", "lisa", "julia", "laura", "sarah", "katharina", "nina",
    # FR
    "jean", "pierre", "michel", "jacques", "alain", "philippe", "christophe",
    "nicolas", "patrick", "thierry", "eric", "olivier", "laurent", "stephane",
    "jerome", "alexandre", "julien", "antoine", "clement", "hugo", "theo",
    "lucas", "nathan", "maxime", "axel", "baptiste", "romain", "kevin",
    "marie", "nathalie", "isabelle", "sylvie", "catherine", "anne", "monique",
    "valerie", "sandrine", "caroline", "aurelie", "camille", "lucie",
    "charlotte", "lea", "manon", "inès", "chloe", "alice", "julie",
    # ES
    "jose", "antonio", "francisco", "juan", "manuel", "pedro", "angel",
    "miguel", "jesus", "javier", "carlos", "alejandro", "david", "daniel",
    "pablo", "luis", "rafael", "sergio", "adrián", "álvaro", "diego",
    "laura", "maria", "carmen", "ana", "isabel", "pilar", "rosa",
    "dolores", "elena", "marta", "patricia", "cristina", "lucía", "paula",
    "sara", "beatriz", "raquel", "andrea", "silvia", "monica", "virginia",
    # PL
    "jan", "piotr", "andrzej", "krzysztof", "stanisław", "tomasz", "paweł",
    "marek", "michał", "grzegorz", "adam", "łukasz", "jacek", "mariusz",
    "marcin", "rafał", "przemysław", "robert", "kamil", "mateusz",
    "anna", "maria", "katarzyna", "małgorzata", "agnieszka", "barbara",
    "ewa", "krystyna", "zofia", "helena", "irena", "monika", "elżbieta",
    "teresa", "danuta", "jadwiga", "joanna", "magdalena", "natalia", "julia",
    # PT
    "joão", "pedro", "manuel", "carlos", "paulo", "luis", "andré",
    "rui", "ricardo", "nuno", "miguel", "tiago", "filipe", "jorge",
    "sérgio", "marco", "hugo", "diogo", "rodrigo", "daniel",
    "ana", "maria", "sofia", "inês", "catarina", "marta", "patrícia",
    "rita", "carla", "sandra", "vera", "paula", "helena", "isabel",
    "cristina", "margarida", "susana", "cláudia", "filipa", "beatriz",
    # IT
    "marco", "luca", "andrea", "giuseppe", "antonio", "giovanni",
    "roberto", "stefano", "luigi", "mario", "franco", "angelo", "sergio",
    "fabio", "enrico", "davide", "matteo", "alberto", "massimo", "daniele",
    "anna", "maria", "giulia", "francesca", "sara", "laura", "valentina",
    "chiara", "elena", "paola", "roberta", "silvia", "claudia", "angela",
    "lucia", "marta", "federica", "serena", "alessandra", "simona",
    # NL
    "jan", "piet", "kees", "henk", "gerrit", "arie", "cor", "peter",
    "hans", "dirk", "willem", "bas", "ruud", "sjaak", "theo",
    "emma", "mia", "sophie", "julia", "olivia", "anna", "lisa",
    "lotte", "floor", "nora", "iris", "fien", "eline", "roos",
})

# Regex to find candidate capitalised name tokens
_CAP_TOKEN_RE = re.compile(r"\b([A-ZÁÀÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ][a-záàâãäåæçèéêëìíîïðñòóôõöøùúûüýþ]+(?:-[A-Z][a-z]+)?)\b")

# Blocklist words that are capitalized but not names
_WORD_BLOCKLIST: FrozenSet[str] = frozenset({
    "january", "february", "march", "april", "june", "july",
    "august", "september", "october", "november", "december",
    "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
    "the", "this", "that", "these", "those", "here", "there",
    "true", "false", "null", "none", "yes", "no",
    "http", "https", "www", "page", "section", "chapter",
    "street", "avenue", "boulevard", "road", "drive", "lane",
    "dear", "hello", "hi", "hey", "sincerely", "regards",
    "company", "corporation", "limited", "inc", "ltd", "llc", "gmbh", "sa",
    # Document / form field label words — prevent extending first name into these
    "account", "invoice", "balance", "subtotal", "total", "amount",
    "date", "number", "description", "quantity", "reference", "holder",
    "withdrawal", "deposit", "transfer", "payment", "transaction",
    "statement", "cardholder", "expiry", "issued", "issuer",
    "address", "city", "country", "nationality", "gender", "occupation",
    "subject", "attachment", "porice",
    "name", "email", "phone", "id", "bank",
})


def _is_title(word: str) -> bool:
    return word.lower().rstrip(".") in _TITLES


def _normalize(word: str) -> str:
    """Lowercase and strip common diacritics for accent-insensitive lexicon lookup."""
    import unicodedata
    nfkd = unicodedata.normalize("NFKD", word.lower())
    return "".join(c for c in nfkd if not unicodedata.combining(c))


_FIRST_NAMES_NORMALIZED: FrozenSet[str] = frozenset(_normalize(n) for n in _FIRST_NAMES)


def _is_in_lexicon(word: str) -> bool:
    lower = word.lower()
    return lower in _FIRST_NAMES or _normalize(word) in _FIRST_NAMES_NORMALIZED


def detect_name_dict_entities(
    text: str, config: UPFConfig
) -> List[Dict[str, Union[int, str]]]:
    """Detect person names using the first-name lexicon + contextual triggers.

    Complements the heuristic NER detector — emits NAME findings for tokens
    found in the lexicon, extended with a following capitalised surname token.
    """
    if not config.redact_names:
        return []

    findings: List[Dict[str, Union[int, str]]] = []
    seen: Set[tuple] = set()

    def _add(start: int, end: int, matched: str) -> None:
        key = (start, end)
        if key not in seen and end > start:
            seen.add(key)
            findings.append({"type": "NAME", "start": start, "end": end, "text": matched})

    # Scan all capitalised tokens in the text
    tokens = list(_CAP_TOKEN_RE.finditer(text))
    for i, m in enumerate(tokens):
        word = m.group(1)
        if word.lower() in _WORD_BLOCKLIST:
            continue
        if not _is_in_lexicon(word):
            continue

        start = m.start(1)
        end = m.end(1)

        # Try to extend with a following capitalised surname token
        if i + 1 < len(tokens):
            next_m = tokens[i + 1]
            next_word = next_m.group(1)
            # Only extend if next token is adjacent (within a few chars — same phrase)
            # and the gap contains no newlines (prevents grabbing cross-line label text)
            gap = text[end:next_m.start(1)]
            if (gap.strip() == "" and len(gap) <= 3 and "\n" not in gap
                    and next_word.lower() not in _WORD_BLOCKLIST):
                # Emit first+last as a single span
                _add(start, next_m.end(1), text[start:next_m.end(1)])
                continue

        # Standalone first name — only emit with context trigger or title before it
        preceding = text[max(0, start - 80): start]
        if _NAME_CONTEXT_RE.search(preceding):
            _add(start, end, word)
            continue

        # Check for title immediately before
        words_before = preceding.rstrip().split()
        if words_before:
            last = words_before[-1].rstrip(".,;:")
            if _is_title(last):
                _add(start, end, word)

    findings.sort(key=lambda f: (int(f["start"]), int(f["end"])))
    return findings
