//! Detect synthetic (script-driven) keyboard and mouse input.
//!
//! # Why this exists
//!
//! The desktop GUI's passphrase modal is a normal user-session
//! window. Any process running as the same user can drive it via
//! `SendInput` / `SendKeys` — fill the passphrase field, click
//! Submit, and unlock the vault. That defeats the entire "sudo
//! can't read your keys" promise the moment a malicious process
//! has shell access *and* knows the passphrase (or just waits for
//! the cached vault and then drives Peek/Revoke).
//!
//! # Defense
//!
//! Windows and macOS surface a per-event "this came from
//! `SendInput`" bit. We install a system-wide low-level hook,
//! count every injected event, snapshot the count when the modal
//! opens, and refuse to honor the Submit click if the count went
//! up while the modal was visible. The user sees a clear warning
//! and is told to use the CLI (or to enable virtual-keyboard /
//! Hardened tier, which adds independent defenses).
//!
//! # Platform coverage
//!
//! - **Windows**: full — `WH_KEYBOARD_LL` + `WH_MOUSE_LL` hooks
//!   running on a dedicated message-pump thread, checking
//!   `LLKHF_INJECTED` / `LLMHF_INJECTED`.
//! - **macOS**: full — `CGEventTap` with `kCGEventSourceStateID`
//!   on a dedicated `CFRunLoop` thread. Requires the user to grant
//!   envseal Accessibility permission in System Settings; without
//!   it `CGEventTapCreate` returns NULL and we fall back to the
//!   "use Hardened tier" guidance.
//! - **Linux**: not implementable on X11 (no per-event injection
//!   flag in the protocol). On Wayland it's compositor-specific.
//!   Linux users SHOULD use Hardened tier so the in-window virtual
//!   keyboard makes synthetic input ineffective even when we can't
//!   detect it.

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

/// Counts injected keyboard/mouse events the OS has reported since
/// process start. Monotonically increasing. Snapshot via
/// [`InjectionMonitor::count`].
static INJECTED_EVENTS: AtomicU64 = AtomicU64::new(0);

/// Handle to the injection monitor. The monitor itself runs on a
/// dedicated background thread; this handle is the cheap clonable
/// observer interface.
#[derive(Clone)]
pub struct InjectionMonitor {
    /// True when the platform implementation actually monitors
    /// input. False on platforms where we can't (Linux X11) — the
    /// GUI surfaces this so the user sees an informational note in
    /// the modal explaining that synthetic-input detection is off.
    supported: bool,
    counter: Arc<AtomicU64>,
}

impl InjectionMonitor {
    /// Spawn the platform monitor thread and return the observer
    /// handle. Cheap to call once at app startup.
    #[must_use]
    pub fn spawn() -> Self {
        let supported = platform::install_hooks();
        Self {
            supported,
            counter: Arc::new(AtomicU64::new(0)),
        }
    }

    /// Total injected events the platform has seen since process
    /// start. The modal stashes this on open and compares on submit.
    #[must_use]
    pub fn count(&self) -> u64 {
        // The static counter is what the platform hook updates; the
        // Arc-wrapped one in this struct is reserved for future
        // per-modal scoping but always tracks the static for now.
        let n = INJECTED_EVENTS.load(Ordering::Relaxed);
        self.counter.store(n, Ordering::Relaxed);
        n
    }

    /// Whether the running platform actually monitors injected input.
    /// Returns `true` on Windows and macOS (assuming the OS allows the
    /// hook to install — Windows policy or macOS Accessibility
    /// permission can deny). Returns `false` on Linux until a
    /// per-compositor Wayland implementation lands.
    #[must_use]
    pub fn supported(&self) -> bool {
        self.supported
    }
}

/// Bumped by the platform hook callback. Public to the platform
/// submodules only — external callers go through
/// [`InjectionMonitor::count`].
#[allow(dead_code)]
pub(crate) fn record_injection() {
    INJECTED_EVENTS.fetch_add(1, Ordering::Relaxed);
}

