"""macOS global hotkey manager using NSEvent global monitor.

Monitors keyboard events at the session level and fires callbacks
when registered key combinations are pressed.

Supports:
  - Standard combos: 'cmd+shift+space', 'ctrl+f9', etc.
  - Double-tap modifiers: 'shift+shift' (press Shift twice quickly)

Event flow
----------
1. NSEvent.addGlobalMonitorForEventsMatchingMask_handler_ installs a listener
2. All keyDown and flagsChanged events flow through our callback
3. Callback checks keycode + modifier flags against registered combos
4. On match → fires the Python callback in a daemon thread
"""

from __future__ import annotations

import logging
import threading
import time
from collections.abc import Callable

from .manager import HotkeyManager

log = logging.getLogger(__name__)

# ── CGEvent modifier masks ──────────────────────────────────────────────────
_CGShift = 0x00020000  # kCGEventFlagMaskShift
_CGControl = 0x00040000  # kCGEventFlagMaskControl
_CGOption = 0x00080000  # kCGEventFlagMaskAlternate
_CGCommand = 0x00100000  # kCGEventFlagMaskCommand

_MOD_MAP: dict[str, int] = {
    "cmd": _CGCommand,
    "command": _CGCommand,
    "shift": _CGShift,
    "option": _CGOption,
    "alt": _CGOption,
    "ctrl": _CGControl,
    "control": _CGControl,
}

# Common macOS key codes (US layout)
_KEYCODE_MAP: dict[str, int] = {
    "a": 0x00,
    "s": 0x01,
    "d": 0x02,
    "f": 0x03,
    "h": 0x04,
    "g": 0x05,
    "z": 0x06,
    "x": 0x07,
    "c": 0x08,
    "v": 0x09,
    "b": 0x0B,
    "q": 0x0C,
    "w": 0x0D,
    "e": 0x0E,
    "r": 0x0F,
    "y": 0x10,
    "t": 0x11,
    "1": 0x12,
    "2": 0x13,
    "3": 0x14,
    "4": 0x15,
    "6": 0x16,
    "5": 0x17,
    "=": 0x18,
    "9": 0x19,
    "7": 0x1A,
    "-": 0x1B,
    "8": 0x1C,
    "0": 0x1D,
    "]": 0x1E,
    "o": 0x1F,
    "u": 0x20,
    "[": 0x21,
    "i": 0x22,
    "p": 0x23,
    "enter": 0x24,
    "l": 0x25,
    "j": 0x26,
    "'": 0x27,
    "k": 0x28,
    ";": 0x29,
    "\\": 0x2A,
    ",": 0x2B,
    "/": 0x2C,
    "n": 0x2D,
    "m": 0x2E,
    ".": 0x2F,
    "tab": 0x30,
    "space": 0x31,
    "`": 0x32,
    "backspace": 0x33,
    "escape": 0x35,
    "f1": 0x7A,
    "f2": 0x78,
    "f3": 0x63,
    "f4": 0x76,
    "f5": 0x60,
    "f6": 0x61,
    "f7": 0x62,
    "f8": 0x64,
    "f9": 0x65,
    "f10": 0x6D,
    "f11": 0x67,
    "f12": 0x6F,
    "left": 0x7B,
    "right": 0x7C,
    "down": 0x7D,
    "up": 0x7E,
    "delete": 0x75,
    "home": 0x73,
    "end": 0x77,
    "pageup": 0x74,
    "pagedown": 0x79,
}

# Modifier keycodes for double-tap detection
_MODIFIER_KEYCODES: dict[str, set[int]] = {
    "shift": {0x38, 0x3C},  # Left Shift, Right Shift
    "ctrl": {0x3B, 0x3E},
    "control": {0x3B, 0x3E},
    "cmd": {0x37, 0x36},
    "command": {0x37, 0x36},
    "option": {0x3A, 0x3D},
    "alt": {0x3A, 0x3D},
}

# Double-tap window in seconds
_DOUBLE_TAP_INTERVAL = 0.4


