"""Windows global hotkey manager — multi-strategy input detection.

Strategies (tried in order, all run simultaneously):
1. WH_KEYBOARD_LL hook — installed AFTER exam software. Hooks are
   processed LIFO: last installed = called first, so our hook sees
   keystrokes BEFORE ksHook.exe can consume them.
2. Raw Input API — reads from HID driver, below win32k.sys hooks.
3. GetAsyncKeyState polling (win32u → user32 fallback).

Trade-off: ~0.5% CPU on one core.
"""

from __future__ import annotations

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

from .manager import HotkeyManager

log = logging.getLogger(__name__)

_user32 = ctypes.windll.user32

# Fix 64-bit Windows: LRESULT/LPARAM are pointer-sized (8 bytes),
# but ctypes defaults to c_int (4 bytes) causing OverflowError.
_user32.CallNextHookEx.restype = ctypes.wintypes.LPARAM
_user32.CallNextHookEx.argtypes = [
    ctypes.wintypes.HHOOK,
    ctypes.c_int,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM,
]
_user32.DefWindowProcW.restype = ctypes.wintypes.LPARAM
_user32.DefWindowProcW.argtypes = [
    ctypes.wintypes.HWND,
    ctypes.c_uint,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM,
]

# Try win32u for direct NT syscall
try:
    _win32u = ctypes.windll.win32u
    _has_win32u = hasattr(_win32u, "NtUserGetAsyncKeyState")
except (OSError, AttributeError):
    _win32u = None
    _has_win32u = False

_KEY_PRESSED = 0x8000

# ------------------------------------------------------------------ #
# Virtual key code tables
# ------------------------------------------------------------------ #

_VK_MAP: dict[str, list[int]] = {
    "space": [0x20],
    "enter": [0x0D],
    "tab": [0x09],
    "escape": [0x1B],
    "backspace": [0x08],
    "delete": [0x2E],
    "insert": [0x2D],
    "home": [0x24],
    "end": [0x23],
    "pageup": [0x21],
    "pagedown": [0x22],
    "left": [0x25],
    "up": [0x26],
    "right": [0x27],
    "down": [0x28],
    "f1": [0x70],
    "f2": [0x71],
    "f3": [0x72],
    "f4": [0x73],
    "f5": [0x74],
    "f6": [0x75],
    "f7": [0x76],
    "f8": [0x77],
    "f9": [0x78],
    "f10": [0x79],
    "f11": [0x7A],
    "f12": [0x7B],
    "/": [0xBF],
    "oem_2": [0xBF],
    "`": [0xC0],
    "\\": [0xDC],
}

_MOD_VK_MAP: dict[str, list[int]] = {
    "ctrl": [0x11],
    "control": [0x11],
    "lctrl": [0xA2],
    "leftctrl": [0xA2],
    "rctrl": [0xA3],
    "rightctrl": [0xA3],
    "shift": [0x10],
    "lshift": [0xA0],
    "leftshift": [0xA0],
    "rshift": [0xA1],
    "rightshift": [0xA1],
    "alt": [0x12],
    "ralt": [0xA5],
    "rightalt": [0xA5],
    "lalt": [0xA4],
    "leftalt": [0xA4],
    "win": [0x5B, 0x5C],
    "windows": [0x5B, 0x5C],
    "cmd": [0x11],
    "command": [0x11],
    "option": [0x12],
}

_DEBOUNCE_POLLS = 1  # re-arm after 1 poll cycle (was 3 — caused missed keypresses)
_POLL_INTERVAL = 0.02

_DOUBLE_TAP_INTERVAL = 0.4

_MOD_SINGLE_VK: dict[str, int] = {
    "shift": 0x10,
    "ctrl": 0x11,
    "control": 0x11,
    "alt": 0x12,
}

# All modifier VK codes for filtering
_ALL_MOD_VKS = {0x10, 0x11, 0x12, 0x5B, 0x5C, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}

# ------------------------------------------------------------------ #
# WH_KEYBOARD_LL hook constants
# ------------------------------------------------------------------ #
WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
WM_SYSKEYDOWN = 0x0104
WM_KEYUP = 0x0101
WM_SYSKEYUP = 0x0105

