//! Desktop Push-to-Talk capability model (SRS-195 / SRS-196 / SAD-071 /
//! SDD-081 / SDD-082).
//!
//! This module defines the typed `PttCapabilityLevel` enum, the
//! lightweight `PttBackendDescriptor` value the audio engine
//! publishes to upstream consumers, the `AudioTransmitGate` object
//! that owns the authoritative `transmit_active` flag (SAD-075 /
//! SDD-089), and the `MissedKeyUpWatchdog` task (SAD-079 / SDD-092
//! / DEC-028).
//!
//! Concrete platform backends (`WindowsRawInputBackend`,
//! `MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) live in the
//! sibling `backends` module; this file holds the cross-platform
//! pieces.

use core::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

use tokio::sync::watch;
use tracing::{info, warn};

/// Detected runtime capability of the active desktop PTT backend.
///
/// The reported value shall match runtime behaviour — a backend
/// that *could* deliver `L2GlobalHoldToTalk` but lacks the
/// user-granted permission or the required compositor support
/// reports `L0Focused` (SRS-196 / SysRS-298).
///
/// `L4DeviceAware` is reserved per the gen2 v0.9.3 baseline
/// (SDD-082) and is **not** produced by any MVP implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PttCapabilityLevel {
    /// Focused PTT. Press / release works only while the
    /// application window has input focus. Mandatory baseline on
    /// every desktop platform per SysRS-296.
    L0Focused,
    /// Global shortcut activation. The OS recognises a global
    /// accelerator and notifies the application, but hold-to-talk
    /// semantics may be approximated rather than guaranteed.
    L1GlobalShortcut,
    /// Global hold-to-talk. Press and release events are delivered
    /// while the application is not focused.
    L2GlobalHoldToTalk,
    /// Global hold-to-talk plus mouse side buttons (typically
    /// Mouse4 / Mouse5, sometimes labelled "back" / "forward").
    L3GlobalWithMouseButtons,
    /// Device-aware PTT. Reserved; no MVP implementation produces
    /// this value (SDD-082).
    L4DeviceAware,
}

impl PttCapabilityLevel {
    /// Short identifier used by the diagnostics sanitizer and by
    /// the release verification record. Stable across releases —
    /// release notes and platform-test traces compare against these
    /// strings.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::L0Focused => "L0Focused",
            Self::L1GlobalShortcut => "L1GlobalShortcut",
            Self::L2GlobalHoldToTalk => "L2GlobalHoldToTalk",
            Self::L3GlobalWithMouseButtons => "L3GlobalWithMouseButtons",
            Self::L4DeviceAware => "L4DeviceAware",
        }
    }

    /// True when the level represents a global (non-focused)
    /// behaviour. UI consumers use this to gate the "global PTT
    /// available" affordance.
    pub fn is_global(self) -> bool {
        matches!(
            self,
            Self::L1GlobalShortcut
                | Self::L2GlobalHoldToTalk
                | Self::L3GlobalWithMouseButtons
                | Self::L4DeviceAware
        )
    }
}

impl fmt::Display for PttCapabilityLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Diagnostics-safe descriptor of the active PTT backend. Carries
/// only the fields the privacy policy permits in the user-initiated
/// diagnostic export (SRS-202 / DEC-027): the detected capability
/// level, a fixed `backend_id` string per implementation, and an
/// optional bound-input class (`"keyboard"`, `"mouse-side-button"`,
/// …). The raw key code, scan code, virtual-key value, or keysym
/// of any user binding is **never** part of this structure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PttBackendDescriptor {
    /// Detected runtime capability.
    pub level: PttCapabilityLevel,
    /// Stable identifier per implementation. Examples:
    /// `"focused"`, `"raw-input"`, `"low-level-hook"`,
    /// `"event-tap"`, `"gnome-wayland-portal"`.
    pub backend_id: &'static str,
    /// Coarse description of the bound input. `None` when no
    /// binding is active. The value is a stable category string,
    /// never a key code.
    pub bound_input_class: Option<&'static str>,
}

impl PttBackendDescriptor {
    /// The universal Focused-PTT fallback (SDD-087). Every desktop
    /// platform reports this value until a platform-specific
    /// global backend lands in a follow-up code milestone.
    pub const fn focused() -> Self {
        Self {
            level: PttCapabilityLevel::L0Focused,
            backend_id: "focused",
            bound_input_class: Some("keyboard"),
        }
    }
}

impl Default for PttBackendDescriptor {
    fn default() -> Self {
        Self::focused()
    }
}

