"""
HotkeyService — global hotkey via Win32 ``RegisterHotKey``.

Replaces the previous pynput low-level hook (WH_KEYBOARD_LL), which
Windows silently detaches on hook-timeout, desktop switches, and power
events.  ``RegisterHotKey`` is an OS-managed registration and immune to
those failure modes.  Power-resume and session-unlock callbacks
re-register as a belt-and-suspenders measure.

Public API is identical to the pynput implementation — every existing
caller in ``app.py`` and ``ui_bridge.py`` works unchanged.
"""
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING

from loguru import logger
from PySide6.QtCore import QTimer

from utils.hotkey_parse import parse_hotkey
from utils.win32_input import (
    MOD_NOREPEAT,
    get_last_error, is_key_down,
    register_hotkey, unregister_hotkey,
)

from .base_service import BaseService
from .i18n import t

if TYPE_CHECKING:
    from bridge.signals import AppSignals
    from services.hotkey_native_filter import HotkeyNativeEventFilter


_HOTKEY_ID = 1
_HOLD_POLL_MS = 30
_RESUME_REREGISTER_DELAY_MS = 250
_ERR_HOTKEY_ALREADY_REGISTERED = 1409  # ERROR_HOTKEY_ALREADY_REGISTERED


class HotkeyService(BaseService):
    def __init__(self, signals: AppSignals) -> None:
        self._signals = signals
        self._ready = False
        self._hotkey_str = "<ctrl>+<shift>+<space>"
        self._mode = "toggle"  # "hold" | "toggle"

        self._filter: HotkeyNativeEventFilter | None = None
        self._registered = False
        self._mods = 0
        self._vk = 0

        # Mode state
        self._toggle_active = False
        self._hold_active = False
        self._hold_timer: QTimer | None = None

    # ─── BaseService ─────────────────────────────────────────────────────
    async def start(self) -> None:
        self._ready = True
        self._register_current()
        logger.info(
            "HotkeyService started, hotkey={}, mode={}",
            self._hotkey_str, self._mode,
        )

    async def stop(self) -> None:
        self._cancel_hold_timer()
        self._unregister_current()
        self._ready = False

    @property
    def is_ready(self) -> bool:
        return self._ready

    # ─── Legacy API kept for compatibility ──────────────────────────────
    def set_loop(self, loop: asyncio.AbstractEventLoop) -> None:
        """Kept for API compatibility — no longer needed.

        In the pynput implementation, this bridged callbacks from the
        listener thread into the asyncio loop.  ``RegisterHotKey``
        delivers events to the Qt main thread via the native filter,
        so no loop handle is required.
        """
        return None

    # ─── Configuration ──────────────────────────────────────────────────
    def attach_filter(self, filter_: HotkeyNativeEventFilter) -> None:
        """Bind the native event filter.  Called once during app init."""
        self._filter = filter_
        filter_.bind_service(self)

    def set_hotkey(self, hotkey_str: str) -> None:
        # If we're in an active toggle recording, stop it cleanly first
        # (same behaviour as the pynput implementation).
        if self._toggle_active:
            self._toggle_active = False
            self._emit_stop()
        self._cancel_hold_timer()
        if self._hold_active:
            self._hold_active = False
            self._emit_stop()

        self._hotkey_str = hotkey_str
        if self._ready:
            self._register_current()
        logger.info("Hotkey updated: {}", hotkey_str)

    def set_mode(self, mode: str) -> None:
        self._mode = mode if mode in ("hold", "toggle") else "hold"
        # Mode change should not leave dangling recording state
        if self._toggle_active:
            self._toggle_active = False
            self._emit_stop()
        self._cancel_hold_timer()
        if self._hold_active:
            self._hold_active = False
            self._emit_stop()

    def notify_recording_stopped(self) -> None:
        """Reset toggle state when recording stops (external trigger).

        Called by the app pipeline after every recording stop so that
        the hotkey toggle stays in sync with the actual recording state.
        Without this, an external stop (e.g. device disconnect) would
        leave ``_toggle_active`` as True, causing the next hotkey press
        to send a stop instead of a start.
        """
        self._toggle_active = False
        # Hold-mode uses the polling timer to detect release; nothing
        # to reset here.

    @property
    def current_hotkey(self) -> str:
        return self._hotkey_str

    # ─── Native filter callbacks (main thread) ──────────────────────────
    def on_wm_hotkey(self, hotkey_id: int) -> None:
        if hotkey_id != _HOTKEY_ID:
            return
        if not self._ready:
            return

        if self._mode == "hold":
            if self._hold_active:
                return  # MOD_NOREPEAT should prevent this, guard anyway
            self._hold_active = True
            self._emit_start()
            self._start_hold_timer()
        else:  # toggle
            if not self._toggle_active:
                self._toggle_active = True
                self._emit_start()
            else:
                self._toggle_active = False
                self._emit_stop()

    def on_suspend(self) -> None:
        logger.info("System suspending — stopping active recording")
        self._cancel_hold_timer()
        if self._hold_active:
            self._hold_active = False
            self._emit_stop()
        if self._toggle_active:
            self._toggle_active = False
            self._emit_stop()
        # Leave registration intact — Windows preserves it across sleep,
        # but we re-register on resume defensively anyway.

    def on_resume(self) -> None:
        logger.info("System resumed — re-registering hotkey")
        QTimer.singleShot(
            _RESUME_REREGISTER_DELAY_MS, self._do_resume_reregister,
        )

    def on_session_lock(self) -> None:
        logger.info("Session locked — stopping active recording")
        self._cancel_hold_timer()
        if self._hold_active:
            self._hold_active = False
            self._emit_stop()
        if self._toggle_active:
            self._toggle_active = False
            self._emit_stop()

    def on_session_unlock(self) -> None:
        logger.info("Session unlocked — re-registering hotkey")
        QTimer.singleShot(
            _RESUME_REREGISTER_DELAY_MS, self._do_resume_reregister,
        )

    # ─── Internals ──────────────────────────────────────────────────────
    def _do_resume_reregister(self) -> None:
        if not self._ready:
            return
        self._register_current()

    def _register_current(self) -> bool:
        """Parse ``self._hotkey_str`` and (re-)register with the OS."""
        if self._filter is None or not self._filter.hwnd:
            logger.error("Hotkey filter not attached — cannot register")
            self._signals.error_occurred.emit(t("err_hotkey_init"))
            return False

        parsed = parse_hotkey(self._hotkey_str)
        if parsed.error is not None:
            logger.error("Invalid hotkey '{}': {}", self._hotkey_str, parsed.error)
            self._signals.error_occurred.emit(
                f"Invalid hotkey: {parsed.error}",
            )
            return False

        # Unregister previous (idempotent) before registering new mods/vk.
        self._unregister_current()

        self._mods = parsed.modifiers
        self._vk = parsed.vk
        mods_with_norepeat = self._mods | MOD_NOREPEAT

        ok = register_hotkey(
            self._filter.hwnd, _HOTKEY_ID, mods_with_norepeat, self._vk,
        )
        if not ok:
            err = get_last_error()
            if err == _ERR_HOTKEY_ALREADY_REGISTERED:
                msg = (
                    f"Hotkey '{parsed.pretty}' is already in use by "
                    "another application"
                )
            else:
                msg = (
                    f"Failed to register hotkey '{parsed.pretty}' "
                    f"(Win32 error {err})"
                )
            logger.error(msg)
            self._signals.error_occurred.emit(msg)
            self._registered = False
            return False

        self._registered = True
        logger.debug(
            "RegisterHotKey OK: id={}, mods=0x{:x}, vk=0x{:x} ({})",
            _HOTKEY_ID, self._mods, self._vk, parsed.pretty,
        )
        return True

    def _unregister_current(self) -> None:
        if not self._registered:
            return
        if self._filter is None or not self._filter.hwnd:
            self._registered = False
            return
        try:
            unregister_hotkey(self._filter.hwnd, _HOTKEY_ID)
        except Exception as e:
            logger.warning("UnregisterHotKey error: {}", e)
        self._registered = False

    # ─── Hold-release polling (QTimer on main thread) ───────────────────
    def _start_hold_timer(self) -> None:
        if self._hold_timer is None:
            self._hold_timer = QTimer()
            self._hold_timer.setInterval(_HOLD_POLL_MS)
            self._hold_timer.timeout.connect(self._poll_hold_release)
        self._hold_timer.start()

    def _cancel_hold_timer(self) -> None:
        if self._hold_timer is not None:
            self._hold_timer.stop()

    def _poll_hold_release(self) -> None:
        if not self._hold_active:
            self._cancel_hold_timer()
            return
        # Only poll the non-modifier vk — matches pynput's behaviour
        # where release of the primary key ended the recording, and is
        # simpler + better UX than "any key in combo released".
        if not is_key_down(self._vk):
            self._hold_active = False
            self._cancel_hold_timer()
            self._emit_stop()

    # ─── Signal emission ────────────────────────────────────────────────
    def _emit_start(self) -> None:
        self._signals.recording_should_start.emit()

    def _emit_stop(self) -> None:
        self._signals.recording_should_stop.emit()