def _parse_hotkey(hotkey: str) -> tuple[int, int] | tuple[str, str]:
    """Parse hotkey string.

    Returns (keyCode, modifierMask) for normal combos,
    or ('double', modifierName) for double-tap hotkeys like 'shift+shift'.
    """
    parts = [p.strip().lower() for p in hotkey.split("+")]

    # Detect double-tap: 'shift+shift', 'ctrl+ctrl', etc.
    if len(parts) == 2 and parts[0] == parts[1] and parts[0] in _MODIFIER_KEYCODES:
        return ("double", parts[0])

    modifiers = 0
    key_code = -1

    for part in parts:
        if part in _MOD_MAP:
            modifiers |= _MOD_MAP[part]
        elif part in _KEYCODE_MAP:
            key_code = _KEYCODE_MAP[part]
        elif len(part) == 1:
            key_code = _KEYCODE_MAP.get(part, -1)
            if key_code == -1:
                raise ValueError(f"Unknown key: {part!r}")
        else:
            raise ValueError(f"Unknown part: {part!r}")

    if key_code == -1 and modifiers != 0:
        # Modifier-only hotkey (e.g. "ctrl+shift") — key_code stays -1
        return -1, modifiers

    if key_code == -1:
        raise ValueError(f"No main key in hotkey: {hotkey!r}")

    return key_code, modifiers


