"""Global push-to-talk hotkey.

The wake word is the primary trigger, but it misfires in a noisy room and
won't fire at all if you're on a call — so there's a keyboard fallback that
works even when the pet has no focus (it's a frameless Qt.Tool window with no
taskbar entry, so an ordinary QShortcut would never see the key).

Needs `pynput`, which needs an X11/Win32/macOS input hook: on most Wayland
sessions it can't grab global keys, and on macOS it needs Accessibility
permission. All of that is a soft failure — start() reports the reason and
the wake word / tray keep working.
"""

from __future__ import annotations

from typing import Callable, Optional

# Aliases for the names people actually type in a .env file.
_ALIASES = {
    "control": "ctrl",
    "ctl": "ctrl",
    "option": "alt",
    "opt": "alt",
    "win": "cmd",
    "windows": "cmd",
    "super": "cmd",
    "meta": "cmd",
    "command": "cmd",
    "return": "enter",
    "escape": "esc",
    "del": "delete",
    "ins": "insert",
    "pgup": "page_up",
    "pgdn": "page_down",
}


class HotkeyError(Exception):
    pass


def to_pynput_spec(spec: str) -> str:
    """"ctrl+alt+space" -> "<ctrl>+<alt>+<space>" (pynput's GlobalHotKeys
    syntax: named keys in angle brackets, literal characters bare)."""
    tokens = [t.strip().lower() for t in (spec or "").split("+")]
    tokens = [t for t in tokens if t]
    if not tokens:
        raise HotkeyError("empty hotkey")
    parts = []
    for token in tokens:
        token = _ALIASES.get(token, token)
        parts.append(token if len(token) == 1 else f"<{token}>")
    return "+".join(parts)


class GlobalHotkey:
    """Fires *callback* whenever the hotkey is pressed, anywhere. Safe to
    construct unconditionally — nothing happens until start(), and start()
    reports failure instead of raising into the UI thread."""

    def __init__(self, spec: str, callback: Callable[[], None]):
        self.spec = spec
        self._callback = callback
        self._listener = None

    @property
    def running(self) -> bool:
        return self._listener is not None

    def start(self) -> Optional[str]:
        """None on success, otherwise a human-readable reason it's off."""
        if not (self.spec or "").strip():
            return None  # explicitly disabled — not an error worth reporting
        try:
            pynput_spec = to_pynput_spec(self.spec)
        except HotkeyError as exc:
            return f"push-to-talk hotkey {self.spec!r} is invalid: {exc}"
        try:
            from pynput import keyboard
        except Exception as exc:  # ImportError, or a backend that won't load
            return f"push-to-talk needs pynput ({exc}) — wake word still works"
        try:
            listener = keyboard.GlobalHotKeys({pynput_spec: self._safe_callback})
            listener.daemon = True
            listener.start()
        except Exception as exc:
            return f"push-to-talk unavailable on this session ({exc}) — wake word still works"
        self._listener = listener
        return None

    def _safe_callback(self) -> None:
        # This runs on pynput's listener thread; an exception there would
        # silently kill the listener for the rest of the session.
        try:
            self._callback()
        except Exception:
            pass

    def stop(self) -> None:
        if self._listener is not None:
            try:
                self._listener.stop()
            except Exception:
                pass
            self._listener = None
