"""TUI key parsing — parse raw terminal input into key identifiers."""

from __future__ import annotations

import re
from typing import Any

# ---------------------------------------------------------------------------
# Codepoint constants
# ---------------------------------------------------------------------------

CODEPOINTS: dict[str, int] = {
    "escape": 27,
    "tab": 9,
    "enter": 13,
    "space": 32,
    "backspace": 127,
    "kpEnter": 57414,
}

ARROW_CODEPOINTS: dict[str, int] = {
    "up": -1,
    "down": -2,
    "right": -3,
    "left": -4,
}

FUNCTIONAL_CODEPOINTS: dict[str, int] = {
    "delete": -10,
    "insert": -11,
    "pageUp": -12,
    "pageDown": -13,
    "home": -14,
    "end": -15,
}

ALL_CODEPOINTS: dict[str, int] = {**CODEPOINTS, **ARROW_CODEPOINTS, **FUNCTIONAL_CODEPOINTS}

# Reverse map: codepoint -> key name
_CODEPOINT_TO_NAME: dict[int, str] = {v: k for k, v in ALL_CODEPOINTS.items()}

# Modifier bitmasks
SHIFT = 1
ALT = 2
CTRL = 4
LOCK_MASK = 192  # Caps Lock + Num Lock

# Kitty protocol state
_kitty_active = False


def set_kitty_protocol_active(active: bool) -> None:
    """Set Kitty keyboard protocol active state."""
    global _kitty_active
    _kitty_active = active


def is_kitty_protocol_active() -> bool:
    """Query Kitty keyboard protocol active state."""
    return _kitty_active


# ---------------------------------------------------------------------------
# Key name aliases
# ---------------------------------------------------------------------------

_KEY_ALIASES: dict[str, str] = {
    "esc": "escape",
    "return": "enter",
}

# Symbol keys
_SYMBOL_KEYS: dict[str, int] = {
    "`": ord("`"), "-": ord("-"), "=": ord("="),
    "[": ord("["), "]": ord("]"), "\\": ord("\\"),
    ";": ord(";"), "'": ord("'"), ",": ord(","),
    ".": ord("."), "/": ord("/"),
    "!": ord("!"), "@": ord("@"), "#": ord("#"),
    "$": ord("$"), "%": ord("%"), "^": ord("^"),
    "&": ord("&"), "*": ord("*"), "(": ord("("),
    ")": ord(")"), "_": ord("_"), "+": ord("+"),
    "|": ord("|"), "~": ord("~"), "{": ord("{"),
    "}": ord("}"), ":": ord(":"), "<": ord("<"),
    ">": ord(">"), "?": ord("?"),
}

# Function key codepoints (Kitty protocol)
_F_KEY_CODEPOINTS: dict[str, int] = {}
for i in range(1, 13):
    _F_KEY_CODEPOINTS[f"f{i}"] = 57364 + i - 1  # F1=57364 per Kitty spec

# Legacy escape sequences
_LEGACY_ARROWS: dict[str, str] = {
    "\x1b[A": "up", "\x1bOA": "up",
    "\x1b[B": "down", "\x1bOB": "down",
    "\x1b[C": "right", "\x1bOC": "right",
    "\x1b[D": "left", "\x1bOD": "left",
}

_LEGACY_SPECIAL: dict[str, str] = {
    "\x1b[3~": "delete", "\x1b[2~": "insert",
    "\x1b[5~": "pageUp", "\x1b[6~": "pageDown",
    "\x1b[H": "home", "\x1bOH": "home", "\x1b[1~": "home",
    "\x1b[F": "end", "\x1bOF": "end", "\x1b[4~": "end",
}

_LEGACY_FKEYS: dict[str, str] = {
    "\x1bOP": "f1", "\x1b[11~": "f1",
    "\x1bOQ": "f2", "\x1b[12~": "f2",
    "\x1bOR": "f3", "\x1b[13~": "f3",
    "\x1bOS": "f4", "\x1b[14~": "f4",
    "\x1b[15~": "f5",
    "\x1b[17~": "f6",
    "\x1b[18~": "f7",
    "\x1b[19~": "f8",
    "\x1b[20~": "f9",
    "\x1b[21~": "f10",
    "\x1b[23~": "f11",
    "\x1b[24~": "f12",
}

_LEGACY_SHIFT_ARROWS: dict[str, str] = {
    "\x1b[1;2A": "shift+up", "\x1b[1;2B": "shift+down",
    "\x1b[1;2C": "shift+right", "\x1b[1;2D": "shift+left",
}

_LEGACY_CTRL_ARROWS: dict[str, str] = {
    "\x1b[1;5A": "ctrl+up", "\x1b[1;5B": "ctrl+down",
    "\x1b[1;5C": "ctrl+right", "\x1b[1;5D": "ctrl+left",
}

