"""Shared Unicode normalization for Obol hook modules.

Single source of truth for text and path normalization. Consolidates the
four divergent implementations from claude_code.py (2 call sites), gate.py,
and detection.py into one canonical module.

All maps are module-level constants (built once, not per call).
Zero external dependencies -- only stdlib: unicodedata, re, html, urllib.parse.
"""

import html
import re
import unicodedata
from urllib.parse import unquote


# ---------------------------------------------------------------------------
# Latin small capitals  (21 chars)
# ---------------------------------------------------------------------------
_SMALL_CAPS = {
    "\u026a": "i",  # LATIN LETTER SMALL CAPITAL I
    "\u0262": "g",  # LATIN LETTER SMALL CAPITAL G
    "\u0274": "n",  # LATIN LETTER SMALL CAPITAL N
    "\u1d0f": "o",  # LATIN LETTER SMALL CAPITAL O
    "\u0280": "r",  # LATIN LETTER SMALL CAPITAL R
    "\u1d07": "e",  # LATIN LETTER SMALL CAPITAL E
    "\u1d00": "a",  # LATIN LETTER SMALL CAPITAL A
    "\u0299": "b",  # LATIN LETTER SMALL CAPITAL B
    "\u1d04": "c",  # LATIN LETTER SMALL CAPITAL C
    "\u1d05": "d",  # LATIN LETTER SMALL CAPITAL D
    "\u029c": "h",  # LATIN LETTER SMALL CAPITAL H
    "\u1d0b": "k",  # LATIN LETTER SMALL CAPITAL K
    "\u029f": "l",  # LATIN LETTER SMALL CAPITAL L
    "\u1d0d": "m",  # LATIN LETTER SMALL CAPITAL M
    "\u1d18": "p",  # LATIN LETTER SMALL CAPITAL P
    "\u1d1b": "t",  # LATIN LETTER SMALL CAPITAL T
    "\u1d1c": "u",  # LATIN LETTER SMALL CAPITAL U
    "\u1d20": "v",  # LATIN LETTER SMALL CAPITAL V
    "\u1d21": "w",  # LATIN LETTER SMALL CAPITAL W
    "\u028f": "y",  # LATIN LETTER SMALL CAPITAL Y
    "\u1d22": "z",  # LATIN LETTER SMALL CAPITAL Z
}

# ---------------------------------------------------------------------------
# Cyrillic homoglyphs  (12 chars)
# ---------------------------------------------------------------------------
_CYRILLIC = {
    "\u0430": "a",  # CYRILLIC SMALL LETTER A
    "\u0435": "e",  # CYRILLIC SMALL LETTER IE
    "\u043e": "o",  # CYRILLIC SMALL LETTER O
    "\u0440": "p",  # CYRILLIC SMALL LETTER ER
    "\u0441": "c",  # CYRILLIC SMALL LETTER ES
    "\u0445": "x",  # CYRILLIC SMALL LETTER HA
    "\u0455": "s",  # CYRILLIC SMALL LETTER DZE
    "\u0456": "i",  # CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
    "\u0443": "y",  # CYRILLIC SMALL LETTER U
    "\u04bb": "h",  # CYRILLIC SMALL LETTER SHHA
    "\u0460": "o",  # CYRILLIC CAPITAL LETTER OMEGA
    "\u0462": "e",  # CYRILLIC CAPITAL LETTER YAT
}

# ---------------------------------------------------------------------------
# Greek homoglyphs  (2 chars)
# ---------------------------------------------------------------------------
_GREEK = {
    "\u03bf": "o",  # GREEK SMALL LETTER OMICRON
    "\u03b1": "a",  # GREEK SMALL LETTER ALPHA
}

# ---------------------------------------------------------------------------
# CONFUSABLE_MAP — union of all three sets, for str.translate()
# ---------------------------------------------------------------------------
CONFUSABLE_MAP = str.maketrans({**_SMALL_CAPS, **_CYRILLIC, **_GREEK})

