"""Global hotkey listener using pynput.

Hotkey map:
  2×Cmd          → trigger Claude query
  2×ESC          → toggle overlay show/hide
  ESC + → / ←   → scroll response down/up
  ESC + ] / [    → navigate prev/next response
  Cmd + ↑/↓     → font size up/down
"""

from __future__ import annotations

import asyncio
import logging
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import TYPE_CHECKING, Optional

from pynput import keyboard

if TYPE_CHECKING:
    from meeting_helper.config import AppConfig

logger = logging.getLogger(__name__)

_ESC_DOUBLE_TAP_THRESHOLD = 0.5
_DOUBLE_TAP_THRESHOLD = 0.4

_current_keys: set = set()
_last_esc_time: float = 0.0
_esc_press_time: float = 0.0
_esc_was_released: bool = True
_esc_double_tapped: bool = False

_last_cmd_release: float = 0.0
_cmd_was_solo: bool = True

_overlay_proc: Optional[subprocess.Popen] = None
_overlay_visible: bool = True


def _beep(freq: float, duration: float = 0.07, volume: float = 0.4) -> None:
    """Play a short sine-wave tone via NSSound."""
    def _play():
        try:
            import math
            import struct
            import wave
            import io

            sr = 44100
            n = int(sr * duration)
            fade_samples = int(sr * 0.01)
            frames = bytearray()
            for i in range(n):
                s = math.sin(2 * math.pi * freq * i / sr) * volume
                if i >= n - fade_samples:
                    s *= (n - i) / fade_samples
                frames += struct.pack('<h', max(-32767, min(32767, int(s * 32767))))
            buf = io.BytesIO()
            with wave.open(buf, 'wb') as wf:
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(sr)
                wf.writeframes(bytes(frames))
            from AppKit import NSSound
            from Foundation import NSData
            data = NSData.dataWithBytes_length_(buf.getvalue(), len(buf.getvalue()))
            sound = NSSound.alloc().initWithData_(data)
            if sound:
                sound.setVolume_(min(1.0, volume * 2))
                sound.play()
                time.sleep(duration + 0.05)
        except Exception:
            pass
    threading.Thread(target=_play, daemon=True).start()


def _schedule(coro):
    from meeting_helper.state import state
    loop = state.asyncio_loop
    if loop is not None:
        asyncio.run_coroutine_threadsafe(coro, loop)


def _is_meeting_active() -> bool:
    from meeting_helper.state import state
    return state.session_active


def _do_trigger_query(config: "AppConfig"):
    if not _is_meeting_active():
        return

    _beep(660, 0.08)

    async def _query():
        from meeting_helper.ai.client import handle_query
        from meeting_helper.state import state
        await handle_query(state, config)

    _schedule(_query())


def _do_nav(direction: str):
    async def _nav():
        from meeting_helper.server.websocket import broadcast
        await broadcast({"type": "nav", "direction": direction})
    _schedule(_nav())


def _do_scroll(direction: str):
    async def _scroll():
        from meeting_helper.server.websocket import broadcast
        await broadcast({"type": "scroll", "direction": direction, "amount": 150})
    _schedule(_scroll())


def _do_font_size(direction: str):
    async def _fs():
        from meeting_helper.server.websocket import broadcast
        await broadcast({"type": "font_size", "direction": direction})
    _schedule(_fs())


def _kill_stale_overlays():
    try:
        result = subprocess.run(
            ["pgrep", "-f", "meeting_helper.overlay"],
            capture_output=True, text=True, timeout=2,
        )
        import os, signal
        for pid_str in result.stdout.strip().split("\n"):
            pid_str = pid_str.strip()
            if pid_str and pid_str.isdigit():
                try:
                    os.kill(int(pid_str), signal.SIGTERM)
                except ProcessLookupError:
                    pass
    except Exception:
        pass


def _do_toggle_overlay(config: "AppConfig"):
    global _overlay_proc, _overlay_visible

    if not _is_meeting_active():
        return

    if _overlay_proc is not None and _overlay_proc.poll() is None:
        _overlay_visible = not _overlay_visible
        async def _toggle():
            from meeting_helper.server.websocket import broadcast
            await broadcast({"type": "overlay_toggle", "visible": _overlay_visible})
        _schedule(_toggle())
        return

    _kill_stale_overlays()
    _overlay_visible = True
    port = config.port
    ui_log = Path.home() / ".meeting-helper-ui.log"
    with open(ui_log, "a") as log_fh:
        _overlay_proc = subprocess.Popen(
            [sys.executable, "-m", "meeting_helper.overlay", str(port)],
            stdout=log_fh, stderr=log_fh, stdin=subprocess.DEVNULL,
        )
    logger.info("Overlay launched on port %d", port)


def start_hotkey_listener(config: "AppConfig") -> None:
    """Start the pynput keyboard listener in a daemon thread."""
    global _last_esc_time, _esc_press_time, _esc_was_released, _esc_double_tapped

    _CMD_KEYS = {keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r}

    def on_press(key):
        global _last_esc_time, _esc_press_time, _esc_was_released, _esc_double_tapped
        global _cmd_was_solo

        _current_keys.add(key)

        if key not in _CMD_KEYS and _current_keys & _CMD_KEYS:
            _cmd_was_solo = False

        esc = keyboard.Key.esc
        up = keyboard.Key.up
        down = keyboard.Key.down
        right = keyboard.Key.right
        left = keyboard.Key.left

        if key == esc:
            now = time.time()
            if _esc_was_released:
                _esc_press_time = now
            if (now - _last_esc_time) < _ESC_DOUBLE_TAP_THRESHOLD and _esc_was_released:
                _esc_double_tapped = True
            _esc_was_released = False
            _last_esc_time = now
            return

        # Cmd+↑/↓ — font size
        cmd_held = any(k in _current_keys for k in _CMD_KEYS)
        if cmd_held and key == up:
            _do_font_size("up")
            return
        if cmd_held and key == down:
            _do_font_size("down")
            return

        if esc not in _current_keys:
            return

        if key == right:
            _do_scroll("down")
        elif key == left:
            _do_scroll("up")
        else:
            try:
                ch = key.char
            except AttributeError:
                ch = None
            if ch == '[':
                _do_nav("prev")
            elif ch == ']':
                _do_nav("next")

    def on_release(key):
        global _esc_press_time, _esc_was_released, _esc_double_tapped
        global _last_cmd_release, _cmd_was_solo

        _current_keys.discard(key)

        if key == keyboard.Key.esc:
            _esc_was_released = True
            held_duration = time.time() - _esc_press_time
            if _esc_double_tapped and held_duration < 0.2:
                logger.info("Hotkey: 2×ESC (toggle overlay)")
                threading.Thread(target=lambda: _do_toggle_overlay(config), daemon=True).start()
            _esc_double_tapped = False

        elif key in _CMD_KEYS:
            now = time.time()
            if _cmd_was_solo and (now - _last_cmd_release) < _DOUBLE_TAP_THRESHOLD:
                logger.info("Hotkey: 2×Cmd (query)")
                threading.Thread(target=lambda: _do_trigger_query(config), daemon=True).start()
                _last_cmd_release = 0.0
            else:
                _last_cmd_release = now
            _cmd_was_solo = True

    listener = keyboard.Listener(on_press=on_press, on_release=on_release)
    t = threading.Thread(target=listener.run, daemon=True)
    t.start()
    logger.info("Hotkey listener started")