class MacHotkeyManager(HotkeyManager):
    """macOS hotkey manager using NSEvent global monitor."""

    def __init__(self) -> None:
        self._lock = threading.Lock()
        # Normal hotkeys: {hotkey_str: (keyCode, modMask, callback)}
        self._hotkeys: dict[str, tuple[int, int, Callable]] = {}
        # Double-tap hotkeys: {hotkey_str: (modName, callback)}
        self._double_taps: dict[str, tuple[str, Callable]] = {}
        # Modifier-only hotkeys: {hotkey_str: (modMask, callback)}
        self._mod_only: dict[str, tuple[int, Callable]] = {}
        self._mod_only_armed: dict[str, bool] = {}
        # Double-tap state
        self._last_mod_up: dict[str, float] = {}  # modName → timestamp
        # NSEvent global monitor
        self._monitor = None
        self._on_event_handler = None
        self._running = False

    def start(self) -> None:
        """Install NSEvent global monitor for keyboard events.

        Uses NSEvent.addGlobalMonitorForEventsMatchingMask_handler_
        which works reliably with pystray's NSApplication run loop.
        """
        if self._running:
            return
        try:
            import AppKit

            # Monitor keyDown + flagsChanged events
            mask = AppKit.NSEventMaskKeyDown | AppKit.NSEventMaskFlagsChanged

            def _on_event(event):
                try:
                    event_type = event.type()
                    keycode = event.keyCode()
                    flags = event.modifierFlags()

                    # Convert NSEvent flags to CGEvent-style flags
                    active_mods = 0
                    if flags & AppKit.NSEventModifierFlagCommand:
                        active_mods |= _CGCommand
                    if flags & AppKit.NSEventModifierFlagShift:
                        active_mods |= _CGShift
                    if flags & AppKit.NSEventModifierFlagControl:
                        active_mods |= _CGControl
                    if flags & AppKit.NSEventModifierFlagOption:
                        active_mods |= _CGOption

                    is_flags_changed = event_type == AppKit.NSEventTypeFlagsChanged
                    self._handle_event(keycode, active_mods, is_flags_changed)
                except Exception as exc:
                    log.error("NSEvent monitor error: %s", exc)

            self._monitor = AppKit.NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(
                mask, _on_event
            )
            # Keep reference to prevent GC
            self._on_event_handler = _on_event

            if self._monitor:
                self._running = True
                log.info("NSEvent global monitor installed — listening for hotkeys")
            else:
                log.error("NSEvent global monitor failed — grant Input Monitoring permission")
        except ImportError:
            log.error("AppKit not available")
        except Exception as exc:
            log.error("Failed to install NSEvent monitor: %s", exc)

    def stop(self) -> None:
        """Remove the NSEvent monitor and clear all hotkeys."""
        if self._monitor:
            try:
                import AppKit

                AppKit.NSEvent.removeMonitor_(self._monitor)
            except Exception:
                pass
            self._monitor = None
            self._running = False
        with self._lock:
            self._hotkeys.clear()
            self._double_taps.clear()

    def register(self, hotkey: str, callback: Callable[[], None]) -> bool:
        """Register a hotkey combo or double-tap modifier."""
        try:
            parsed = _parse_hotkey(hotkey)
        except ValueError as exc:
            log.error("Invalid hotkey %r: %s", hotkey, exc)
            return False

        with self._lock:
            if isinstance(parsed[0], str) and parsed[0] == "double":
                mod_name = parsed[1]
                self._double_taps[hotkey] = (mod_name, callback)
                log.info("macOS hotkey registered (double-tap): %r", hotkey)
            else:
                key_code, mod_mask = parsed
                self._hotkeys[hotkey] = (key_code, mod_mask, callback)
                log.info("macOS hotkey registered: %r", hotkey)

        # Auto-start the event tap on first registration
        if not self._running:
            self.start()

        return True

    def unregister(self, hotkey: str) -> bool:
        """Remove a registered hotkey."""
        with self._lock:
            removed = (
                self._hotkeys.pop(hotkey, None)
                or self._double_taps.pop(hotkey, None)
                or self._mod_only.pop(hotkey, None)
            )
            self._mod_only_armed.pop(hotkey, None)
        return removed is not None

    # ── Event handling ──────────────────────────────────────────────────

    def _handle_event(self, keycode: int, active_mods: int, is_flags_changed: bool) -> None:
        """Process a keyboard event — match normal hotkeys or detect double-tap."""
        if not is_flags_changed:
            # keyDown — check normal hotkeys
            with self._lock:
                for hk_str, (kc, mod_mask, cb) in self._hotkeys.items():
                    if keycode == kc and active_mods == mod_mask:
                        log.debug("Hotkey matched: %s", hk_str)
                        threading.Thread(target=cb, daemon=True, name=f"hotkey-{hk_str}").start()
                        break
        else:
            # flagsChanged — check double-tap
            self._check_double_tap(keycode, active_mods)

    def _check_mod_only(self, active_mods: int) -> None:
        """Detect modifier-only combos (e.g. ctrl+shift held with no other keys)."""
        with self._lock:
            items = list(self._mod_only.items())
        for hk_str, (mod_mask, cb) in items:
            if active_mods == mod_mask:
                if self._mod_only_armed.get(hk_str, True):
                    self._mod_only_armed[hk_str] = False
                    log.debug("Modifier-only hotkey fired: %s", hk_str)
                    threading.Thread(target=cb, daemon=True).start()
            else:
                self._mod_only_armed[hk_str] = True

    def _check_double_tap(self, keycode: int, active_mods: int) -> None:
        """Detect double-tap of a modifier key (e.g. shift+shift)."""
        now = time.monotonic()

        with self._lock:
            double_taps = dict(self._double_taps)

        for hk_str, (mod_name, cb) in double_taps.items():
            mod_keycodes = _MODIFIER_KEYCODES.get(mod_name, set())
            if keycode not in mod_keycodes:
                continue

            # flagsChanged fires on both press AND release.
            # We want release (modifier flag cleared).
            mod_flag = _MOD_MAP.get(mod_name, 0)
            is_released = not (active_mods & mod_flag)

            if is_released:
                last = self._last_mod_up.get(mod_name, 0.0)
                if now - last < _DOUBLE_TAP_INTERVAL:
                    # Double-tap detected!
                    self._last_mod_up[mod_name] = 0.0  # reset
                    log.info("Double-tap detected: %s", hk_str)
                    threading.Thread(target=cb, daemon=True, name=f"hotkey-{hk_str}").start()
                else:
                    self._last_mod_up[mod_name] = now