# ---------------------------------------------------------------------------
# PATH_CONFUSABLES — path-specific confusables applied via str.replace loop.
# These map multi-category punctuation that str.maketrans can't cleanly hold
# alongside the letter confusables (detection.py's _normalize used a dict).
# ---------------------------------------------------------------------------
PATH_CONFUSABLES = {
    # Slash confusables
    "\u2215": "/",  # DIVISION SLASH
    "\uff0f": "/",  # FULLWIDTH SOLIDUS
    "\u2044": "/",  # FRACTION SLASH
    "\u29f8": "/",  # BIG SOLIDUS
    # Period confusables
    "\u2024": ".",  # ONE DOT LEADER
    "\uff0e": ".",  # FULLWIDTH FULL STOP
    "\u2219": ".",  # BULLET OPERATOR
    "\u22c5": ".",  # DOT OPERATOR
    # Tilde confusables
    "\uff5e": "~",  # FULLWIDTH TILDE
    "\u223c": "~",  # TILDE OPERATOR
    # Angle bracket confusables
    "\uff1c": "<",  # FULLWIDTH LESS-THAN SIGN
    "\uff1e": ">",  # FULLWIDTH GREATER-THAN SIGN
    "\u2039": "<",  # SINGLE LEFT-POINTING ANGLE QUOTATION MARK
    "\u203a": ">",  # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
    "\u276e": "<",  # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT
    "\u276f": ">",  # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT
    # Latin letter confusable
    "\ua731": "s",  # LATIN SMALL LETTER S (capital lookalike)
    # Equals sign confusable
    "\uff1d": "=",  # FULLWIDTH EQUALS SIGN
}

# ---------------------------------------------------------------------------
# INVISIBLE_CHARS — zero-width, bidi-override, and formatting characters
# ---------------------------------------------------------------------------
INVISIBLE_CHARS = (
    "\u200b"  # ZERO WIDTH SPACE
    "\u200c"  # ZERO WIDTH NON-JOINER
    "\u200d"  # ZERO WIDTH JOINER
    "\ufeff"  # ZERO WIDTH NO-BREAK SPACE (BOM)
    "\u200e"  # LEFT-TO-RIGHT MARK
    "\u200f"  # RIGHT-TO-LEFT MARK
    "\u2060"  # WORD JOINER
    "\u00ad"  # SOFT HYPHEN
    "\u202a"  # LEFT-TO-RIGHT EMBEDDING
    "\u202b"  # RIGHT-TO-LEFT EMBEDDING
    "\u202c"  # POP DIRECTIONAL FORMATTING
    "\u202d"  # LEFT-TO-RIGHT OVERRIDE
    "\u202e"  # RIGHT-TO-LEFT OVERRIDE
)

# ---------------------------------------------------------------------------
# LEET_MAP — common leetspeak substitutions, for str.translate()
# ---------------------------------------------------------------------------
LEET_MAP = str.maketrans({
    "0": "o",
    "1": "i",
    "3": "e",
    "4": "a",
    "5": "s",
    "7": "t",
    "@": "a",
    "$": "s",
})

# ---------------------------------------------------------------------------
# TAG_CHAR_RE — Unicode tag characters (U+E0000 .. U+E007F)
# ---------------------------------------------------------------------------
TAG_CHAR_RE = re.compile(r'[\U000e0000-\U000e007f]')


# ---------------------------------------------------------------------------
# Public functions
# ---------------------------------------------------------------------------

