use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tauri::{Emitter, Manager};

// ── Types ────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShortcutSlot {
    QuickThought,
    Dictation,
}

impl ShortcutSlot {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::QuickThought => "quick_thought",
            Self::Dictation => "dictation",
        }
    }

    pub fn from_str(s: &str) -> Result<Self, String> {
        match s {
            "quick_thought" => Ok(Self::QuickThought),
            "dictation" => Ok(Self::Dictation),
            _ => Err(format!("Unknown shortcut slot: {}", s)),
        }
    }

    fn label(&self) -> &'static str {
        match self {
            Self::QuickThought => "Quick Thought",
            Self::Dictation => "Dictation",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShortcutBackend {
    /// Carbon RegisterEventHotKey via tauri-plugin-global-shortcut. No permissions.
    Standard,
    /// CGEventTap via hotkey_macos.rs. Requires Input Monitoring.
    Native,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureStyle {
    Hold,
    Locked,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShortcutStatus {
    pub slot: String,
    pub enabled: bool,
    pub pending: bool,
    pub shortcut: String,
    pub keycode: i64,
    pub backend: String,
    pub needs_permission: bool,
    pub message: String,
}

// ── Constants ────────────────────────────────────────────────

const HOLD_THRESHOLD_MS: u64 = 300;
const MIN_CAPTURE_DURATION_MS: u64 = 400;

/// macOS virtual keycodes that require the native CGEventTap backend
/// because Carbon RegisterEventHotKey cannot intercept them.
const NATIVE_KEYCODES: &[i64] = &[
    57, // Caps Lock
    63, // fn/Globe
];

// ── Classification ───────────────────────────────────────────

/// Determine which backend a shortcut needs based on its keycode.
/// Keycodes in NATIVE_KEYCODES require CGEventTap (Input Monitoring).
/// Keycode -1 means "standard combo, no specific keycode."
pub fn classify_shortcut(keycode: i64) -> ShortcutBackend {
    if NATIVE_KEYCODES.contains(&keycode) {
        ShortcutBackend::Native
    } else {
        ShortcutBackend::Standard
    }
}

// ── State Machine ────────────────────────────────────────────

/// Unified three-mode state machine for shortcut-triggered actions.
///
/// Modes:
/// - Hold-to-talk: keydown starts capture immediately, release at/after
///   HOLD_THRESHOLD_MS stops and finalizes.
/// - Tap-to-lock: keydown starts capture immediately, release before
///   HOLD_THRESHOLD_MS keeps the session capturing until the next tap.
/// - Quick-tap-discard: captures shorter than MIN_CAPTURE_DURATION_MS are discarded.
#[derive(Debug, Default)]
pub struct ShortcutStateMachine {
    pub key_down: bool,
    pub key_down_started_at: Option<Instant>,
    pub active_capture: Option<CaptureStyle>,
    pub capture_started_at: Option<Instant>,
}

/// Actions the state machine tells the caller to take.
#[derive(Debug)]
pub enum StateMachineAction {
    /// Start a new session in hold mode (user is holding the key).
    StartHold,
    /// Start a new session in locked mode (user tapped quickly).
    StartLocked,
    /// Stop the active session (release after hold, or second tap after lock).
    Stop { discard: bool },
    /// No action (key event consumed but nothing to do).
    None,
}

impl ShortcutStateMachine {
    pub fn clear(&mut self) {
        self.key_down = false;
        self.key_down_started_at = None;
        self.active_capture = None;
        self.capture_started_at = None;
    }

    /// Record that a session was started with the given capture style.
    pub fn mark_session_started(&mut self, style: CaptureStyle) {
        self.active_capture = Some(style);
        self.capture_started_at = Some(Instant::now());
    }

    /// Handle a key press event. Returns the action to take.
    ///
    /// For Standard backend (global-shortcut plugin), this is called on ShortcutState::Pressed.
    /// For Native backend (CGEventTap), this is called on HotkeyEvent::Press.
    pub fn handle_press(&mut self, is_session_active: bool) -> StateMachineAction {
        if self.key_down {
            return StateMachineAction::None; // Key repeat, ignore
        }
        self.key_down = true;
        self.key_down_started_at = Some(Instant::now());

        if is_session_active || self.active_capture.is_some() {
            return StateMachineAction::None;
        }

        self.active_capture = Some(CaptureStyle::Hold);
        self.capture_started_at = Some(Instant::now());
        StateMachineAction::StartHold
    }

    /// Handle a key release event. Returns the action to take.
    pub fn handle_release(&mut self, is_session_active: bool) -> StateMachineAction {
        let pressed_at = self.key_down_started_at;
        self.key_down = false;
        self.key_down_started_at = None;

        let was_short_tap = pressed_at
            .map(|pressed| {
                Instant::now().duration_since(pressed).as_millis() < HOLD_THRESHOLD_MS as u128
            })
            .unwrap_or(false);

        // A quick release latches the already-started hold capture into locked
        // mode. A release at/after the threshold is a normal hold-to-talk stop.
        if matches!(self.active_capture, Some(CaptureStyle::Hold)) {
            if was_short_tap {
                self.active_capture = Some(CaptureStyle::Locked);
                return StateMachineAction::None;
            }
            let discard = self.should_discard_capture();
            self.active_capture = None;
            self.capture_started_at = None;
            return StateMachineAction::Stop { discard };
        }

        if matches!(self.active_capture, Some(CaptureStyle::Locked)) && is_session_active {
            self.active_capture = None;
            self.capture_started_at = None;
            return StateMachineAction::Stop { discard: false };
        }

        // Not a short tap = we were waiting for hold but never started. Ignore.
        if !was_short_tap {
            return StateMachineAction::None;
        }

        // Short tap while session is active = stop (locked mode off).
        if is_session_active {
            self.active_capture = None;
            self.capture_started_at = None;
            return StateMachineAction::Stop { discard: false };
        }

        // Short tap with no active session = start locked mode.
        StateMachineAction::StartLocked
    }

    /// Whether the current capture should be discarded (too short to be useful).
    fn should_discard_capture(&self) -> bool {
        self.capture_started_at
            .map(|started| {
                Instant::now().duration_since(started).as_millis() < MIN_CAPTURE_DURATION_MS as u128
            })
            .unwrap_or(false)
    }
}

// ── Active Shortcut Registration ─────────────────────────────

pub(crate) struct ActiveShortcut {
    shortcut_string: String,
    keycode: i64,
    backend: ShortcutBackend,
    state_machine: ShortcutStateMachine,
    /// For Standard backend: the shortcut ID for matching global-shortcut events.
    standard_shortcut_id: Option<u32>,
    /// For Native backend (macOS only): the running HotkeyMonitor.
    #[cfg(target_os = "macos")]
    native_monitor: Option<minutes_core::hotkey_macos::HotkeyMonitor>,
    /// Lifecycle for native monitors.
    native_lifecycle: NativeLifecycle,
    native_generation: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NativeLifecycle {
    NotApplicable,
    Starting,
    Active,
    Failed,
}

// ── ShortcutManager ──────────────────────────────────────────

pub struct ShortcutManager {
    slots: HashMap<ShortcutSlot, ActiveShortcut>,
    last_error: HashMap<ShortcutSlot, String>,
}

impl ShortcutManager {
    pub fn new() -> Self {
        Self {
            slots: HashMap::new(),
            last_error: HashMap::new(),
        }
    }

    /// Register a shortcut for a slot. Unregisters the previous one first.
    pub fn register(
        &mut self,
        slot: ShortcutSlot,
        shortcut_string: String,
        keycode: i64,
        app: &tauri::AppHandle,
    ) -> Result<ShortcutStatus, String> {
        let backend = classify_shortcut(keycode);

        // Conflict detection: check the other slot by both string and keycode.
        for (&other_slot, other_active) in &self.slots {
            if other_slot == slot {
                continue;
            }
            let string_conflict = other_active.shortcut_string == shortcut_string;
            let keycode_conflict =
                keycode >= 0 && other_active.keycode >= 0 && other_active.keycode == keycode;
            if string_conflict || keycode_conflict {
                return Err(format!(
                    "{} is already used by {}. Choose a different shortcut.",
                    shortcut_string,
                    other_slot.label()
                ));
            }
        }

        // Unregister the previous shortcut for this slot.
        self.unregister(slot, app)?;

        match backend {
            ShortcutBackend::Standard => {
                self.register_standard(slot, shortcut_string, keycode, app)
            }
            ShortcutBackend::Native => self.register_native(slot, shortcut_string, keycode, app),
        }
    }

    fn register_standard(
        &mut self,
        slot: ShortcutSlot,
        shortcut_string: String,
        keycode: i64,
        app: &tauri::AppHandle,
    ) -> Result<ShortcutStatus, String> {
        use tauri_plugin_global_shortcut::GlobalShortcutExt;

        // Parse the shortcut string first to get the ID for event matching.
        // If parsing fails, don't register -- the string is malformed.
        let parsed_shortcut =
            <tauri_plugin_global_shortcut::Shortcut as std::str::FromStr>::from_str(
                shortcut_string.as_str(),
            )
            .map_err(|e| format!("Invalid shortcut format: {}. ({})", shortcut_string, e))?;
        let shortcut_id = Some(parsed_shortcut.id());

        let manager = app.global_shortcut();
        if let Err(e) = manager.register(shortcut_string.as_str()) {
            let msg = format!(
                "Could not register {}. Another app may already be using it. ({})",
                shortcut_string, e
            );
            self.last_error.insert(slot, msg.clone());
            return Err(msg);
        }

        self.last_error.remove(&slot);
        self.slots.insert(
            slot,
            ActiveShortcut {
                shortcut_string: shortcut_string.clone(),
                keycode,
                backend: ShortcutBackend::Standard,
                state_machine: ShortcutStateMachine::default(),
                standard_shortcut_id: shortcut_id,
                #[cfg(target_os = "macos")]
                native_monitor: None,
                native_lifecycle: NativeLifecycle::NotApplicable,
                native_generation: 0,
            },
        );

        Ok(self.build_status(slot))
    }

    #[allow(unused_variables)]
    fn register_native(
        &mut self,
        slot: ShortcutSlot,
        shortcut_string: String,
        keycode: i64,
        app: &tauri::AppHandle,
    ) -> Result<ShortcutStatus, String> {
        #[cfg(target_os = "macos")]
        {
            self.register_native_macos(slot, shortcut_string, keycode, app)
        }
        #[cfg(not(target_os = "macos"))]
        {
            Err("Native hotkey (Caps Lock, fn) is only available on macOS.".into())
        }
    }

    #[cfg(target_os = "macos")]
    fn register_native_macos(
        &mut self,
        slot: ShortcutSlot,
        shortcut_string: String,
        keycode: i64,
        app: &tauri::AppHandle,
    ) -> Result<ShortcutStatus, String> {
        use minutes_core::hotkey_macos::HotkeyMonitor;

        let generation: u64 = self
            .slots
            .get(&slot)
            .map(|s| s.native_generation.wrapping_add(1))
            .unwrap_or(1);

        // Insert placeholder while monitor starts
        self.slots.insert(
            slot,
            ActiveShortcut {
                shortcut_string: shortcut_string.clone(),
                keycode,
                backend: ShortcutBackend::Native,
                state_machine: ShortcutStateMachine::default(),
                standard_shortcut_id: None,
                native_monitor: None,
                native_lifecycle: NativeLifecycle::Starting,
                native_generation: generation,
            },
        );
        self.last_error.remove(&slot);

        // We need to use a shared reference for the callbacks since HotkeyMonitor
        // takes ownership of the closures. The ShortcutManager is behind an
        // Arc<Mutex<>> in AppState, so the callbacks will lock it.
        let app_for_events = app.clone();
        let app_for_status = app.clone();

        let monitor = HotkeyMonitor::start(
            keycode,
            move |event| {
                handle_native_event_callback(&app_for_events, slot, event);
            },
            move |status| {
                handle_native_status_callback(&app_for_status, slot, generation, status);
            },
        )?;

        // Store the monitor
        if let Some(active) = self.slots.get_mut(&slot) {
            if active.native_generation == generation {
                active.native_monitor = Some(monitor);
            } else {
                // Generation changed while we were starting, stop this one
                monitor.stop();
            }
        }

        Ok(self.build_status(slot))
    }

    /// Unregister the shortcut for a slot.
    pub fn unregister(&mut self, slot: ShortcutSlot, app: &tauri::AppHandle) -> Result<(), String> {
        if let Some(active) = self.slots.remove(&slot) {
            match active.backend {
                ShortcutBackend::Standard => {
                    use tauri_plugin_global_shortcut::GlobalShortcutExt;
                    let manager = app.global_shortcut();
                    // Ignore unregister errors (shortcut may already be gone)
                    let _ = manager.unregister(active.shortcut_string.as_str());
                }
                ShortcutBackend::Native =>
                {
                    #[cfg(target_os = "macos")]
                    if let Some(monitor) = active.native_monitor {
                        monitor.stop();
                    }
                }
            }
        }
        self.last_error.remove(&slot);
        Ok(())
    }

    /// Find which slot owns a given shortcut ID (for the global-shortcut handler).
    pub fn find_slot_for_shortcut_id(&self, shortcut_id: u32) -> Option<ShortcutSlot> {
        self.slots
            .iter()
            .find(|(_, active)| active.standard_shortcut_id == Some(shortcut_id))
            .map(|(&slot, _)| slot)
    }

    /// Handle a press event for a slot. Returns the action to execute AFTER the
    /// caller drops the lock.
    pub fn handle_press(
        &mut self,
        slot: ShortcutSlot,
        is_session_active: bool,
    ) -> (ShortcutSlot, StateMachineAction) {
        let active = match self.slots.get_mut(&slot) {
            Some(active) => active,
            None => return (slot, StateMachineAction::None),
        };

        let action = active.state_machine.handle_press(is_session_active);
        (slot, action)
    }

    /// Handle a release event for a slot. Returns the action to execute
    /// AFTER the caller drops the lock.
    pub fn handle_release(
        &mut self,
        slot: ShortcutSlot,
        is_session_active: bool,
    ) -> (ShortcutSlot, StateMachineAction) {
        let active = match self.slots.get_mut(&slot) {
            Some(active) => active,
            None => return (slot, StateMachineAction::None),
        };

        let action = active.state_machine.handle_release(is_session_active);
        (slot, action)
    }

    /// Update native monitor lifecycle status.
    #[cfg(target_os = "macos")]
    pub fn update_native_status(
        &mut self,
        slot: ShortcutSlot,
        generation: u64,
        lifecycle: NativeLifecycle,
        error: Option<String>,
    ) {
        if let Some(active) = self.slots.get_mut(&slot) {
            if active.native_generation != generation {
                return; // Stale update
            }
            active.native_lifecycle = lifecycle;
            if lifecycle == NativeLifecycle::Failed {
                if let Some(monitor) = active.native_monitor.take() {
                    monitor.stop();
                }
            }
        }
        if let Some(err) = error {
            self.last_error.insert(slot, err);
        } else {
            self.last_error.remove(&slot);
        }
    }

    /// Build status for a slot.
    pub fn build_status(&self, slot: ShortcutSlot) -> ShortcutStatus {
        match self.slots.get(&slot) {
            Some(active) => {
                let needs_permission = active.backend == ShortcutBackend::Native && {
                    #[cfg(target_os = "macos")]
                    {
                        !minutes_core::hotkey_macos::is_input_monitoring_granted()
                    }
                    #[cfg(not(target_os = "macos"))]
                    {
                        false
                    }
                };

                let enabled = match active.backend {
                    ShortcutBackend::Standard => true,
                    ShortcutBackend::Native => active.native_lifecycle == NativeLifecycle::Active,
                };
                let pending = active.native_lifecycle == NativeLifecycle::Starting;

                let message = if let Some(err) = self.last_error.get(&slot) {
                    err.clone()
                } else if pending {
                    format!("Starting {}...", active.shortcut_string)
                } else if needs_permission {
                    "Requires Input Monitoring permission.".into()
                } else if enabled {
                    format!(
                        "Active. Hold {} to {}, or tap to lock and tap again to stop.",
                        active.shortcut_string,
                        match slot {
                            ShortcutSlot::QuickThought => "record a quick thought",
                            ShortcutSlot::Dictation => "dictate",
                        }
                    )
                } else {
                    "Off.".into()
                };

                ShortcutStatus {
                    slot: slot.as_str().into(),
                    enabled,
                    pending,
                    shortcut: active.shortcut_string.clone(),
                    keycode: active.keycode,
                    backend: match active.backend {
                        ShortcutBackend::Standard => "standard".into(),
                        ShortcutBackend::Native => "native".into(),
                    },
                    needs_permission,
                    message,
                }
            }
            None => {
                // Inactive slot: surface the PERSISTED config shortcut rather
                // than the compile-time default. Otherwise a disabled slot with
                // a custom chord reports the default, and toggling it back on
                // (which echoes the reported shortcut) would clobber the user's
                // saved chord. `Config::load()` here is cheap and only happens
                // for inactive slots — not on any per-keystroke hot path.
                let (shortcut, keycode, backend) =
                    persisted_shortcut_for_slot(&minutes_core::Config::load(), slot);
                ShortcutStatus {
                    slot: slot.as_str().into(),
                    enabled: false,
                    pending: false,
                    shortcut,
                    keycode,
                    backend: backend.into(),
                    needs_permission: false,
                    message: "Off.".into(),
                }
            }
        }
    }

    /// Mark a session as started for a slot's state machine.
    pub fn mark_session_started(&mut self, slot: ShortcutSlot, style: CaptureStyle) {
        if let Some(active) = self.slots.get_mut(&slot) {
            active.state_machine.mark_session_started(style);
        }
    }
}

// ── Defaults ─────────────────────────────────────────────────

pub fn default_shortcut_for_slot(slot: ShortcutSlot) -> &'static str {
    match slot {
        ShortcutSlot::QuickThought => "CmdOrCtrl+Shift+M",
        ShortcutSlot::Dictation => "CmdOrCtrl+Shift+Space",
    }
}

/// Resolve the persisted shortcut for a slot from config, for use when the slot
/// is currently inactive (not registered in the ShortcutManager).
///
/// Returns `(shortcut_string, keycode, backend_label)`. This mirrors the
/// canonical startup-restore mapping in `main.rs`: for dictation the native
/// keycode variant (CapsLock=57 / fn=63) takes precedence when
/// `dictation.hotkey_enabled` is set, otherwise the standard chord
/// (`dictation.shortcut`, keycode -1) is used. Quick Thought reads
/// `global_hotkey.shortcut` (keycode -1, always standard backend).
///
/// Falls back to the compile-time defaults only when a config field is empty,
/// so a user's saved-but-disabled custom chord is never replaced by the default.
pub fn persisted_shortcut_for_slot(
    config: &minutes_core::Config,
    slot: ShortcutSlot,
) -> (String, i64, &'static str) {
    match slot {
        ShortcutSlot::Dictation => {
            if config.dictation.hotkey_enabled {
                let kc = config.dictation.hotkey_keycode;
                let label = if kc == 63 { "fn" } else { "CapsLock" };
                (label.to_string(), kc, "native")
            } else if !config.dictation.shortcut.is_empty() {
                (config.dictation.shortcut.clone(), -1, "standard")
            } else {
                (default_shortcut_for_slot(slot).to_string(), -1, "standard")
            }
        }
        ShortcutSlot::QuickThought => {
            if !config.global_hotkey.shortcut.is_empty() {
                (config.global_hotkey.shortcut.clone(), -1, "standard")
            } else {
                (default_shortcut_for_slot(slot).to_string(), -1, "standard")
            }
        }
    }
}

// ── Helpers ──────────────────────────────────────────────────

/// Fast in-memory check if a session is active. No filesystem I/O.
/// Safe to call from any thread, including CGEventTap callbacks.
pub fn is_slot_session_active_fast(app: &tauri::AppHandle, slot: ShortcutSlot) -> bool {
    let state = match app.try_state::<crate::commands::AppState>() {
        Some(s) => s,
        None => return false,
    };
    match slot {
        ShortcutSlot::QuickThought => {
            // Only block on active recording/starting, NOT processing.
            // Processing happens in the background and should not prevent new captures.
            state.recording.load(Ordering::Relaxed) || state.starting.load(Ordering::Relaxed)
        }
        ShortcutSlot::Dictation => state.dictation_active.load(Ordering::Relaxed),
    }
}

/// Execute a state machine action for a slot.
/// IMPORTANT: This must be called AFTER the ShortcutManager lock is dropped.
/// It may re-acquire the lock briefly to update state on success.
pub fn execute_action(app: &tauri::AppHandle, slot: ShortcutSlot, action: StateMachineAction) {
    match action {
        StateMachineAction::StartHold => {
            if let Err(e) = start_slot_session(app, slot, CaptureStyle::Hold) {
                // Revert the mark on failure
                if let Some(mgr_state) = app.try_state::<Arc<Mutex<ShortcutManager>>>() {
                    if let Ok(mut mgr) = mgr_state.lock() {
                        if let Some(active) = mgr.slots.get_mut(&slot) {
                            active.state_machine.clear();
                        }
                    }
                }
                crate::commands::show_user_notification(app, slot.label(), &e);
            }
        }
        StateMachineAction::StartLocked => {
            if let Some(mgr_state) = app.try_state::<Arc<Mutex<ShortcutManager>>>() {
                if let Ok(mut mgr) = mgr_state.lock() {
                    mgr.mark_session_started(slot, CaptureStyle::Locked);
                }
            }
            if let Err(e) = start_slot_session(app, slot, CaptureStyle::Locked) {
                if let Some(mgr_state) = app.try_state::<Arc<Mutex<ShortcutManager>>>() {
                    if let Ok(mut mgr) = mgr_state.lock() {
                        if let Some(active) = mgr.slots.get_mut(&slot) {
                            active.state_machine.clear();
                        }
                    }
                }
                crate::commands::show_user_notification(app, slot.label(), &e);
            }
        }
        StateMachineAction::Stop { discard } => {
            stop_slot_session(app, slot, discard);
        }
        StateMachineAction::None => {}
    }
}

/// Start a session for a slot.
fn start_slot_session(
    app: &tauri::AppHandle,
    slot: ShortcutSlot,
    style: CaptureStyle,
) -> Result<(), String> {
    match slot {
        ShortcutSlot::QuickThought => {
            let state = app.state::<crate::commands::AppState>();
            let capture_style = match style {
                CaptureStyle::Hold => crate::commands::HotkeyCaptureStyle::Hold,
                CaptureStyle::Locked => crate::commands::HotkeyCaptureStyle::Locked,
            };
            // Set up runtime tracking for the old start_recording path
            if let Ok(mut runtime) = state.hotkey_runtime.lock() {
                runtime.active_capture = Some(capture_style);
                runtime.recording_started_at = Some(Instant::now());
            }
            state
                .discard_short_hotkey_capture
                .store(false, Ordering::Relaxed);
            let hotkey_runtime = state.hotkey_runtime.clone();
            let discard_flag = state.discard_short_hotkey_capture.clone();
            crate::commands::launch_recording(
                app.clone(),
                &state,
                minutes_core::CaptureMode::QuickThought,
                Some(minutes_core::capture::RecordingIntent::Memo),
                false,
                None,
                None,
                Some(hotkey_runtime),
                Some(discard_flag),
            )
            // QuickThought is not a meeting, so Require never intercepts it;
            // map the launch outcome back to this fn's unit result.
            .map(|_| ())
        }
        ShortcutSlot::Dictation => {
            let capture = match style {
                CaptureStyle::Hold => Some(crate::commands::HotkeyCaptureStyle::Hold),
                CaptureStyle::Locked => Some(crate::commands::HotkeyCaptureStyle::Locked),
            };
            crate::commands::start_dictation_session_public(app, capture)
        }
    }
}

/// Stop a session for a slot.
fn stop_slot_session(app: &tauri::AppHandle, slot: ShortcutSlot, discard: bool) {
    let state = match app.try_state::<crate::commands::AppState>() {
        Some(s) => s,
        None => return,
    };

    match slot {
        ShortcutSlot::QuickThought => {
            if discard {
                state
                    .discard_short_hotkey_capture
                    .store(true, Ordering::Relaxed);
            }
            if let Ok(mut runtime) = state.hotkey_runtime.lock() {
                runtime.active_capture = None;
                runtime.recording_started_at = None;
            }
            if let Err(e) = crate::commands::request_stop(&state.recording, &state.stop_flag) {
                crate::commands::show_user_notification(
                    app,
                    "Quick Thought",
                    &format!("Could not stop recording: {}", e),
                );
            }
        }
        ShortcutSlot::Dictation => {
            if let Ok(mut released_at) = state.dictation_release_started_at.lock() {
                *released_at = Some(Instant::now());
            }
            state.dictation_stop_flag.store(true, Ordering::Relaxed);
        }
    }
}

/// Emit shortcut status for a slot.
pub fn emit_status(app: &tauri::AppHandle, slot: ShortcutSlot) {
    if let Some(mgr_state) = app.try_state::<Arc<Mutex<ShortcutManager>>>() {
        if let Ok(mgr) = mgr_state.lock() {
            let status = mgr.build_status(slot);
            app.emit("shortcut:status", &status).ok();
        }
    }
}

// ── Native backend callbacks (called from CGEventTap thread) ─

#[cfg(target_os = "macos")]
fn handle_native_event_callback(
    app: &tauri::AppHandle,
    slot: ShortcutSlot,
    event: minutes_core::hotkey_macos::HotkeyEvent,
) {
    let mgr_state = match app.try_state::<Arc<Mutex<ShortcutManager>>>() {
        Some(state) => state,
        None => return,
    };

    match event {
        minutes_core::hotkey_macos::HotkeyEvent::Press => {
            if slot == ShortcutSlot::Dictation {
                crate::commands::capture_pending_dictation_target(app);
            }
            let action = {
                let mut mgr = match mgr_state.lock() {
                    Ok(mgr) => mgr,
                    Err(e) => {
                        eprintln!("[shortcut_manager] mutex poisoned on press: {}", e);
                        return;
                    }
                };
                let session_active = is_slot_session_active_fast(app, slot);
                let (_slot, action) = mgr.handle_press(slot, session_active);
                action
            }; // lock dropped
            if !matches!(action, StateMachineAction::None) {
                let app_clone = app.clone();
                std::thread::spawn(move || {
                    execute_action(&app_clone, slot, action);
                });
            }
        }
        minutes_core::hotkey_macos::HotkeyEvent::Release => {
            let session_active = is_slot_session_active_fast(app, slot);
            let (_slot, action) = {
                let mut mgr = match mgr_state.lock() {
                    Ok(mgr) => mgr,
                    Err(e) => {
                        eprintln!("[shortcut_manager] mutex poisoned on release: {}", e);
                        return;
                    }
                };
                mgr.handle_release(slot, session_active)
            }; // lock dropped

            // Spawn action off the CGEventTap thread to avoid blocking the
            // macOS HID event pipeline with file I/O and window creation.
            if !matches!(action, StateMachineAction::None) {
                let app_clone = app.clone();
                std::thread::spawn(move || {
                    execute_action(&app_clone, slot, action);
                });
            }
        }
    }
}

#[cfg(target_os = "macos")]
fn handle_native_status_callback(
    app: &tauri::AppHandle,
    slot: ShortcutSlot,
    generation: u64,
    status: minutes_core::hotkey_macos::HotkeyMonitorStatus,
) {
    use minutes_core::hotkey_macos::HotkeyMonitorStatus;

    let mgr_state = match app.try_state::<Arc<Mutex<ShortcutManager>>>() {
        Some(state) => state,
        None => return,
    };

    let should_prompt = {
        let mut mgr = match mgr_state.lock() {
            Ok(mgr) => mgr,
            Err(_) => return,
        };

        match status {
            HotkeyMonitorStatus::Starting => {
                mgr.update_native_status(slot, generation, NativeLifecycle::Starting, None);
                false
            }
            HotkeyMonitorStatus::Active => {
                mgr.update_native_status(slot, generation, NativeLifecycle::Active, None);
                false
            }
            HotkeyMonitorStatus::Failed(message) => {
                mgr.update_native_status(slot, generation, NativeLifecycle::Failed, Some(message));
                true
            }
            HotkeyMonitorStatus::Stopped => {
                mgr.update_native_status(slot, generation, NativeLifecycle::NotApplicable, None);
                false
            }
        }
    };

    if should_prompt {
        #[cfg(target_os = "macos")]
        minutes_core::hotkey_macos::open_input_monitoring_settings();
    }

    emit_status(app, slot);
}

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

    #[test]
    fn classify_caps_lock_as_native() {
        assert_eq!(classify_shortcut(57), ShortcutBackend::Native);
    }

    #[test]
    fn classify_fn_as_native() {
        assert_eq!(classify_shortcut(63), ShortcutBackend::Native);
    }

    #[test]
    fn classify_regular_key_as_standard() {
        assert_eq!(classify_shortcut(0), ShortcutBackend::Standard);
        assert_eq!(classify_shortcut(-1), ShortcutBackend::Standard);
        assert_eq!(classify_shortcut(49), ShortcutBackend::Standard); // Space
    }

    #[test]
    fn slot_roundtrip() {
        assert_eq!(
            ShortcutSlot::from_str("quick_thought").unwrap(),
            ShortcutSlot::QuickThought
        );
        assert_eq!(
            ShortcutSlot::from_str("dictation").unwrap(),
            ShortcutSlot::Dictation
        );
        assert!(ShortcutSlot::from_str("bogus").is_err());
    }

    #[test]
    fn state_machine_hold_to_talk() {
        let mut sm = ShortcutStateMachine::default();

        let action = sm.handle_press(false);
        assert!(matches!(action, StateMachineAction::StartHold));
        assert!(sm.key_down);
        assert!(matches!(sm.active_capture, Some(CaptureStyle::Hold)));

        // Release stops the hold
        sm.key_down_started_at = Some(Instant::now() - Duration::from_millis(300));
        let action = sm.handle_release(true);
        assert!(matches!(action, StateMachineAction::Stop { .. }));
    }

    #[test]
    fn state_machine_tap_to_lock() {
        let mut sm = ShortcutStateMachine::default();

        let action = sm.handle_press(false);
        assert!(matches!(action, StateMachineAction::StartHold));

        // Quick release (before threshold) with no active session
        sm.key_down_started_at = Some(Instant::now()); // ensure it's "just now"
        let action = sm.handle_release(false);
        assert!(matches!(action, StateMachineAction::None));
        assert!(matches!(sm.active_capture, Some(CaptureStyle::Locked)));
    }

    #[test]
    fn state_machine_discriminates_tap_under_threshold_from_hold() {
        let mut sm = ShortcutStateMachine::default();
        let action = sm.handle_press(false);
        assert!(matches!(action, StateMachineAction::StartHold));
        sm.key_down_started_at = Some(Instant::now() - Duration::from_millis(299));

        let action = sm.handle_release(false);

        assert!(matches!(action, StateMachineAction::None));
        assert!(matches!(sm.active_capture, Some(CaptureStyle::Locked)));
    }

    #[test]
    fn state_machine_release_after_threshold_stops_hold() {
        let mut sm = ShortcutStateMachine::default();
        let action = sm.handle_press(false);
        assert!(matches!(action, StateMachineAction::StartHold));
        sm.key_down_started_at = Some(Instant::now() - Duration::from_millis(301));

        let action = sm.handle_release(false);

        assert!(matches!(action, StateMachineAction::Stop { .. }));
    }

    #[test]
    fn state_machine_tap_to_stop_locked() {
        let mut sm = ShortcutStateMachine::default();

        // First tap starts capture immediately, then latches it.
        sm.handle_press(false);
        sm.key_down_started_at = Some(Instant::now());
        let action = sm.handle_release(false);
        assert!(matches!(action, StateMachineAction::None));
        assert!(matches!(sm.active_capture, Some(CaptureStyle::Locked)));

        // Session is now active. Second tap to stop.
        sm.handle_press(true);
        sm.key_down_started_at = Some(Instant::now());
        let action = sm.handle_release(true); // session active
        assert!(matches!(
            action,
            StateMachineAction::Stop { discard: false }
        ));
    }

    #[test]
    fn state_machine_ignores_key_repeat() {
        let mut sm = ShortcutStateMachine::default();
        sm.handle_press(false);
        assert!(sm.key_down);

        // Second press while key is down = ignored
        let action = sm.handle_press(false);
        assert!(matches!(action, StateMachineAction::None));
    }

    #[test]
    fn persisted_shortcut_disabled_slot_reports_custom_not_default() {
        // A disabled slot with a custom persisted chord must report the custom
        // chord, not the compile-time default. This is the core round-trip fix:
        // build_status' inactive branch reads these values.
        let mut config = minutes_core::Config::default();

        // Quick Thought: custom chord, disabled.
        config.global_hotkey.shortcut_enabled = false;
        config.global_hotkey.shortcut = "CmdOrCtrl+Shift+J".into();
        let (shortcut, keycode, backend) =
            persisted_shortcut_for_slot(&config, ShortcutSlot::QuickThought);
        assert_eq!(shortcut, "CmdOrCtrl+Shift+J");
        assert_ne!(
            shortcut,
            default_shortcut_for_slot(ShortcutSlot::QuickThought)
        );
        assert_eq!(keycode, -1);
        assert_eq!(backend, "standard");

        // Dictation, standard chord variant: custom chord, disabled.
        config.dictation.hotkey_enabled = false;
        config.dictation.shortcut_enabled = false;
        config.dictation.shortcut = "CmdOrCtrl+Shift+P".into();
        let (shortcut, keycode, backend) =
            persisted_shortcut_for_slot(&config, ShortcutSlot::Dictation);
        assert_eq!(shortcut, "CmdOrCtrl+Shift+P");
        assert_ne!(shortcut, default_shortcut_for_slot(ShortcutSlot::Dictation));
        assert_eq!(keycode, -1);
        assert_eq!(backend, "standard");
    }

    #[test]
    fn persisted_shortcut_dictation_native_keycode_preserved() {
        // Dictation's native keycode special-casing (CapsLock=57, fn=63) must be
        // preserved when hotkey_enabled is set, even though the slot is inactive.
        let mut config = minutes_core::Config::default();

        config.dictation.hotkey_enabled = true;
        config.dictation.hotkey_keycode = 57; // Caps Lock
        let (shortcut, keycode, backend) =
            persisted_shortcut_for_slot(&config, ShortcutSlot::Dictation);
        assert_eq!(shortcut, "CapsLock");
        assert_eq!(keycode, 57);
        assert_eq!(backend, "native");

        config.dictation.hotkey_keycode = 63; // fn
        let (shortcut, keycode, backend) =
            persisted_shortcut_for_slot(&config, ShortcutSlot::Dictation);
        assert_eq!(shortcut, "fn");
        assert_eq!(keycode, 63);
        assert_eq!(backend, "native");
    }

    #[test]
    fn persisted_shortcut_falls_back_to_default_when_config_empty() {
        // An empty config field falls back to the compile-time default so we
        // never report an empty chord.
        let mut config = minutes_core::Config::default();
        config.global_hotkey.shortcut = String::new();
        let (shortcut, _, _) = persisted_shortcut_for_slot(&config, ShortcutSlot::QuickThought);
        assert_eq!(
            shortcut,
            default_shortcut_for_slot(ShortcutSlot::QuickThought)
        );

        config.dictation.hotkey_enabled = false;
        config.dictation.shortcut = String::new();
        let (shortcut, _, _) = persisted_shortcut_for_slot(&config, ShortcutSlot::Dictation);
        assert_eq!(shortcut, default_shortcut_for_slot(ShortcutSlot::Dictation));
    }
}
