//! Low-level keyboard hook for capturing system key combinations.
//!
//! `WH_KEYBOARD_LL` is a GLOBAL hook: the OS invokes it for ALL desktop input regardless
//! of which window is focused. We therefore gate diversion on the viewer's focus state.
//! ONLY when the viewer window actually has focus AND "send system keys to remote" is
//! enabled does the hook DIVERT the system combinations the local shell would otherwise
//! consume — the Windows key, Win+R, Win+E, Alt+Tab, Ctrl+Esc, Alt+Esc — and forward them
//! to the remote as full-fidelity `KeyEvent`s (virtual key + hardware scan code +
//! extended-key flag + modifier snapshot), returning 1 from the hook proc to suppress the
//! local handling. All other keys flow through the normal viewer input path.
//!
//! When the toggle is OFF, the viewer is not focused, or the key is not a system combo,
//! the hook diverts NOTHING — it falls through to `CallNextHookEx` and every key reaches
//! the local OS unchanged. This keeps the technician's own Start menu / Alt+Tab working
//! while the viewer sits unfocused in the background.

use super::InputEvent;
#[cfg(windows)]
use crate::proto;
use anyhow::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::mpsc;
#[cfg(windows)]
use tracing::trace;

#[cfg(windows)]
use windows::{
    Win32::Foundation::{LPARAM, LRESULT, WPARAM},
    Win32::UI::WindowsAndMessaging::{
        CallNextHookEx, DispatchMessageW, PeekMessageW, SetWindowsHookExW, TranslateMessage,
        UnhookWindowsHookEx, HHOOK, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, PM_REMOVE,
        WH_KEYBOARD_LL, WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
    },
};

#[cfg(windows)]
use std::sync::OnceLock;

/// Global toggle: when `true`, system key combinations are diverted to the remote;
/// when `false`, the hook is transparent and the local OS handles them. Default ON.
///
/// Lives at module scope because the `WH_KEYBOARD_LL` callback is a bare `extern "system"`
/// function with no user context pointer, so its state must be reachable statically.
static SEND_SYSTEM_KEYS: AtomicBool = AtomicBool::new(true);

/// Set whether system key combinations are forwarded to the remote (vs. handled locally).
///
/// Part of the programmatic toggle API (alongside `toggle_send_system_keys`, which the
/// Pause/Break host key drives). Retained for a future viewer menu / tray item and used
/// by the unit tests; not yet called from non-test code, hence the allow.
#[allow(dead_code)]
pub fn set_send_system_keys(enabled: bool) {
    SEND_SYSTEM_KEYS.store(enabled, Ordering::Relaxed);
}

/// Flip the "send system keys to remote" toggle and return the new value.
pub fn toggle_send_system_keys() -> bool {
    // fetch_xor(true) flips the bit and returns the PREVIOUS value; invert for the new one.
    !SEND_SYSTEM_KEYS.fetch_xor(true, Ordering::Relaxed)
}

/// Current state of the "send system keys to remote" toggle.
///
/// Part of the programmatic toggle API; used by the unit tests and available for a
/// viewer menu / status indicator. Not yet read from non-test code, hence the allow.
#[allow(dead_code)]
pub fn send_system_keys_enabled() -> bool {
    SEND_SYSTEM_KEYS.load(Ordering::Relaxed)
}

/// Whether the viewer window currently has input focus. Default `false`.
///
/// `WH_KEYBOARD_LL` is a GLOBAL hook fired for all desktop input, so it must NOT divert
/// system combos while the viewer is unfocused — otherwise the technician's own local
/// Win key / Alt+Tab / Ctrl+Esc would be suppressed and pushed to the remote. The render
/// loop updates this on `WindowEvent::Focused`. Lives at module scope for the same reason
/// as `SEND_SYSTEM_KEYS`: the bare `extern "system"` callback has no user-context pointer.
static VIEWER_FOCUSED: AtomicBool = AtomicBool::new(false);

/// Record whether the viewer window has input focus (drives the hook's focus gate).
pub fn set_viewer_focused(focused: bool) {
    VIEWER_FOCUSED.store(focused, Ordering::Relaxed);
}

/// Current focus state as seen by the keyboard hook.
///
/// Used by the unit tests and available for diagnostics; not yet read from non-test code
/// beyond the hook callback itself, hence the allow.
#[allow(dead_code)]
pub fn viewer_focused() -> bool {
    VIEWER_FOCUSED.load(Ordering::Relaxed)
}

#[cfg(windows)]
static INPUT_TX: OnceLock<mpsc::Sender<InputEvent>> = OnceLock::new();