class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ("vkCode", ctypes.wintypes.DWORD),
        ("scanCode", ctypes.wintypes.DWORD),
        ("flags", ctypes.wintypes.DWORD),
        ("time", ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
    ]

HOOKPROC = ctypes.CFUNCTYPE(
    ctypes.wintypes.LPARAM,  # LRESULT — pointer-sized on 64-bit Windows
    ctypes.c_int,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM,
)

# ------------------------------------------------------------------ #
# Raw Input constants
# ------------------------------------------------------------------ #
WM_INPUT = 0x00FF
RID_INPUT = 0x10000003
RIM_TYPEKEYBOARD = 1
RIDEV_INPUTSINK = 0x00000100

class RAWINPUTDEVICE(ctypes.Structure):
    _fields_ = [
        ("usUsagePage", ctypes.c_ushort),
        ("usUsage", ctypes.c_ushort),
        ("dwFlags", ctypes.wintypes.DWORD),
        ("hwndTarget", ctypes.wintypes.HWND),
    ]

class RAWINPUTHEADER(ctypes.Structure):
    _fields_ = [
        ("dwType", ctypes.wintypes.DWORD),
        ("dwSize", ctypes.wintypes.DWORD),
        ("hDevice", ctypes.wintypes.HANDLE),
        ("wParam", ctypes.wintypes.WPARAM),
    ]

class RAWKEYBOARD(ctypes.Structure):
    _fields_ = [
        ("MakeCode", ctypes.c_ushort),
        ("Flags", ctypes.c_ushort),
        ("Reserved", ctypes.c_ushort),
        ("VKey", ctypes.c_ushort),
        ("Message", ctypes.c_uint),
        ("ExtraInformation", ctypes.c_ulong),
    ]

class RAWINPUT_KEYBOARD(ctypes.Structure):
    _fields_ = [
        ("header", RAWINPUTHEADER),
        ("keyboard", RAWKEYBOARD),
    ]


def _parse_hotkey(hotkey: str) -> tuple[list[int], list[int]] | tuple[str, str]:
    """Parse 'ctrl+shift+space' → (modifier_vks, main_vks)."""
    parts = [p.strip().lower() for p in hotkey.split("+")]

    if len(parts) == 2 and parts[0] == parts[1] and parts[0] in _MOD_SINGLE_VK:
        return ("double", parts[0])

    mod_vks: list[int] = []
    main_vks: list[int] = []

    for part in parts:
        if part in _MOD_VK_MAP:
            mod_vks.extend(_MOD_VK_MAP[part])
        elif part in _VK_MAP:
            main_vks.extend(_VK_MAP[part])
        elif len(part) == 1 and part.isalpha():
            main_vks.append(ord(part.upper()))
        elif len(part) == 1:
            vk_result = _user32.VkKeyScanW(ord(part))
            if vk_result == -1:
                raise ValueError(f"Unknown key: {part!r}")
            main_vks.append(vk_result & 0xFF)
        else:
            raise ValueError(f"Unknown hotkey part: {part!r}")

    return mod_vks, main_vks


def _is_down(vk: int) -> bool:
    """Return True if key is pressed. Tries win32u → user32."""
    if _has_win32u:
        try:
            return bool(_win32u.NtUserGetAsyncKeyState(vk) & _KEY_PRESSED)
        except Exception:
            pass
    return bool(_user32.GetAsyncKeyState(vk) & _KEY_PRESSED)


# ------------------------------------------------------------------ #
# Shared key state tracker (fed by LL hook and Raw Input)
# ------------------------------------------------------------------ #

class _KeyStateTracker:
    """Thread-safe tracker of currently pressed keys from multiple sources."""

    def __init__(self) -> None:
        self._pressed: set[int] = set()
        self._lock = threading.Lock()

    def key_down(self, vk: int) -> None:
        with self._lock:
            self._pressed.add(vk)

    def key_up(self, vk: int) -> None:
        with self._lock:
            self._pressed.discard(vk)

    def is_down(self, vk: int) -> bool:
        with self._lock:
            return vk in self._pressed

    def clear(self) -> None:
        with self._lock:
            self._pressed.clear()


