"""Unicode-aware Title Case entity candidate extractor.

This is the *hint producer* for the fast entity-extraction path — a cheap
Unicode-aware scanner that surfaces likely person/place/project names for
the real graph to confirm. It is not authoritative and is never the sole
path for entity creation: the multilingual LLM triple extractor runs in
parallel on longer messages and catches what this misses.

Guarantees:

- Input is stripped of markdown structure before scanning (via
  :func:`agntspace.text.markdown.strip_markdown_structure`), so headers,
  bullets, and fences can never donate candidates.
- No candidate spans a line break — the regex uses ``[ \\t]+`` rather
  than ``\\s+`` so inter-word separators are strictly same-line.
- Candidates are whitespace-normalized and de-duplicated.
- Works on any cased script (Latin, Cyrillic, Greek, Armenian, …).
  Scripts without case distinction (CJK, Arabic, Hebrew) are handled by
  the LLM path and return no candidates from this function — which is
  correct, because a capitalization heuristic is meaningless there.
- Nothing is returned for empty or very short input.
"""

from __future__ import annotations

import re

from agntspace.text.markdown import strip_markdown_structure

# Word tokenizer — ``\w`` is Unicode-aware in Python 3 by default, so it
# matches letters in every cased script (Latin, Cyrillic, Greek, Armenian,
# Latin-extended, …). We scan token-by-token rather than greedy-matching
# a run, so a lowercase word breaking the run doesn't invalidate the
# Title Case words that came before it.
_WORD = re.compile(r"\w+", re.UNICODE)

# Punctuation that can wrap a word without being part of the word itself.
# Stripped from token edges before case analysis.
_EDGE_PUNCT = ".,;:!?\"'`()[]{}«»„"


def _is_title_case_word(token: str) -> bool:
    """Return True if *token* looks like a proper-noun word in any script.

    Accepts any word whose first character is an uppercase letter and whose
    remaining characters are all lowercase letters. Uses Python's built-in
    Unicode case methods so Latin, Cyrillic, Greek, Armenian, and other
    cased scripts all work. Non-cased scripts (CJK, Arabic, Hebrew) return
    False by construction — and the LLM path handles those.
    """
    if len(token) < 2:
        return False
    first = token[0]
    if not first.isupper() or not first.isalpha():
        return False
    rest = token[1:]
    return rest.isalpha() and rest.islower()


def extract_entity_candidates(text: str) -> list[str]:
    """Return multi-word Title Case phrases from cleaned text.

    Scans line-by-line and emits maximal runs of 2+ consecutive Title
    Case words separated only by spaces/tabs. ``"Sarah Chen met John
    Smith"`` yields ``["Sarah Chen", "John Smith"]`` — the lowercase
    ``met`` breaks the first run. See module docstring for full
    guarantees. Intended usage::

        from agntspace.text import extract_entity_candidates
        for name in extract_entity_candidates(message):
            graph.add_entity(name, ...)
    """
    if not text or len(text) < 4:
        return []
    scan = strip_markdown_structure(text)
    out: list[str] = []
    seen: set[str] = set()

    def _emit(run: list[str]) -> None:
        if len(run) < 2:
            return
        name = " ".join(run)
        if "\n" in name or name in seen:
            return
        seen.add(name)
        out.append(name)

    for line in scan.splitlines():
        run: list[str] = []
        last_end = 0
        for m in _WORD.finditer(line):
            token = m.group(0).strip(_EDGE_PUNCT)
            # A gap between the previous token and this one must contain
            # only spaces/tabs to keep the run alive — any punctuation or
            # digit in the gap breaks adjacency.
            gap = line[last_end:m.start()]
            last_end = m.end()
            if run and (not gap or any(c not in " \t" for c in gap)):
                _emit(run)
                run = []
            if _is_title_case_word(token):
                run.append(token)
            else:
                _emit(run)
                run = []
        _emit(run)
    return out