#[cfg(windows)]
static mut HOOK_HANDLE: HHOOK = HHOOK(std::ptr::null_mut());

/// Virtual key codes for keys the hook reasons about.
#[cfg(windows)]
mod vk {
    pub const VK_LWIN: u32 = 0x5B;
    pub const VK_RWIN: u32 = 0x5C;
    pub const VK_APPS: u32 = 0x5D;
    pub const VK_TAB: u32 = 0x09;
    pub const VK_ESCAPE: u32 = 0x1B;
}

#[cfg(windows)]
pub struct KeyboardHook {
    _hook: HHOOK,
}

#[cfg(windows)]
impl KeyboardHook {
    pub fn new(input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
        // Store the sender globally for the hook callback. If it was already set (e.g.
        // a previous viewer instance in the same process), reuse the existing one rather
        // than failing — the hook handle itself is what we re-install.
        let _ = INPUT_TX.set(input_tx);

        unsafe {
            let hook = SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook_proc), None, 0)?;

            HOOK_HANDLE = hook;
            Ok(Self { _hook: hook })
        }
    }
}

#[cfg(windows)]
impl Drop for KeyboardHook {
    fn drop(&mut self) {
        unsafe {
            if !HOOK_HANDLE.0.is_null() {
                let _ = UnhookWindowsHookEx(HOOK_HANDLE);
                HOOK_HANDLE = HHOOK(std::ptr::null_mut());
            }
        }
    }
}

/// Decide whether a key event is a SYSTEM combination we must divert to the remote.
///
/// `vk_code` is the key; `alt`/`ctrl` are the modifier state at the moment of the event
/// (from `GetAsyncKeyState`). The Windows-key combos (Win, Win+R, Win+E) are recognized
/// by matching the Win keys themselves, so the held-Win state is not needed here. Pure
/// functions like this keep the (untestable) hook callback thin and unit-testable.
#[cfg(windows)]
fn is_system_combo(vk_code: u32, alt: bool, ctrl: bool) -> bool {
    match vk_code {
        // The Windows keys and the Applications (context-menu) key: always divert so the
        // local Start menu / Win+R / Win+E / Win+E etc. do not fire. With Win forwarded
        // down to the remote, subsequent letters (R, E, ...) compose there naturally.
        vk::VK_LWIN | vk::VK_RWIN | vk::VK_APPS => true,
        // Alt+Tab and Alt+Esc — the local window-switcher would otherwise eat these.
        vk::VK_TAB if alt => true,
        vk::VK_ESCAPE if alt => true,
        // Ctrl+Esc opens the local Start menu; divert it.
        vk::VK_ESCAPE if ctrl => true,
        _ => false,
    }
}

#[cfg(windows)]
unsafe extern "system" fn keyboard_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    if code >= 0 {
        let kb_struct = &*(lparam.0 as *const KBDLLHOOKSTRUCT);
        let vk_code = kb_struct.vkCode;
        let scan_code = kb_struct.scanCode;
        // LLKHF_EXTENDED (bit 0) marks extended keys (right Ctrl/Alt, arrows, etc.).
        let is_extended = (kb_struct.flags.0 & LLKHF_EXTENDED.0) != 0;

        let is_down = wparam.0 as u32 == WM_KEYDOWN || wparam.0 as u32 == WM_SYSKEYDOWN;
        let is_up = wparam.0 as u32 == WM_KEYUP || wparam.0 as u32 == WM_SYSKEYUP;

        if is_down || is_up {
            let forwarding = SEND_SYSTEM_KEYS.load(Ordering::Relaxed);
            let focused = VIEWER_FOCUSED.load(Ordering::Relaxed);
            let modifiers = current_modifiers();

            // Divert ONLY a SYSTEM combo, ONLY while forwarding is enabled, and ONLY while
            // the viewer window has focus. This is a global hook, so without the focus gate
            // we would swallow the technician's own Win/Alt+Tab/Ctrl+Esc while the viewer
            // sits unfocused in the background. When any condition is false we fall through
            // to CallNextHookEx and suppress nothing — the local OS handles the key. Ordinary
            // keys are left to the normal winit viewer input path (they are NOT forwarded
            // here to avoid double-injection).
            let divert =
                forwarding && focused && is_system_combo(vk_code, modifiers.alt, modifiers.ctrl);

            if divert {
                if let Some(tx) = INPUT_TX.get() {
                    let event = proto::KeyEvent {
                        down: is_down,
                        key_type: proto::KeyEventType::KeyVk as i32,
                        vk_code,
                        scan_code,
                        unicode: String::new(),
                        is_extended,
                        modifiers: Some(modifiers),
                    };

                    let _ = tx.try_send(InputEvent::Key(event));
                    trace!(
                        "System-key hook diverted: vk={:#x} scan={} ext={} down={}",
                        vk_code,
                        scan_code,
                        is_extended,
                        is_down
                    );
                }

                // Suppress local handling of the diverted system combo.
                return LRESULT(1);
            }
        }
    }

    CallNextHookEx(HOOK_HANDLE, code, wparam, lparam)
}