_LEGACY_ALT_ARROWS: dict[str, str] = {
    "\x1b[1;3A": "alt+up", "\x1b[1;3B": "alt+down",
    "\x1b[1;3C": "alt+right", "\x1b[1;3D": "alt+left",
}

# Combine all legacy sequences
_ALL_LEGACY: dict[str, str] = {
    **_LEGACY_ARROWS, **_LEGACY_SPECIAL, **_LEGACY_FKEYS,
    **_LEGACY_SHIFT_ARROWS, **_LEGACY_CTRL_ARROWS, **_LEGACY_ALT_ARROWS,
}

# Kitty CSI-u regex: ESC [ codepoint (;modifier(:shifted_key)?)? u
_KITTY_CSI_U = re.compile(r"\x1b\[(\d+)(?:;(\d+)(?::(\d+))?)?u")

# CSI with modifier: ESC [ 1 ; modifier <final>
_CSI_MODIFIED = re.compile(r"\x1b\[1;(\d+)([A-HF~])")

# CSI tilde: ESC [ number ; modifier ~
_CSI_TILDE = re.compile(r"\x1b\[(\d+);(\d+)~")


def _modifier_string(mods: int) -> str:
    """Build modifier prefix string from bitmask."""
    parts: list[str] = []
    if mods & CTRL:
        parts.append("ctrl")
    if mods & ALT:
        parts.append("alt")
    if mods & SHIFT:
        parts.append("shift")
    return "+".join(parts) + "+" if parts else ""


def _codepoint_to_key(cp: int) -> str | None:
    """Convert codepoint to key name."""
    if cp in _CODEPOINT_TO_NAME:
        return _CODEPOINT_TO_NAME[cp]
    # F-keys
    for name, fcp in _F_KEY_CODEPOINTS.items():
        if cp == fcp:
            return name
    # Printable
    if 32 <= cp <= 126:
        return chr(cp).lower()
    return None


def _final_to_key(final: str) -> str | None:
    """Convert CSI final byte to key name."""
    mapping = {"A": "up", "B": "down", "C": "right", "D": "left", "H": "home", "F": "end"}
    return mapping.get(final)


def _tilde_to_key(num: int) -> str | None:
    """Convert CSI tilde number to key name."""
    mapping = {
        2: "insert", 3: "delete", 5: "pageUp", 6: "pageDown",
        1: "home", 4: "end",
        11: "f1", 12: "f2", 13: "f3", 14: "f4", 15: "f5",
        17: "f6", 18: "f7", 19: "f8", 20: "f9", 21: "f10",
        23: "f11", 24: "f12",
    }
    return mapping.get(num)


def parse_key(data: str) -> str | None:
    """Parse raw keyboard input into a key identifier string.

    Returns key identifiers like: "escape", "ctrl+c", "shift+tab", "alt+left".
    Returns None if the input cannot be parsed.
    """
    if not data:
        return None

    # Kitty CSI-u format
    m = _KITTY_CSI_U.match(data)
    if m:
        cp = int(m.group(1))
        mods_raw = int(m.group(2)) - 1 if m.group(2) else 0
        mods = mods_raw & ~LOCK_MASK
        key = _codepoint_to_key(cp)
        if key is None:
            return None
        prefix = _modifier_string(mods)
        return f"{prefix}{key}"

    # CSI modified arrow/special
    m = _CSI_MODIFIED.match(data)
    if m:
        mods = int(m.group(1)) - 1
        mods = mods & ~LOCK_MASK
        key = _final_to_key(m.group(2))
        if key is None:
            return None
        prefix = _modifier_string(mods)
        return f"{prefix}{key}"

    # CSI tilde with modifier
    m = _CSI_TILDE.match(data)
    if m:
        num = int(m.group(1))
        mods = int(m.group(2)) - 1
        mods = mods & ~LOCK_MASK
        key = _tilde_to_key(num)
        if key is None:
            return None
        prefix = _modifier_string(mods)
        return f"{prefix}{key}"

    # Legacy escape sequences
    if data in _ALL_LEGACY:
        return _ALL_LEGACY[data]

    # Single escape
    if data == "\x1b":
        return "escape"

    # Alt + letter (ESC followed by printable)
    if len(data) == 2 and data[0] == "\x1b" and 32 <= ord(data[1]) <= 126:
        return f"alt+{data[1].lower()}"

    # Ctrl + letter (0x01-0x1A → ctrl+a through ctrl+z)
    if len(data) == 1:
        cp = ord(data)
        if 1 <= cp <= 26:
            return f"ctrl+{chr(cp + 96)}"
        if cp == 0:
            return "ctrl+space"
        if cp == 27:
            return "escape"
        if cp == 9:
            return "tab"
        if cp == 13 or cp == 10:
            return "enter"
        if cp == 127:
            return "backspace"
        if cp == 32:
            return "space"
        if 32 <= cp <= 126:
            return data.lower()

    return None