def normalize_text(text: str) -> str:
    """Normalize text for injection / obfuscation detection.

    Expects *already-lowercased* input.

    Pipeline:
        NFD decompose -> strip combining marks (Mn) -> NFKC ->
        translate CONFUSABLE_MAP -> strip INVISIBLE_CHARS -> remove tag chars
    """
    # 1. NFD decompose (split precomposed chars: e + combining accent)
    text = unicodedata.normalize("NFD", text)
    # 2. Strip combining marks (Mn) and modifier letters (Lm)
    #    Mn = diacritics/accents, Lm = modifier letters that can hide characters
    text = "".join(c for c in text if unicodedata.category(c) not in ("Mn", "Lm"))
    # 3. NFKC normalize (collapse math/styled/fullwidth to ASCII)
    text = unicodedata.normalize("NFKC", text)
    # 4. Translate confusable characters to ASCII equivalents
    text = text.translate(CONFUSABLE_MAP)
    # 5. Strip invisible / zero-width / bidi characters
    for ch in INVISIBLE_CHARS:
        text = text.replace(ch, "")
    # 6. Remove Unicode tag characters
    text = TAG_CHAR_RE.sub("", text)
    # 7. Strip null bytes
    text = text.replace("\x00", "")
    return text


def normalize_path(target: str) -> str:
    """Normalize a file path or target string for sensitive-path detection.

    Does NOT lowercase -- callers decide case handling.

    Pipeline:
        NFKC -> NFD decompose -> strip combining marks (Mn) -> NFKC ->
        HTML unescape -> URL decode (2 rounds) -> strip bidi chars ->
        backslash -> forward slash -> strip quotes ->
        apply PATH_CONFUSABLES -> translate CONFUSABLE_MAP ->
        strip INVISIBLE_CHARS -> remove tag chars
    """
    # 1. NFKC first pass (fullwidth, math alphanumerics, compatibility)
    target = unicodedata.normalize("NFKC", target)
    # 2. NFD decompose -> strip combining marks (Mn) and modifier letters (Lm) -> NFKC recompose
    target = unicodedata.normalize("NFD", target)
    target = "".join(c for c in target if unicodedata.category(c) not in ("Mn", "Lm"))
    target = unicodedata.normalize("NFKC", target)
    # 3. HTML entity decode
    target = html.unescape(target)
    # 4. URL-decode %XX sequences (2 rounds for double-encoding)
    target = unquote(unquote(target))
    # 5. Strip bidi override / directional / Arabic letter mark characters
    for bidi in (
        "\u202a", "\u202b", "\u202c", "\u202d", "\u202e",
        "\u2066", "\u2067", "\u2068", "\u2069",
        "\u200e", "\u200f",
        "\u061c",
    ):
        target = target.replace(bidi, "")
    # 6. Normalize backslashes to forward slashes
    target = target.replace("\\", "/")
    # 7. Strip quote characters
    target = target.replace("'", "").replace('"', "").replace("`", "")
    # 8. Path-specific confusables (slash, period, tilde, angle brackets, etc.)
    for char, replacement in PATH_CONFUSABLES.items():
        target = target.replace(char, replacement)
    # 9. Letter confusables (small caps, Cyrillic, Greek)
    target = target.translate(CONFUSABLE_MAP)
    # 10. Strip invisible / zero-width characters
    for ch in INVISIBLE_CHARS:
        target = target.replace(ch, "")
    # 11. Remove Unicode tag characters
    target = TAG_CHAR_RE.sub("", target)
    # 12. Strip null bytes (prevent path truncation attacks)
    target = target.replace("\x00", "")
    # 13. Collapse redundant /./ path components (path traversal evasion)
    while "/./" in target:
        target = target.replace("/./", "/")
    # 14. Collapse /../ path traversal (e.g. /var/run/../run/ → /run/)
    while "/../" in target:
        idx = target.find("/../")
        if idx <= 0:
            break
        parent = target.rfind("/", 0, idx)
        if parent < 0:
            break
        target = target[:parent] + target[idx + 3:]
    return target


def deleet(text: str) -> str:
    """Decode common leetspeak substitutions.

    Expects already-normalized text (output of normalize_text).
    """
    return text.translate(LEET_MAP)
