"""Global hotkey listener using pynput.

Runs in a background daemon thread. Detects ESC+arrow combos and
bridges to the asyncio event loop via asyncio.run_coroutine_threadsafe().

Hotkey map:
  ESC + ↓  → capture screenshot
  ESC + →  → scroll viewer down
  ESC + ←  → scroll viewer up
  ESC + [ / ]          → navigate prev/next response
  ESC double-tap+release → toggle overlay UI
  ESC double-tap+hold   → manual voice recording
  Cmd double-tap       → trigger query
  Ctrl double-tap      → copy selection + trigger query
  Cmd + ↑/↓            → font size up/down
"""

from __future__ import annotations

import asyncio
import logging
import threading
import time
from typing import Optional, TYPE_CHECKING

from pynput import keyboard


# ---------------------------------------------------------------------------
# Audio feedback
# ---------------------------------------------------------------------------

def _beep(freq: float, duration: float = 0.07, volume: float = 0.4) -> None:
    """Play a short sine-wave tone via NSSound (no fork — safe alongside CoreAudio/SCKit)."""
    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 beep_screenshot():   _beep(900, 0.06)           # short high click
def beep_send():         _beep(660, 0.08)           # mid tone — "sending"
def beep_rec_start():    _beep(500, 0.12)           # low — recording on
def beep_rec_stop():     _beep(400, 0.10)           # lower — recording off

if TYPE_CHECKING:
    from ai_interview.config import Config

logger = logging.getLogger(__name__)

_ESC_DOUBLE_TAP_THRESHOLD = 0.5   # seconds between two ESC presses
_ESC_HOLD_THRESHOLD = 0.2         # seconds ESC must be held for "double-tap+hold"

_DOUBLE_TAP_THRESHOLD = 0.4  # seconds between two taps for Cmd/Option

_current_keys: set = set()
_last_esc_time: float = 0.0
_esc_press_time: float = 0.0
_esc_was_released: bool = True   # must be released between tap and hold
_esc_double_tapped: bool = False  # True between double-tap press and release
_is_recording_manual: bool = False
_manual_recording_thread: Optional[threading.Thread] = None
_manual_audio_chunks: list = []
_lock = threading.Lock()

# Double-tap tracking for Cmd and Option
_last_cmd_release: float = 0.0
_last_ctrl_release: float = 0.0
_cmd_was_solo: bool = True   # True if no other key pressed while Cmd held
_ctrl_was_solo: bool = True   # True if no other key pressed while Option held


def _schedule(coro):
    """Schedule a coroutine on the daemon's asyncio event loop."""
    from ai_interview.state import state
    loop = state.asyncio_loop
    if loop is not None:
        asyncio.run_coroutine_threadsafe(coro, loop)


def _do_capture_screenshot():
    """Capture screenshot and add to buffer."""
    from ai_interview.screenshot import capture_active_window
    from ai_interview.state import state

    path = capture_active_window()
    if path and state.screenshot_buffer is not None:
        state.screenshot_buffer.append(path)
        logger.info("Screenshot buffered: %s", path.name)
    elif path is None:
        logger.warning("Screenshot capture returned None")


def _do_trigger_query(config: "Config"):
    """Trigger AI query — scheduled on the asyncio loop."""
    async def _query():
        from ai_interview.ai_client import handle_query
        from ai_interview.state import state
        await handle_query(state, config)

    _schedule(_query())


def _do_nav(direction: str):
    """Navigate prev/next through response history on the viewer."""
    async def _nav():
        from ai_interview.server.websocket import broadcast
        await broadcast({"type": "nav", "direction": direction})
    _schedule(_nav())


def _do_scroll(direction: str):
    """Send scroll command to viewer — scheduled on asyncio loop."""
    async def _scroll():
        from ai_interview.server.websocket import broadcast
        await broadcast({"type": "scroll", "direction": direction, "amount": 150})

    _schedule(_scroll())


def _do_font_size(direction: str):
    """Change viewer font size up/down by 1px."""
    async def _fs():
        from ai_interview.server.websocket import broadcast
        await broadcast({"type": "font_size", "direction": direction})
    _schedule(_fs())


_overlay_proc: Optional["subprocess.Popen"] = None


_overlay_visible: bool = True  # starts visible when launched


def _kill_stale_overlays():
    """Kill any leftover overlay processes from previous daemon sessions."""
    import subprocess
    try:
        result = subprocess.run(
            ["pgrep", "-f", "ai_interview.overlay"],
            capture_output=True, text=True, timeout=2,
        )
        for pid_str in result.stdout.strip().split("\n"):
            pid_str = pid_str.strip()
            if pid_str and pid_str.isdigit():
                import os, signal
                try:
                    os.kill(int(pid_str), signal.SIGTERM)
                    logger.info("Killed stale overlay process %s", pid_str)
                except ProcessLookupError:
                    pass
    except Exception:
        pass


