import re
from typing import Dict, List, Set, Union

from upf.config import UPFConfig

# Two-word capitalized pattern, optionally hyphenated within each part.
# Does NOT cross newlines — prevents grabbing multi-line document label spans.
NAME_RE = re.compile(r"\b([A-Z][a-z]+(?:-[A-Z][a-z]+)?)[ \t]+([A-Z][a-z]+(?:-[A-Z][a-z]+)?)\b")

# Titles / salutations that strongly signal the following words are a person name.
_TITLES: Set[str] = {
    "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",
}

# Common word pairs that are NOT names. Compared lowercased.
_BLOCKLIST: Set[frozenset] = {
    frozenset({"new", "york"}),
    frozenset({"new", "zealand"}),
    frozenset({"new", "jersey"}),
    frozenset({"new", "mexico"}),
    frozenset({"new", "orleans"}),
    frozenset({"los", "angeles"}),
    frozenset({"san", "francisco"}),
    frozenset({"las", "vegas"}),
    frozenset({"hong", "kong"}),
    frozenset({"united", "states"}),
    frozenset({"united", "kingdom"}),
    frozenset({"north", "america"}),
    frozenset({"south", "america"}),
    frozenset({"north", "korea"}),
    frozenset({"south", "korea"}),
    frozenset({"north", "africa"}),
    frozenset({"latin", "america"}),
    frozenset({"middle", "east"}),
    frozenset({"supreme", "court"}),
    frozenset({"white", "house"}),
    frozenset({"wall", "street"}),
    frozenset({"main", "street"}),
    frozenset({"high", "school"}),
    frozenset({"real", "estate"}),
    frozenset({"social", "security"}),
    frozenset({"artificial", "intelligence"}),
    frozenset({"machine", "learning"}),
    frozenset({"open", "source"}),
    # Document / form field label pairs
    frozenset({"account", "holder"}),
    frozenset({"account", "number"}),
    frozenset({"account", "balance"}),
    frozenset({"invoice", "number"}),
    frozenset({"invoice", "date"}),
    frozenset({"date", "description"}),
    frozenset({"unit", "price"}),
    frozenset({"closing", "balance"}),
    frozenset({"opening", "balance"}),
    frozenset({"full", "name"}),
    frozenset({"first", "name"}),
    frozenset({"last", "name"}),
    frozenset({"date", "birth"}),
    frozenset({"date", "issue"}),
    frozenset({"date", "expiry"}),
    frozenset({"bank", "statement"}),
    frozenset({"bank", "name"}),
    frozenset({"bank", "transfer"}),
    frozenset({"tax", "number"}),
    frozenset({"id", "number"}),
    frozenset({"reference", "number"}),
    frozenset({"travel", "expenses"}),
    frozenset({"consulting", "services"}),
    frozenset({"direct", "deposit"}),
    frozenset({"atm", "withdrawal"}),
    frozenset({"application", "form"}),
    frozenset({"cardholder", "name"}),
    frozenset({"credit", "card"}),
    frozenset({"debit", "card"}),
    frozenset({"postal", "code"}),
    frozenset({"zip", "code"}),
    frozenset({"phone", "number"}),
    frozenset({"email", "address"}),
    frozenset({"date", "birth"}),
    frozenset({"birth", "date"}),
    frozenset({"identity", "card"}),
    frozenset({"national", "id"}),
    frozenset({"passport", "number"}),
    frozenset({"sort", "code"}),
    frozenset({"routing", "number"}),
}

# Individual words that are almost never part of a person name.
_WORD_BLOCKLIST: Set[str] = {
    "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",
    # Document field label words
    "account", "invoice", "balance", "subtotal", "total", "amount",
    "date", "number", "description", "quantity", "reference",
    "withdrawal", "deposit", "transfer", "payment", "transaction",
    "statement", "cardholder", "expiry", "issued", "issuer",
    "address", "street", "avenue", "road", "drive", "lane", "boulevard",
    "city", "country", "nationality", "gender", "occupation",
    "company", "corporation", "limited", "incorporated",
    "subject", "regards", "sincerely", "attachment",
}


def _preceded_by_title(text: str, match_start: int) -> bool:
    """Return True if the word immediately before the match is a title/salutation."""
    prefix = text[:match_start].rstrip()
    words = prefix.split()
    if not words:
        return False
    last_word = words[-1].rstrip(".,;:")
    return last_word.lower().rstrip(".") in _TITLES


def _is_blocklisted(first: str, second: str) -> bool:
    pair = frozenset({first.lower(), second.lower()})
    if pair in _BLOCKLIST:
        return True
    if first.lower() in _WORD_BLOCKLIST or second.lower() in _WORD_BLOCKLIST:
        return True
    return False


# Matches a title word followed by two capitalized words (the name).
# Uses [ \t]+ instead of \s+ to avoid crossing newlines.
_TITLE_NAME_RE = re.compile(
    r"(?i)\b(" + "|".join(re.escape(t) for t in sorted(_TITLES, key=len, reverse=True)) + r")\.?[ \t]+"
    r"([A-Z][a-z]+(?:-[A-Z][a-z]+)?)[ \t]+([A-Z][a-z]+(?:-[A-Z][a-z]+)?)\b"
)


def detect_named_entities(text: str, config: UPFConfig) -> List[Dict[str, Union[int, str]]]:
    if not config.redact_names:
        return []

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

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

    # Pass 1: title-triggered matches (highest confidence, works at sentence start)
    for match in _TITLE_NAME_RE.finditer(text):
        first_name_start = match.start(2)
        name_text = match.group(2) + " " + match.group(3)
        name_end = match.end(3)
        if not _is_blocklisted(match.group(2), match.group(3)):
            _add(first_name_start, name_end, name_text)

    # Pass 2: mid-sentence names not at sentence start
    for match in NAME_RE.finditer(text):
        first, second = match.group(1), match.group(2)

        if _is_blocklisted(first, second):
            continue

        match_start = match.start()
        preceding = text[:match_start]
        at_sentence_start = (
            not preceding.strip()
            or bool(re.search(r"[.!?]\s*$", preceding))
        )

        # If first word of the match is itself a title/salutation (e.g. "Hello Sarah
        # Connor"), treat the *second* word + next capitalized word as the name instead
        # of suppressing. We skip this pair and let the next iteration pick up the name.
        if first.lower().rstrip(".") in _TITLES:
            continue

        if at_sentence_start and not _preceded_by_title(text, match_start):
            continue

        _add(match_start, match.end(), match.group())

    return findings