_tracker = _KeyStateTracker()


def _is_down_any(vk: int) -> bool:
    """Check if key is down via hook tracker OR GetAsyncKeyState."""
    if _tracker.is_down(vk):
        return True
    return _is_down(vk)


# ------------------------------------------------------------------ #
# Hotkey entries
# ------------------------------------------------------------------ #

class _HotkeyEntry:
    def __init__(self, hotkey: str, mod_vks: list[int], main_vks: list[int],
                 callback: Callable[[], None]) -> None:
        self.hotkey = hotkey
        self.mod_vks = mod_vks
        self.main_vks = main_vks
        self.callback = callback
        self._debounce_counter = 0
        self._armed = True

    def check_and_fire(self) -> None:
        mods_down = all(_is_down_any(vk) for vk in self.mod_vks)
        if self.main_vks:
            main_down = any(_is_down_any(vk) for vk in self.main_vks)
            all_down = mods_down and main_down
        else:
            extra_key = any(
                _is_down_any(vk)
                for vk in range(0x20, 0xE0)
                if vk not in _ALL_MOD_VKS and vk not in self.mod_vks
            )
            all_down = mods_down and not extra_key

        if all_down and self._armed:
            self._armed = False
            self._debounce_counter = 0
            log.debug("Hotkey fired: %s", self.hotkey)
            try:
                threading.Thread(target=self.callback, daemon=True).start()
            except Exception as exc:
                log.error("Hotkey callback error: %s", exc)
        elif not all_down:
            if not self._armed:
                self._debounce_counter += 1
                if self._debounce_counter >= _DEBOUNCE_POLLS:
                    self._armed = True
                    self._debounce_counter = 0


class _DoubleTapEntry:
    def __init__(self, hotkey: str, mod_name: str, callback: Callable[[], None]) -> None:
        self.hotkey = hotkey
        self.mod_name = mod_name
        self.mod_vk = _MOD_SINGLE_VK[mod_name]
        self.callback = callback
        self._last_up: float = 0.0
        self._was_down = False
        self._cooldown = False

    def check_and_fire(self) -> None:
        is_down = _is_down_any(self.mod_vk)
        if is_down and not self._was_down:
            now = time.monotonic()
            if not self._cooldown and (now - self._last_up) < _DOUBLE_TAP_INTERVAL:
                self._cooldown = True
                log.debug("Double-tap hotkey fired: %s", self.hotkey)
                threading.Thread(target=self.callback, daemon=True).start()
            self._was_down = True
        elif not is_down and self._was_down:
            self._last_up = time.monotonic()
            self._was_down = False
            self._cooldown = False


# ------------------------------------------------------------------ #
# WH_KEYBOARD_LL hook thread
# ------------------------------------------------------------------ #

def _start_ll_hook() -> threading.Thread | None:
    """Install a low-level keyboard hook in a dedicated thread with message pump.

    Installed AFTER exam software so it runs first in the LIFO chain,
    capturing keystrokes before ksHook.exe can consume them.
    """
    def _hook_proc(nCode, wParam, lParam):
        if nCode >= 0:
            try:
                kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
                vk = kb.vkCode
                if wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
                    _tracker.key_down(vk)
                elif wParam in (WM_KEYUP, WM_SYSKEYUP):
                    _tracker.key_up(vk)
            except Exception:
                pass
        return _user32.CallNextHookEx(None, nCode, wParam, lParam)

    # Must keep reference alive to prevent GC
    hook_func = HOOKPROC(_hook_proc)

    def _run():
        hHook = _user32.SetWindowsHookExW(WH_KEYBOARD_LL, hook_func, None, 0)
        if not hHook:
            log.warning("[ll-hook] SetWindowsHookExW failed")
            return

        log.info("[ll-hook] WH_KEYBOARD_LL hook installed (LIFO bypass)")

        # Message pump — required for LL hooks to work
        msg = ctypes.wintypes.MSG()
        while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
            _user32.TranslateMessage(ctypes.byref(msg))
            _user32.DispatchMessageW(ctypes.byref(msg))

        _user32.UnhookWindowsHookEx(hHook)

    t = threading.Thread(target=_run, daemon=True, name="ll-keyboard-hook")
    t.start()
    return t