def _do_toggle_overlay(config: "Config"):
    """Toggle the native overlay window: launch if not running, show/hide if running."""
    import subprocess
    import sys
    from pathlib import Path
    global _overlay_proc, _overlay_visible

    # If running, toggle visibility via WS message
    if _overlay_proc is not None and _overlay_proc.poll() is None:
        _overlay_visible = not _overlay_visible
        async def _toggle():
            from ai_interview.server.websocket import broadcast
            await broadcast({"type": "overlay_toggle", "visible": _overlay_visible})
        _schedule(_toggle())
        logger.info("Overlay %s", "shown" if _overlay_visible else "hidden")
        return

    # Kill any stale overlays from previous sessions before launching
    _kill_stale_overlays()

    # Not running — launch it
    _overlay_visible = True
    port = config.port
    ui_log = Path.home() / ".ai-interview-ui.log"
    with open(ui_log, "a") as log_fh:
        _overlay_proc = subprocess.Popen(
            [sys.executable, "-m", "ai_interview.overlay", str(port)],
            stdout=log_fh,
            stderr=log_fh,
            stdin=subprocess.DEVNULL,
        )
    logger.info("Overlay launched on port %d", port)


def _start_manual_recording(config: "Config"):
    """Start recording microphone manually (bypasses rolling transcriber)."""
    global _manual_audio_chunks, _is_recording_manual

    import sounddevice as sd
    import numpy as np

    _manual_audio_chunks = []
    _is_recording_manual = True
    logger.info("Manual recording started")

    def _callback(indata, frames, time_info, status):
        if _is_recording_manual:
            _manual_audio_chunks.append(indata[:, 0].copy())

    try:
        stream = sd.InputStream(
            channels=1,
            samplerate=16000,
            dtype=np.float32,
            callback=_callback,
        )
        stream.start()

        while _is_recording_manual:
            time.sleep(0.05)

        stream.stop()
        stream.close()
        _flush_manual_recording(config)
    except Exception as exc:
        logger.error("Manual recording error: %s", exc)
        _is_recording_manual = False


def _flush_manual_recording(config: "Config"):
    """Transcribe manual recording via Deepgram REST, append to buffer, auto-trigger query."""
    import io
    import wave
    import numpy as np
    from ai_interview.state import state

    if not _manual_audio_chunks:
        return

    audio = np.concatenate(_manual_audio_chunks)
    if len(audio) < 8000:  # less than 0.5 seconds — ignore noise
        return

    logger.info("Transcribing manual recording (%d samples)…", len(audio))

    # Show "Transcribing…" in the viewer immediately
    async def _show_transcribing():
        from ai_interview.server.websocket import broadcast
        from ai_interview.state import state
        await broadcast({"type": "status", "value": "processing"})
        await broadcast({"type": "start"})
        await broadcast({"type": "chunk", "text": "_Transcribing your question…_"})
    _schedule(_show_transcribing())

    def _transcribe():
        try:
            # Encode float32 → 16-bit PCM WAV in memory
            audio_int16 = (audio * 32767).astype(np.int16)
            buf = io.BytesIO()
            with wave.open(buf, "wb") as wf:
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(16000)
                wf.writeframes(audio_int16.tobytes())
            wav_bytes = buf.getvalue()

            # Transcribe with Deepgram REST (pre-recorded)
            import httpx
            lang = config.transcription_language or "en"
            resp = httpx.post(
                f"https://api.deepgram.com/v1/listen?model=nova-2&smart_format=true&language={lang}",
                headers={
                    "Authorization": f"Token {config.deepgram_api_key}",
                    "Content-Type": "audio/wav",
                },
                content=wav_bytes,
                timeout=15,
            )
            resp.raise_for_status()
            text = (
                resp.json()
                .get("results", {})
                .get("channels", [{}])[0]
                .get("alternatives", [{}])[0]
                .get("transcript", "")
                .strip()
            )

            if text:
                logger.info("Manual transcript: %s", text[:120])
                if state.transcript_buffer is not None:
                    state.transcript_buffer.append(f"[user] {text}")
                # Reset status then auto-trigger query
                async def _reset():
                    from ai_interview.server.websocket import broadcast
                    from ai_interview.state import state as st
                    st.status = "idle"
                    await broadcast({"type": "start"})  # clear placeholder
                _schedule(_reset())
                _do_trigger_query(config)
            else:
                logger.warning("Manual recording: no speech detected")

        except Exception as exc:
            logger.error("Manual transcription error: %s", exc)

    threading.Thread(target=_transcribe, daemon=True).start()


