"""Windows global hotkey manager using Win32 RegisterHotKey.

Runs a dedicated message-loop thread (required by Windows).
Uses a queue for thread-safe registration from any thread.
"""

from __future__ import annotations

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

from .manager import HotkeyManager

log = logging.getLogger(__name__)

# Win32 modifier key flags
_MOD_ALT = 0x0001
_MOD_CONTROL = 0x0002
_MOD_SHIFT = 0x0004
_MOD_WIN = 0x0008
_MOD_NOREPEAT = 0x4000  # prevent auto-repeat

# Common virtual key codes
_VK_MAP: dict[str, 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,
    # Letters a-z → VK codes 0x41-0x5A (uppercase ASCII)
    **{chr(c): c - 0x20 for c in range(ord("a"), ord("z") + 1)},
}

_MOD_MAP: dict[str, int] = {
    "ctrl": _MOD_CONTROL,
    "control": _MOD_CONTROL,
    "alt": _MOD_ALT,
    "shift": _MOD_SHIFT,
    "win": _MOD_WIN,
    "windows": _MOD_WIN,
    # macOS style aliases → map to Windows equivalents
    "cmd": _MOD_CONTROL,
    "command": _MOD_CONTROL,
    "option": _MOD_ALT,
}

_WM_HOTKEY = 0x0312
_WM_QUIT = 0x0012

user32 = ctypes.windll.user32  # type: ignore[attr-defined]


def _parse_hotkey(hotkey: str) -> tuple[int, int]:
    """Parse 'ctrl+shift+space' → (modifiers, vk_code).

    Raises ValueError on unknown key.
    """
    parts = [p.strip().lower() for p in hotkey.split("+")]
    modifiers = _MOD_NOREPEAT
    vk = 0

    for part in parts:
        if part in _MOD_MAP:
            modifiers |= _MOD_MAP[part]
        elif part in _VK_MAP:
            vk = _VK_MAP[part]
        elif len(part) == 1:
            # Single character — use VkKeyScanW
            vk_result = user32.VkKeyScanW(ord(part))
            if vk_result == -1:
                raise ValueError(f"Unknown key: {part!r}")
            vk = vk_result & 0xFF
        else:
            raise ValueError(f"Unknown hotkey part: {part!r}")

    if vk == 0:
        raise ValueError(f"No main key found in hotkey: {hotkey!r}")

    return modifiers, vk


class WindowsHotkeyManager(HotkeyManager):
    """Win32 RegisterHotKey with a background message-loop thread."""

    def __init__(self) -> None:
        self._hotkeys: dict[str, int] = {}  # hotkey_str → id
        self._callbacks: dict[int, Callable] = {}  # id → callback (weak ref)
        self._next_id = 1
        self._queue: queue.Queue = queue.Queue()
        self._thread: threading.Thread | None = None
        self._running = False
        self._thread_hwnd: int | None = None
        self._ready = threading.Event()

    def start(self) -> None:
        """Start the message-loop thread."""
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(target=self._message_loop, daemon=True, name="hotkey-loop")
        self._thread.start()
        self._ready.wait(timeout=2.0)

    def stop(self) -> None:
        """Stop the message loop and unregister all hotkeys."""
        self._running = False
        user32.PostThreadMessageW(
            self._thread.ident if self._thread else 0,
            _WM_QUIT,
            0,
            0,
        )
        if self._thread:
            self._thread.join(timeout=2.0)

    def register(self, hotkey: str, callback: Callable[[], None]) -> bool:
        """Register *hotkey* globally. Returns True on success."""
        if not self._running:
            self.start()
        try:
            modifiers, vk = _parse_hotkey(hotkey)
        except ValueError as exc:
            log.error("Invalid hotkey %r: %s", hotkey, exc)
            return False

        hk_id = self._next_id
        self._next_id += 1
        self._callbacks[hk_id] = callback
        self._hotkeys[hotkey] = hk_id

        # Schedule registration in the message-loop thread
        result: list[bool] = []
        done = threading.Event()
        self._queue.put(("register", hk_id, modifiers, vk, result, done))
        done.wait(timeout=2.0)
        ok = bool(result and result[0])
        if not ok:
            log.error("RegisterHotKey failed for %r (id=%d)", hotkey, hk_id)
            self._callbacks.pop(hk_id, None)
            self._hotkeys.pop(hotkey, None)
        else:
            log.info("Hotkey registered: %r (id=%d)", hotkey, hk_id)
        return ok

    def unregister(self, hotkey: str) -> bool:
        """Unregister a hotkey. Returns True on success."""
        hk_id = self._hotkeys.pop(hotkey, None)
        if hk_id is None:
            return False
        self._callbacks.pop(hk_id, None)

        done = threading.Event()
        self._queue.put(("unregister", hk_id, done))
        done.wait(timeout=2.0)
        log.info("Hotkey unregistered: %r", hotkey)
        return True

    # ------------------------------------------------------------------ #
    # Internal message loop (runs in dedicated thread)
    # ------------------------------------------------------------------ #

    def _message_loop(self) -> None:
        """Win32 message loop — must run in its own thread."""
        self._ready.set()

        msg = ctypes.wintypes.MSG()

        while self._running:
            # Process pending register/unregister requests
            while not self._queue.empty():
                try:
                    item = self._queue.get_nowait()
                    self._handle_queue_item(item)
                except queue.Empty:
                    break

            # Peek for WM_HOTKEY (non-blocking)
            if user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1):  # PM_REMOVE=1
                if msg.message == _WM_HOTKEY:
                    hk_id = msg.wParam
                    cb = self._callbacks.get(hk_id)
                    if cb is not None:
                        try:
                            threading.Thread(target=cb, daemon=True).start()
                        except Exception as exc:
                            log.error("Hotkey callback error: %s", exc)
                elif msg.message == _WM_QUIT:
                    break
                user32.TranslateMessage(ctypes.byref(msg))
                user32.DispatchMessageW(ctypes.byref(msg))
            else:
                # Small sleep to avoid busy-wait
                import time

                time.sleep(0.01)

        # Clean up registered hotkeys
        for hk_id in list(self._callbacks.keys()):
            user32.UnregisterHotKey(None, hk_id)

    def _handle_queue_item(self, item: tuple) -> None:
        action = item[0]
        if action == "register":
            _, hk_id, modifiers, vk, result, done = item
            ok = bool(user32.RegisterHotKey(None, hk_id, modifiers, vk))
            result.append(ok)
            done.set()
        elif action == "unregister":
            _, hk_id, done = item
            user32.UnregisterHotKey(None, hk_id)
            done.set()