#[cfg(target_os = "windows")]
mod platform {
    //! Windows backend — `WH_KEYBOARD_LL` + `WH_MOUSE_LL` hooks.
    //!
    //! Low-level Windows hooks must run on a thread with an active
    //! message pump. We spawn a dedicated thread that does nothing
    //! but pump messages and host the hooks; the callbacks update
    //! the global atomic counter.

    use std::ffi::c_void;
    use std::ptr;

    // Win32 constants (avoid pulling in the windows crate for two ids).
    const WH_KEYBOARD_LL: i32 = 13;
    const WH_MOUSE_LL: i32 = 14;
    const LLKHF_INJECTED: u32 = 0x0000_0010;
    const LLMHF_INJECTED: u32 = 0x0000_0001;

    // Win32 type aliases. Names match MSDN; clippy's
    // upper-case-acronym lint is suppressed because matching the
    // platform documentation matters more than Rust naming purity
    // for FFI shims.
    #[allow(clippy::upper_case_acronyms)]
    type HHOOK = *mut c_void;
    #[allow(clippy::upper_case_acronyms)]
    type HINSTANCE = *mut c_void;
    #[allow(clippy::upper_case_acronyms)]
    type LRESULT = isize;
    #[allow(clippy::upper_case_acronyms)]
    type WPARAM = usize;
    #[allow(clippy::upper_case_acronyms)]
    type LPARAM = isize;
    type HookProc = unsafe extern "system" fn(i32, WPARAM, LPARAM) -> LRESULT;

    #[repr(C)]
    struct KbdLLHookStruct {
        vk_code: u32,
        scan_code: u32,
        flags: u32,
        time: u32,
        extra_info: usize,
    }

    #[repr(C)]
    struct MsLLHookStruct {
        pt_x: i32,
        pt_y: i32,
        mouse_data: u32,
        flags: u32,
        time: u32,
        extra_info: usize,
    }

    #[repr(C)]
    struct Msg {
        hwnd: *mut c_void,
        message: u32,
        w_param: WPARAM,
        l_param: LPARAM,
        time: u32,
        pt_x: i32,
        pt_y: i32,
        l_private: u32,
    }

    #[link(name = "user32")]
    extern "system" {
        fn SetWindowsHookExW(
            id_hook: i32,
            lpfn: HookProc,
            hmod: HINSTANCE,
            dw_thread_id: u32,
        ) -> HHOOK;
        fn CallNextHookEx(hhk: HHOOK, n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT;
        fn GetMessageW(msg: *mut Msg, hwnd: *mut c_void, min: u32, max: u32) -> i32;
    }

    unsafe extern "system" fn keyboard_hook(
        n_code: i32,
        w_param: WPARAM,
        l_param: LPARAM,
    ) -> LRESULT {
        if n_code >= 0 && l_param != 0 {
            // l_param is a KBDLLHOOKSTRUCT pointer. Defensive cast.
            let p = l_param as *const KbdLLHookStruct;
            if !p.is_null() {
                let s = unsafe { &*p };
                if s.flags & LLKHF_INJECTED != 0 {
                    super::record_injection();
                }
            }
        }
        unsafe { CallNextHookEx(ptr::null_mut(), n_code, w_param, l_param) }
    }

    unsafe extern "system" fn mouse_hook(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
        if n_code >= 0 && l_param != 0 {
            let p = l_param as *const MsLLHookStruct;
            if !p.is_null() {
                let s = unsafe { &*p };
                if s.flags & LLMHF_INJECTED != 0 {
                    super::record_injection();
                }
            }
        }
        unsafe { CallNextHookEx(ptr::null_mut(), n_code, w_param, l_param) }
    }

    pub(super) fn install_hooks() -> bool {
        std::thread::Builder::new()
            .name("envseal-injection-monitor".to_string())
            .spawn(|| unsafe {
                // SAFETY: SetWindowsHookExW with a valid extern "system"
                // function pointer is the documented Win32 contract.
                // The hook lives for the lifetime of this thread; we
                // never unhook because the thread runs forever.
                let kb = SetWindowsHookExW(WH_KEYBOARD_LL, keyboard_hook, ptr::null_mut(), 0);
                let ms = SetWindowsHookExW(WH_MOUSE_LL, mouse_hook, ptr::null_mut(), 0);
                if kb.is_null() && ms.is_null() {
                    // Both failed — likely permission-denied (some
                    // policies block global hooks). Thread exits;
                    // monitor will report zero injections forever,
                    // which is honest — we couldn't see them.
                    return;
                }
                // Pump messages forever so the hook callbacks fire.
                let mut msg: Msg = std::mem::zeroed();
                while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) > 0 {
                    // Hook callbacks are invoked by the OS during
                    // GetMessageW; we do not need to dispatch.
                }
            })
            .is_ok()
    }
}