/// Owns the authoritative `transmit_active` AtomicBool plus a
/// `tokio::sync::watch` channel so subscribers (notably the
/// missed-key-up watchdog) can observe transitions without
/// polling. Per SAD-075 / SDD-089 this is the **only** mutator
/// path on `transmit_active`; the encoder feed and the platform
/// PTT backends call `set` and `load` exclusively.
///
/// Cheap to clone — internally an `Arc` over the atomic and the
/// watch sender.
#[derive(Clone)]
pub struct AudioTransmitGate {
    inner: Arc<TransmitGateInner>,
}

struct TransmitGateInner {
    flag: Arc<AtomicBool>,
    tx: watch::Sender<bool>,
}

impl AudioTransmitGate {
    /// Construct a new gate initialised to `initial`.
    pub fn new(initial: bool) -> Self {
        let (tx, _rx) = watch::channel(initial);
        Self {
            inner: Arc::new(TransmitGateInner {
                flag: Arc::new(AtomicBool::new(initial)),
                tx,
            }),
        }
    }

    /// Set the transmit flag. When the value changes, every
    /// `watch::Receiver` returned by [`Self::subscribe`] observes
    /// the new value.
    pub fn set(&self, active: bool) {
        // We always store on the atomic (it's the hot read path
        // for the encoder feed) and we always send on the watch
        // channel — the watch implementation deduplicates so a
        // repeat `set(true)` does not wake subscribers.
        self.inner.flag.store(active, Ordering::Relaxed);
        let _ = self.inner.tx.send(active);
    }

    /// Read the current value. Cheap; no locking.
    pub fn load(&self) -> bool {
        self.inner.flag.load(Ordering::Relaxed)
    }

    /// Subscribe to value transitions. The returned receiver
    /// observes every distinct transition (the watch channel
    /// deduplicates same-value sends).
    pub fn subscribe(&self) -> watch::Receiver<bool> {
        self.inner.tx.subscribe()
    }

    /// Live shared handle to the underlying `AtomicBool` for hot
    /// paths that need to consult the flag once per outbound audio
    /// frame without going through the gate wrappers. Callers must
    /// treat the returned Arc as read-only; the gate's `set`
    /// remains the sole mutator (SAD-075).
    pub fn flag_arc(&self) -> Arc<AtomicBool> {
        self.inner.flag.clone()
    }
}

/// Background task that clears `transmit_active` after a configured
/// timeout (SAD-079 / SDD-092 / DEC-028).
///
/// Subscribes to the gate's watch channel, notes the timestamp of
/// each `false -> true` transition, clears the timestamp on each
/// `true -> false` transition. If the `true` lifetime exceeds the
/// configured ceiling the task self-clears the gate to false and
/// emits a sanitised diagnostic record (no key data — DEC-027).
pub struct MissedKeyUpWatchdog {
    handle: Option<tokio::task::JoinHandle<()>>,
}

impl MissedKeyUpWatchdog {
    /// Default timeout per DEC-028 — 30 seconds.
    pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);

    /// Spawn the watchdog. The returned handle aborts the task on
    /// Drop. The watchdog needs a tokio runtime context; the audio
    /// engine spawns it inside `start_audio` where the bridge's
    /// runtime is available.
    pub fn spawn(gate: AudioTransmitGate, timeout: Duration) -> Self {
        let handle = tokio::spawn(async move {
            run_watchdog(gate, timeout).await;
        });
        Self {
            handle: Some(handle),
        }
    }
}

impl Drop for MissedKeyUpWatchdog {
    fn drop(&mut self) {
        if let Some(h) = self.handle.take() {
            h.abort();
        }
    }
}