def matches_key(data: str, key_id: str) -> bool:
    """Check if raw input data matches a specific key identifier."""
    parsed = parse_key(data)
    if parsed is None:
        return False

    # Normalize the key_id
    normalized = key_id.lower().strip()
    normalized = _KEY_ALIASES.get(normalized, normalized)

    return parsed == normalized


def is_key_release(data: str) -> bool:
    """Detect if input is a key release event (Kitty protocol flag 3)."""
    if not data or len(data) < 3:
        return False
    # Avoid bracketed paste markers
    if "\x1b[200~" in data or "\x1b[201~" in data:
        return False
    # Look for :3 followed by u, ~, or A-F/H
    return bool(re.search(r":3[u~A-HF]", data))


def is_key_repeat(data: str) -> bool:
    """Detect if input is a key repeat event (Kitty protocol flag 2)."""
    if not data or len(data) < 3:
        return False
    if "\x1b[200~" in data or "\x1b[201~" in data:
        return False
    return bool(re.search(r":2[u~A-HF]", data))


def decode_kitty_printable(data: str) -> str | None:
    """Extract printable characters from Kitty CSI-u sequences.

    Returns the character to insert, or None if not a printable key.
    """
    m = _KITTY_CSI_U.match(data)
    if not m:
        return None

    cp = int(m.group(1))
    mods_raw = int(m.group(2)) - 1 if m.group(2) else 0
    mods = mods_raw & ~LOCK_MASK
    shifted_cp = int(m.group(3)) if m.group(3) else None

    # Reject if Ctrl or Alt modifier (only plain or Shift accepted)
    if mods & (CTRL | ALT):
        return None

    # If Shift is held and shifted keycode is available, use it
    if (mods & SHIFT) and shifted_cp and shifted_cp >= 32:
        return chr(shifted_cp)

    # Plain printable
    if cp >= 32 and cp != 127:
        return chr(cp)

    return None


# ---------------------------------------------------------------------------
# Default keybindings
# ---------------------------------------------------------------------------

DEFAULT_KEYBINDINGS: dict[str, str | list[str]] = {
    "cursorUp": "up",
    "cursorDown": "down",
    "cursorLeft": ["left", "ctrl+b"],
    "cursorRight": ["right", "ctrl+f"],
    "cursorWordLeft": ["alt+left", "alt+b"],
    "cursorWordRight": ["alt+right", "alt+f"],
    "cursorLineStart": ["home", "ctrl+a"],
    "cursorLineEnd": ["end", "ctrl+e"],
    "deleteCharBackward": "backspace",
    "deleteCharForward": "delete",
    "deleteWordBackward": ["alt+backspace", "ctrl+w"],
    "deleteWordForward": "alt+delete",
    "deleteToLineStart": "ctrl+u",
    "deleteToLineEnd": "ctrl+k",
    "newLine": "shift+enter",
    "submit": "enter",
    "tab": "tab",
    "selectUp": "up",
    "selectDown": "down",
    "selectPageUp": "pageUp",
    "selectPageDown": "pageDown",
    "selectConfirm": "enter",
    "selectCancel": ["escape", "ctrl+c"],
    "copy": "ctrl+c",
    "yank": "ctrl+y",
    "yankPop": "alt+y",
}


class EditorKeybindingsManager:
    """Manages editor keybindings with configurable key mappings."""

    def __init__(self) -> None:
        self._config: dict[str, str | list[str]] = dict(DEFAULT_KEYBINDINGS)

    def matches(self, data: str, action: str) -> bool:
        """Check if raw input matches an editor action."""
        keys = self._config.get(action)
        if keys is None:
            return False
        if isinstance(keys, str):
            keys = [keys]
        return any(matches_key(data, k) for k in keys)

    def get_keys(self, action: str) -> list[str]:
        """Get key bindings for an action."""
        keys = self._config.get(action, [])
        if isinstance(keys, str):
            return [keys]
        return list(keys)

    def set_config(self, config: dict[str, str | list[str]]) -> None:
        """Update keybinding config."""
        self._config = {**DEFAULT_KEYBINDINGS, **config}

    def get_all(self) -> dict[str, str | list[str]]:
        """Get all keybindings."""
        return dict(self._config)


# Global keybindings manager
_keybindings_manager = EditorKeybindingsManager()


def get_keybindings_manager() -> EditorKeybindingsManager:
    """Get the global keybindings manager."""
    return _keybindings_manager