#[cfg(target_os = "macos")]
mod platform {
    //! macOS backend — `CGEventTapCreate` filtering keyboard + mouse
    //! events, examining `kCGEventSourceStateID` per event.
    //!
    //! Real hardware events carry the
    //! `kCGEventSourceStateHIDSystemState` (= 1) state ID. Anything
    //! else — events from `CGEventPost`, AppleScript "tell
    //! application … keystroke …", `osascript`, or any third-party
    //! automation framework — has a different state ID and counts
    //! as injected.
    //!
    //! The tap is installed on a dedicated thread with its own
    //! `CFRunLoop`. `CGEventTapCreate` requires the user to grant
    //! envseal Accessibility permission in System Settings →
    //! Privacy & Security → Accessibility. If permission is denied,
    //! `CGEventTapCreate` returns NULL and we return `false` so the
    //! GUI surfaces the "use Hardened tier" note.
    //!
    //! Raw FFI rather than the `core-foundation` crate — keeps
    //! desktop-native's transitive dep tree small for a feature
    //! that is two function calls plus a runloop pump.

    use std::ffi::c_void;
    use std::ptr;

    // CG / CF type aliases. Names match Apple's headers.
    type CFMachPortRef = *mut c_void;
    type CFRunLoopSourceRef = *mut c_void;
    type CFRunLoopRef = *mut c_void;
    type CFAllocatorRef = *mut c_void;
    type CFStringRef = *const c_void;
    type CGEventRef = *mut c_void;
    type CGEventTapProxy = *mut c_void;
    type CGEventMask = u64;

    // CGEventTapLocation
    const K_CG_SESSION_EVENT_TAP: u32 = 1;
    // CGEventTapPlacement
    const K_CG_HEAD_INSERT_EVENT_TAP: u32 = 0;
    // CGEventTapOptions
    const K_CG_EVENT_TAP_OPTION_LISTEN_ONLY: u32 = 1;

    // CGEventType masks (1 << type)
    const K_CG_EVENT_KEY_DOWN: u32 = 10;
    const K_CG_EVENT_LEFT_MOUSE_DOWN: u32 = 1;
    const K_CG_EVENT_RIGHT_MOUSE_DOWN: u32 = 3;
    const K_CG_EVENT_OTHER_MOUSE_DOWN: u32 = 25;

    // CGEventField — kCGEventSourceStateID
    const K_CG_EVENT_SOURCE_STATE_ID: u32 = 45;

    // kCGEventSourceStateHIDSystemState — real hardware
    const K_CG_EVENT_SOURCE_STATE_HID_SYSTEM_STATE: i64 = 1;

    type CGEventTapCallBack = unsafe extern "C" fn(
        proxy: CGEventTapProxy,
        event_type: u32,
        event: CGEventRef,
        user_info: *mut c_void,
    ) -> CGEventRef;

    #[link(name = "CoreGraphics", kind = "framework")]
    extern "C" {
        fn CGEventTapCreate(
            tap: u32,
            place: u32,
            options: u32,
            events_of_interest: CGEventMask,
            callback: CGEventTapCallBack,
            user_info: *mut c_void,
        ) -> CFMachPortRef;
        fn CGEventGetIntegerValueField(event: CGEventRef, field: u32) -> i64;
    }

