"""Entity name sanitization — the syntactic chokepoint for registry writes.

Called at the top of every ``EntityRegistry.add`` and
``EntityRegistry.resolve`` so no code path — regex, LLM, direct tool call,
system bridge, migration — can land junk in the graph.

All rules are syntactic and language-agnostic:

- Whitespace (including embedded newlines, carriage returns, tabs) is
  collapsed to a single space.
- Names beginning with ``#`` or ``>`` are rejected as residual markdown.
- Names containing no word characters at all (punctuation-only) are
  rejected. ``\\w`` is Unicode-aware so this accepts names in any script
  — Latin, Cyrillic, Greek, CJK, Arabic, Hebrew, etc.

There is no vocabulary check. A name like "User Journal" is valid English
and passes — which is correct, because the upstream extraction layer is
responsible for not generating it from a section header in the first place.
"""

from __future__ import annotations

import re

# Any run of whitespace (spaces, tabs, newlines, CR) becomes one space.
_WS_COLLAPSE = re.compile(r"\s+")

# A name must contain at least one word character in any script.
# ``\w`` is Unicode-aware by default in Python 3.
_HAS_WORD_CHAR = re.compile(r"\w", re.UNICODE)

# Detects "Title-Case head, lowercase descriptor" — the LLM-extraction
# error pattern that produces duplicates like "Erasmo Zuleta, gobernador
# de Córdoba" alongside a clean "Erasmo Zuleta" entity. The trigger is
# the FIRST CHARACTER after the comma + space being a Unicode lowercase
# letter; that's what distinguishes role/description suffixes from
# legitimate comma-bearing names like "University of California, Berkeley"
# (Berkeley is Title-Case → preserved) or "Smith, John" (John is Title-Case
# → preserved). ``\p{Ll}`` would be ideal but isn't available in stdlib re;
# we approximate with a conservative explicit lowercase set covering Latin
# + common European diacritics. CJK / Arabic / Hebrew don't have casing so
# they NEVER match — preserved by construction.
_COMMA_DESCRIPTOR = re.compile(
    # Head: at least one non-whitespace, no commas — so multi-comma input
    # fails the match cleanly (passthrough) rather than producing a partial
    # strip that would land another malformed name in the registry.
    r"^(?P<head>[^,]+?),\s+"
    # Tail: must start with a lowercase letter AND contain no further commas.
    r"(?P<tail>[a-zàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿœßšž][^,]*)$",
    re.UNICODE,
)


def strip_comma_descriptor(name: str) -> tuple[str, str]:
    """Trim a trailing ``", lowercase descriptor"`` from an entity name.

    Returns ``(clean_name, original_full)``. When no strip applies, returns
    ``(name, "")`` so callers can distinguish the no-op cleanly.

    Conservative by design — only fires when the text after the comma
    starts with a lowercase letter. This catches the LLM extraction
    error pattern (``"Stefan, deltagare som satt tyst"``) but preserves
    legitimate names where the comma separates two proper-noun parts
    (``"University of California, Berkeley"``, ``"Smith, John"``).
    """
    if not name or "," not in name:
        return name, ""
    m = _COMMA_DESCRIPTOR.match(name)
    if not m:
        return name, ""
    head = m.group("head").strip()
    if not head or not _HAS_WORD_CHAR.search(head):
        return name, ""
    return head, name


def sanitize_name(name: str) -> str:
    """Normalize an entity name and reject structural junk.

    Returns the cleaned name, or an empty string if the input is not a
    valid entity candidate. Callers that receive an empty string should
    no-op — the sanitizer has decided the name is unsafe and no entity
    should be created.

    Valid inputs are returned with:

    - Leading/trailing whitespace stripped
    - Internal whitespace runs collapsed to a single space
    - No embedded newlines, tabs, or CR characters

    Rejected inputs (return empty string):

    - Empty or ``None``-like values
    - Names beginning with ``#`` or ``>`` (residual markdown)
    - Names containing no word characters at all (punctuation-only)
    """
    if not name:
        return ""
    cleaned = _WS_COLLAPSE.sub(" ", name).strip()
    if not cleaned:
        return ""
    if cleaned.startswith("#") or cleaned.startswith(">"):
        return ""
    if not _HAS_WORD_CHAR.search(cleaned):
        return ""
    return cleaned