#[cfg(windows)]
fn current_modifiers() -> proto::Modifiers {
    use windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;

    unsafe {
        proto::Modifiers {
            ctrl: GetAsyncKeyState(0x11) < 0,  // VK_CONTROL
            alt: GetAsyncKeyState(0x12) < 0,   // VK_MENU
            shift: GetAsyncKeyState(0x10) < 0, // VK_SHIFT
            meta: GetAsyncKeyState(0x5B) < 0 || GetAsyncKeyState(0x5C) < 0, // VK_LWIN/RWIN
            caps_lock: GetAsyncKeyState(0x14) & 1 != 0, // VK_CAPITAL
            num_lock: GetAsyncKeyState(0x90) & 1 != 0, // VK_NUMLOCK
        }
    }
}

/// Pump Windows message queue (required for hooks to work).
#[cfg(windows)]
pub fn pump_messages() {
    unsafe {
        let mut msg = MSG::default();
        while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
            let _ = TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
}

// Non-Windows stubs
#[cfg(not(windows))]
#[allow(dead_code)]
pub struct KeyboardHook;

#[cfg(not(windows))]
#[allow(dead_code)]
impl KeyboardHook {
    pub fn new(_input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
        Ok(Self)
    }
}

#[cfg(not(windows))]
#[allow(dead_code)]
pub fn pump_messages() {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn toggle_defaults_on_and_flips() {
        // Default is ON.
        set_send_system_keys(true);
        assert!(send_system_keys_enabled());

        // Toggling flips and returns the NEW value.
        assert!(!toggle_send_system_keys());
        assert!(!send_system_keys_enabled());
        assert!(toggle_send_system_keys());
        assert!(send_system_keys_enabled());

        // Explicit set wins.
        set_send_system_keys(false);
        assert!(!send_system_keys_enabled());
        set_send_system_keys(true);
    }

    #[test]
    fn viewer_focus_flag_defaults_off_and_tracks() {
        // The hook starts gated CLOSED (unfocused) so a background viewer never swallows
        // the technician's local system keys until it actually gains focus.
        set_viewer_focused(false);
        assert!(!viewer_focused());

        set_viewer_focused(true);
        assert!(viewer_focused());

        set_viewer_focused(false);
        assert!(!viewer_focused());
    }

    #[cfg(windows)]
    #[test]
    fn win_keys_always_divert() {
        // Win / Apps keys divert regardless of modifier state.
        assert!(is_system_combo(vk::VK_LWIN, false, false));
        assert!(is_system_combo(vk::VK_RWIN, false, false));
        assert!(is_system_combo(vk::VK_APPS, false, false));
    }

    #[cfg(windows)]
    #[test]
    fn alt_tab_and_alt_esc_divert_only_with_alt() {
        assert!(is_system_combo(vk::VK_TAB, true, false)); // Alt+Tab
        assert!(!is_system_combo(vk::VK_TAB, false, false)); // plain Tab -> local path
        assert!(is_system_combo(vk::VK_ESCAPE, true, false)); // Alt+Esc
    }

    #[cfg(windows)]
    #[test]
    fn ctrl_esc_diverts_only_with_ctrl() {
        assert!(is_system_combo(vk::VK_ESCAPE, false, true)); // Ctrl+Esc
        assert!(!is_system_combo(vk::VK_ESCAPE, false, false)); // plain Esc -> local path
    }

    #[cfg(windows)]
    #[test]
    fn ordinary_keys_never_divert() {
        // 'R' is NOT itself a "system combo" — Win was already diverted (and forwarded
        // down), so R flows through the normal viewer path and composes Win+R on the remote.
        assert!(!is_system_combo(0x52, false, false)); // 'R'
        assert!(!is_system_combo(0x41, false, false)); // 'A'
        assert!(!is_system_combo(vk::VK_TAB, false, true)); // Ctrl+Tab is app-level, not a shell combo
    }
}