async fn run_watchdog(gate: AudioTransmitGate, timeout: Duration) {
    let mut rx = gate.subscribe();
    // Track whether we are currently in a `transmit_active = true`
    // window. We don't need the actual timestamp; we just need a
    // bounded wait that wakes on either the gate going false or
    // the timeout elapsing.
    info!(
        target: "chanora_audio",
        timeout_secs = timeout.as_secs(),
        "missed-key-up watchdog spawned"
    );
    loop {
        // Wait for a transition.
        if rx.changed().await.is_err() {
            // Gate dropped; exit.
            return;
        }
        let is_active = *rx.borrow_and_update();
        if !is_active {
            // Either the user released, or someone else cleared
            // the gate. Either way the watchdog has nothing to
            // do until the next press.
            continue;
        }
        // The gate just went true. Wait for either a release
        // transition or for the timeout to elapse.
        let release_or_timeout = tokio::select! {
            r = rx.changed() => r.map(|_| *rx.borrow_and_update()),
            _ = tokio::time::sleep(timeout) => {
                // Timeout — self-clear and emit the privacy-safe
                // diagnostic record. The field set here is
                // deliberately limited to the values the gen2
                // sanitizer permits (DEC-027): the active
                // capability descriptor is owned by the controller
                // and is not in scope here, so we emit only the
                // watchdog-relevant fact.
                warn!(
                    target: "chanora_audio",
                    timeout_secs = timeout.as_secs(),
                    "missed-key-up watchdog fired; clearing transmit_active"
                );
                gate.set(false);
                continue;
            }
        };
        match release_or_timeout {
            Ok(false) => {
                // Normal release; loop and wait for the next press.
            }
            Ok(true) => {
                // Edge case: same-value double-set. Watch
                // deduplicates so this should not actually fire,
                // but we keep waiting on the timeout for the
                // next iteration.
            }
            Err(_) => {
                // Channel closed; exit.
                return;
            }
        }
    }
}

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

    #[test]
    fn level_as_str_is_stable() {
        assert_eq!(PttCapabilityLevel::L0Focused.as_str(), "L0Focused");
        assert_eq!(
            PttCapabilityLevel::L2GlobalHoldToTalk.as_str(),
            "L2GlobalHoldToTalk"
        );
        assert_eq!(
            PttCapabilityLevel::L3GlobalWithMouseButtons.as_str(),
            "L3GlobalWithMouseButtons"
        );
    }

    #[test]
    fn level_is_global_classification() {
        assert!(!PttCapabilityLevel::L0Focused.is_global());
        assert!(PttCapabilityLevel::L1GlobalShortcut.is_global());
        assert!(PttCapabilityLevel::L2GlobalHoldToTalk.is_global());
        assert!(PttCapabilityLevel::L3GlobalWithMouseButtons.is_global());
        assert!(PttCapabilityLevel::L4DeviceAware.is_global());
    }

    #[test]
    fn focused_descriptor_carries_only_safe_fields() {
        let d = PttBackendDescriptor::focused();
        assert_eq!(d.level, PttCapabilityLevel::L0Focused);
        assert_eq!(d.backend_id, "focused");
        assert_eq!(d.bound_input_class, Some("keyboard"));
        // Compile-time check: the struct shape itself excludes
        // anything that could carry a key code (DEC-027).
        let _: &str = d.backend_id;
        let _: Option<&str> = d.bound_input_class;
    }

    #[test]
    fn gate_set_and_load_roundtrip() {
        let g = AudioTransmitGate::new(false);
        assert!(!g.load());
        g.set(true);
        assert!(g.load());
        g.set(false);
        assert!(!g.load());
    }

    #[tokio::test(flavor = "current_thread", start_paused = true)]
    async fn gate_subscribe_observes_transitions() {
        let g = AudioTransmitGate::new(false);
        let mut rx = g.subscribe();
        g.set(true);
        rx.changed().await.unwrap();
        assert!(*rx.borrow_and_update());
        g.set(false);
        rx.changed().await.unwrap();
        assert!(!*rx.borrow_and_update());
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn watchdog_clears_transmit_after_timeout() {
        let g = AudioTransmitGate::new(false);
        let _wd = MissedKeyUpWatchdog::spawn(g.clone(), Duration::from_millis(80));
        // Give the watchdog a tick to subscribe before we press.
        tokio::time::sleep(Duration::from_millis(20)).await;
        g.set(true);
        // Wait past the timeout. The watchdog runs on a separate
        // worker so real-time elapses in parallel.
        tokio::time::sleep(Duration::from_millis(200)).await;
        assert!(!g.load(), "watchdog should have cleared transmit_active");
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn watchdog_does_not_clear_on_normal_release() {
        let g = AudioTransmitGate::new(false);
        let _wd = MissedKeyUpWatchdog::spawn(g.clone(), Duration::from_millis(80));
        tokio::time::sleep(Duration::from_millis(20)).await;
        g.set(true);
        tokio::time::sleep(Duration::from_millis(30)).await;
        g.set(false);
        // Wait past what would have been the timeout.
        tokio::time::sleep(Duration::from_millis(200)).await;
        assert!(!g.load());
        // The gate should still be `false` and the next press
        // should re-arm the watchdog cleanly.
        g.set(true);
        tokio::time::sleep(Duration::from_millis(200)).await;
        assert!(!g.load(), "watchdog should fire on the second press as well");
    }
}