# ------------------------------------------------------------------ #
# Raw Input thread
# ------------------------------------------------------------------ #

def _start_raw_input() -> threading.Thread | None:
    """Register for Raw Input keyboard events via a hidden window."""
    WNDPROC = ctypes.CFUNCTYPE(
        ctypes.wintypes.LPARAM,  # LRESULT — pointer-sized on 64-bit Windows
        ctypes.wintypes.HWND,
        ctypes.c_uint,
        ctypes.wintypes.WPARAM,
        ctypes.wintypes.LPARAM,
    )

    def _wnd_proc(hwnd, uMsg, wParam, lParam):
        if uMsg == WM_INPUT:
            try:
                raw_size = ctypes.c_uint(ctypes.sizeof(RAWINPUT_KEYBOARD))
                raw = RAWINPUT_KEYBOARD()
                _user32.GetRawInputData(
                    ctypes.cast(lParam, ctypes.c_void_p),
                    RID_INPUT,
                    ctypes.byref(raw),
                    ctypes.byref(raw_size),
                    ctypes.sizeof(RAWINPUTHEADER),
                )
                if raw.header.dwType == RIM_TYPEKEYBOARD:
                    vk = raw.keyboard.VKey
                    flags = raw.keyboard.Flags
                    if flags & 1:  # key up
                        _tracker.key_up(vk)
                    else:  # key down
                        _tracker.key_down(vk)
            except Exception:
                pass
            return 0
        return _user32.DefWindowProcW(hwnd, uMsg, wParam, lParam)

    wnd_proc_func = WNDPROC(_wnd_proc)

    def _run():
        class WNDCLASSEXW(ctypes.Structure):
            _fields_ = [
                ("cbSize", ctypes.c_uint),
                ("style", ctypes.c_uint),
                ("lpfnWndProc", WNDPROC),
                ("cbClsExtra", ctypes.c_int),
                ("cbWndExtra", ctypes.c_int),
                ("hInstance", ctypes.wintypes.HINSTANCE),
                ("hIcon", ctypes.wintypes.HICON),
                ("hCursor", ctypes.wintypes.HANDLE),
                ("hbrBackground", ctypes.wintypes.HANDLE),
                ("lpszMenuName", ctypes.wintypes.LPCWSTR),
                ("lpszClassName", ctypes.wintypes.LPCWSTR),
                ("hIconSm", ctypes.wintypes.HICON),
            ]

        hInst = ctypes.windll.kernel32.GetModuleHandleW(None)
        class_name = "MuraveyRawInput"

        wc = WNDCLASSEXW()
        wc.cbSize = ctypes.sizeof(WNDCLASSEXW)
        wc.lpfnWndProc = wnd_proc_func
        wc.hInstance = hInst
        wc.lpszClassName = class_name

        if not _user32.RegisterClassExW(ctypes.byref(wc)):
            log.warning("[raw-input] RegisterClassExW failed")
            return

        HWND_MESSAGE = ctypes.wintypes.HWND(-3)
        hwnd = _user32.CreateWindowExW(
            0, class_name, "", 0,
            0, 0, 0, 0,
            HWND_MESSAGE, None, hInst, None,
        )
        if not hwnd:
            log.warning("[raw-input] CreateWindowExW failed")
            return

        # Register for keyboard raw input
        rid = RAWINPUTDEVICE()
        rid.usUsagePage = 0x01  # HID_USAGE_PAGE_GENERIC
        rid.usUsage = 0x06     # HID_USAGE_GENERIC_KEYBOARD
        rid.dwFlags = RIDEV_INPUTSINK
        rid.hwndTarget = hwnd

        if not _user32.RegisterRawInputDevices(ctypes.byref(rid), 1, ctypes.sizeof(rid)):
            log.warning("[raw-input] RegisterRawInputDevices failed")
            return

        log.info("[raw-input] Raw Input keyboard registered (HID-level)")

        msg = ctypes.wintypes.MSG()
        while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
            _user32.TranslateMessage(ctypes.byref(msg))
            _user32.DispatchMessageW(ctypes.byref(msg))

    t = threading.Thread(target=_run, daemon=True, name="raw-input")
    t.start()
    return t