    #[link(name = "CoreFoundation", kind = "framework")]
    extern "C" {
        fn CFMachPortCreateRunLoopSource(
            allocator: CFAllocatorRef,
            port: CFMachPortRef,
            order: isize,
        ) -> CFRunLoopSourceRef;
        fn CFRunLoopGetCurrent() -> CFRunLoopRef;
        fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFStringRef);
        fn CFRunLoopRun();
        static kCFRunLoopCommonModes: CFStringRef;
    }

    unsafe extern "C" fn tap_callback(
        _proxy: CGEventTapProxy,
        _event_type: u32,
        event: CGEventRef,
        _user_info: *mut c_void,
    ) -> CGEventRef {
        if !event.is_null() {
            let state = unsafe { CGEventGetIntegerValueField(event, K_CG_EVENT_SOURCE_STATE_ID) };
            if state != K_CG_EVENT_SOURCE_STATE_HID_SYSTEM_STATE {
                super::record_injection();
            }
        }
        // Pass the event through unchanged — kCGEventTapOptionListenOnly
        // already prevents modification but Apple's API still requires
        // returning the event for the chain.
        event
    }

    pub(super) fn install_hooks() -> bool {
        std::thread::Builder::new()
            .name("envseal-injection-monitor".to_string())
            .spawn(|| unsafe {
                let mask: CGEventMask = (1 << K_CG_EVENT_KEY_DOWN)
                    | (1 << K_CG_EVENT_LEFT_MOUSE_DOWN)
                    | (1 << K_CG_EVENT_RIGHT_MOUSE_DOWN)
                    | (1 << K_CG_EVENT_OTHER_MOUSE_DOWN);

                let tap = CGEventTapCreate(
                    K_CG_SESSION_EVENT_TAP,
                    K_CG_HEAD_INSERT_EVENT_TAP,
                    K_CG_EVENT_TAP_OPTION_LISTEN_ONLY,
                    mask,
                    tap_callback,
                    ptr::null_mut(),
                );

                if tap.is_null() {
                    // Accessibility permission not granted — tap
                    // creation returned NULL. Thread exits; the
                    // global counter never increments, the modal's
                    // `monitor_supported` reads false, and the
                    // user sees the "use Hardened tier" note.
                    return;
                }

                let source = CFMachPortCreateRunLoopSource(ptr::null_mut(), tap, 0);
                if source.is_null() {
                    return;
                }

                let runloop = CFRunLoopGetCurrent();
                CFRunLoopAddSource(runloop, source, kCFRunLoopCommonModes);

                CFRunLoopRun();
            })
            .is_ok()
    }
}

#[cfg(not(any(target_os = "windows", target_os = "macos")))]
mod platform {
    //! Linux fallback. Not implementable on X11 — the protocol has
    //! no per-event "this came from XSendEvent" flag (clients can
    //! set the synthetic bit but most injection tools do not).
    //! Wayland surfaces injected events differently per compositor;
    //! a uniform Linux implementation isn't possible without
    //! per-compositor backends.
    //!
    //! Until a Wayland-specific implementation lands, Linux users
    //! should enable Hardened tier so the in-window randomized
    //! virtual keyboard defeats synthetic keystrokes regardless of
    //! whether we can detect them.

    pub(super) fn install_hooks() -> bool {
        false
    }
}

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

    #[test]
    fn record_injection_increments_global_counter() {
        let monitor = InjectionMonitor::spawn();
        let before = monitor.count();
        record_injection();
        record_injection();
        let after = monitor.count();
        assert!(
            after >= before + 2,
            "global counter must reflect recorded injections: before={before} after={after}"
        );
    }

    #[test]
    fn supported_flag_matches_target_os() {
        let monitor = InjectionMonitor::spawn();
        if cfg!(target_os = "windows") || cfg!(target_os = "macos") {
            // Both attempt to install the hook — `supported` reflects
            // whether the monitor thread *spawned*, not whether the
            // OS subsequently permitted the hook (Windows policy /
            // macOS Accessibility permission can deny at runtime;
            // that surfaces as a flat-zero injection counter, which
            // the modal handles independently).
            assert!(monitor.supported());
        } else {
            assert!(!monitor.supported());
        }
    }
}