def start_hotkey_listener(config: "Config") -> None:
    """Start the pynput keyboard listener in a daemon thread."""

    global _last_esc_time, _esc_press_time, _esc_was_released, _esc_double_tapped, _is_recording_manual, _manual_recording_thread

    _CMD_KEYS = {keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r}
    _CTRL_KEYS = {keyboard.Key.ctrl, keyboard.Key.ctrl_l, keyboard.Key.ctrl_r}

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

        _current_keys.add(key)

        # Track if Cmd/Option was pressed alone (no combo)
        if key not in _CMD_KEYS and _current_keys & _CMD_KEYS:
            _cmd_was_solo = False
        if key not in _CTRL_KEYS and _current_keys & _CTRL_KEYS:
            _ctrl_was_solo = False

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

        # ESC tap-then-hold detection (tap = press+release, then press and hold)
        if key == esc:
            now = time.time()
            gap = now - _last_esc_time
            # Only update press time on genuine presses, not key-repeat events.
            # Key-repeat would reset _esc_press_time so on_release sees a tiny
            # held_duration and never stops the recording.
            if _esc_was_released:
                _esc_press_time = now

            if gap < _ESC_DOUBLE_TAP_THRESHOLD and _esc_was_released:
                _esc_double_tapped = True
                # ESC was tapped (released) then pressed again — check if held
                def _check_hold():
                    global _is_recording_manual, _manual_recording_thread
                    time.sleep(_ESC_HOLD_THRESHOLD)
                    if esc in _current_keys:  # still held → manual recording
                        if not _is_recording_manual:
                            _is_recording_manual = True
                            beep_rec_start()
                            _manual_recording_thread = threading.Thread(
                                target=_start_manual_recording, args=(config,), daemon=True
                            )
                            _manual_recording_thread.start()

                threading.Thread(target=_check_hold, daemon=True).start()

            _esc_was_released = False  # mark as currently held (blocks key-repeat events)
            _last_esc_time = now
            return

        # Cmd+↑ / Cmd+↓ — increase/decrease font size
        cmd_held = any(k in _current_keys for k in (
            keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r))
        if cmd_held and key == up:
            logger.info("Hotkey: Cmd+↑ (font size up)")
            _do_font_size("up")
            return
        if cmd_held and key == down:
            logger.info("Hotkey: Cmd+↓ (font size down)")
            _do_font_size("down")
            return

        if esc not in _current_keys:
            return

        if key == down:
            logger.debug("Hotkey: ESC+↓ (screenshot)")
            threading.Thread(target=_do_capture_screenshot, daemon=True).start()

        elif key == right:
            logger.debug("Hotkey: ESC+→ (scroll down)")
            _do_scroll("down")

        elif key == left:
            logger.debug("Hotkey: ESC+← (scroll up)")
            _do_scroll("up")

        else:
            try:
                ch = key.char
            except AttributeError:
                ch = None
            if ch == '[':

                logger.debug("Hotkey: ESC+[ (prev message)")
                _do_nav("prev")
            elif ch == ']':
                logger.debug("Hotkey: ESC+] (next message)")
                _do_nav("next")

    def _do_copy_and_query():
        """Simulate Cmd+C to copy selection, then trigger query with clipboard."""
        from pynput.keyboard import Controller, Key
        from ai_interview.state import state
        kb = Controller()
        kb.press(Key.cmd)
        kb.press('c')
        kb.release('c')
        kb.release(Key.cmd)
        time.sleep(0.1)  # let clipboard populate
        state.include_clipboard = True
        logger.info("Hotkey: 2×Ctrl (copy + query)")
        _do_trigger_query(config)

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

        _current_keys.discard(key)

        if key == keyboard.Key.esc:
            _esc_was_released = True
            held_duration = time.time() - _esc_press_time
            if _is_recording_manual and held_duration >= _ESC_HOLD_THRESHOLD:
                _is_recording_manual = False
                beep_rec_stop()
                logger.info("Manual recording stopped")
            elif _esc_double_tapped and held_duration < _ESC_HOLD_THRESHOLD:
                # Double-tap + quick release → toggle overlay
                logger.info("Hotkey: 2×ESC (toggle overlay)")
                threading.Thread(target=lambda: _do_toggle_overlay(config), daemon=True).start()
            _esc_double_tapped = False

        # Double-tap Command → trigger query (same as ESC+↑)
        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  # reset to prevent triple-tap
            else:
                _last_cmd_release = now
            _cmd_was_solo = True

        # Double-tap Ctrl → copy selection + trigger query
        elif key in _CTRL_KEYS:
            now = time.time()
            if _ctrl_was_solo and (now - _last_ctrl_release) < _DOUBLE_TAP_THRESHOLD:
                threading.Thread(target=_do_copy_and_query, daemon=True).start()
                _last_ctrl_release = 0.0
            else:
                _last_ctrl_release = now
            _ctrl_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")