# ------------------------------------------------------------------ #
# Main manager
# ------------------------------------------------------------------ #

class PollingHotkeyManager(HotkeyManager):
    """Multi-strategy hotkey manager — LL hook + Raw Input + polling."""

    def __init__(self) -> None:
        self._entries: dict[str, _HotkeyEntry] = {}
        self._double_taps: dict[str, _DoubleTapEntry] = {}
        self._lock = threading.Lock()
        self._running = False
        self._poll_thread: threading.Thread | None = None
        self._hook_thread: threading.Thread | None = None
        self._rawinput_thread: threading.Thread | None = None
        self._on_csca_open: Callable | None = None
        self._on_csca_close: Callable | None = None

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._csca_active = False

        # Start with polling only (reliable without CSCA)
        self._poll_thread = threading.Thread(
            target=self._poll_loop,
            daemon=True,
            name="hotkey-poll",
        )
        self._poll_thread.start()
        log.debug("PollingHotkeyManager started (polling mode)")

    def _activate_csca_bypass(self) -> None:
        """Start LL hook + Raw Input when CSCA is detected."""
        if self._csca_active:
            return
        self._csca_active = True
        log.info("[hotkey] CSCA detected — activating LL hook + Raw Input bypass")

        try:
            self._hook_thread = _start_ll_hook()
        except Exception as exc:
            log.warning("[hotkey] LL hook failed: %s", exc)

        try:
            self._rawinput_thread = _start_raw_input()
        except Exception as exc:
            log.warning("[hotkey] Raw Input failed: %s", exc)

    def stop(self) -> None:
        self._running = False
        if self._poll_thread:
            self._poll_thread.join(timeout=1.0)

    def register(self, hotkey: str, callback: Callable[[], None]) -> bool:
        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, tuple) and parsed[0] == "double":
                _, mod_name = parsed
                self._double_taps[hotkey] = _DoubleTapEntry(hotkey, mod_name, callback)
            else:
                mod_vks, main_vks = parsed
                self._entries[hotkey] = _HotkeyEntry(hotkey, mod_vks, main_vks, callback)

        if not self._running:
            self.start()

        log.debug("Hotkey registered: %r", hotkey)
        return True

    def unregister(self, hotkey: str) -> bool:
        with self._lock:
            removed = self._entries.pop(hotkey, None) or self._double_taps.pop(hotkey, None)
        if removed:
            log.info("Hotkey unregistered: %r", hotkey)
            return True
        return False

    def _csca_check_loop(self) -> None:
        """Background thread for CSCA detection — never blocks hotkey polling."""
        while self._running:
            try:
                from ..seb.detection import is_exam_running
                csca_running = is_exam_running()
                if csca_running and not self._csca_active:
                    self._activate_csca_bypass()
                    if self._on_csca_open:
                        try:
                            self._on_csca_open()
                        except Exception:
                            pass
                elif not csca_running and self._csca_active:
                    self._csca_active = False
                    if self._on_csca_close:
                        try:
                            self._on_csca_close()
                        except Exception:
                            pass
            except Exception:
                pass
            time.sleep(5.0)

    def _poll_loop(self) -> None:
        # Start CSCA detection in separate thread (never blocks polling)
        threading.Thread(target=self._csca_check_loop, daemon=True, name="csca-detect").start()

        while self._running:
            time.sleep(_POLL_INTERVAL)

            with self._lock:
                entries = list(self._entries.values())
                double_taps = list(self._double_taps.values())
            for dt in double_taps:
                dt.check_and_fire()
            for entry in entries:
                entry.check_and_fire()
