//! Platform-specific desktop Push-to-Talk backends (SDD-081 trait,
//! SDD-083..087 implementations).
//!
//! The cross-platform trait surface lives in this module's root;
//! per-OS implementations live in the platform sub-modules and are
//! conditionally compiled. `select()` is the runtime factory the
//! audio engine calls during `start_audio`.
//!
//! Every backend has the same shape:
//!   * `start(gate, binding)` arms the backend.
//!   * `stop()` releases OS-level resources.
//!   * `rebind(binding)` updates the active binding without
//!     restarting the backend (used by the UI binding-capture
//!     flow).
//!   * `descriptor()` returns the privacy-safe descriptor for
//!     diagnostics + the UI capability badge.
//!
//! Backends never log raw key codes or scan codes — only the
//! stable `bound_input_class` string ("keyboard",
//! "mouse-side-button") plus the backend identifier.

use core::fmt;

use crate::ptt::{AudioTransmitGate, PttBackendDescriptor};

mod focused;

#[cfg(target_os = "windows")]
mod windows;

#[cfg(target_os = "macos")]
mod macos;

#[cfg(target_os = "linux")]
mod linux;

pub use focused::FocusedPttBackend;

/// User-bound PTT input. The struct deliberately carries only
/// privacy-safe coarse-grained values so the diagnostics rule
/// (DEC-027) holds at the type level. The OS-side key identity
/// stays inside the platform backend implementation and is never
/// exposed across this surface.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PttBinding {
    /// Coarse-grained class. Stable values: `"keyboard"`,
    /// `"mouse-side-button"`. Future levels (DEC-026 mouse buttons
    /// on Linux portal) may add `"mouse-other"`.
    pub input_class: PttInputClass,
    /// Opaque platform-defined key value. The value is a string so
    /// diverse platform representations (Windows scan code, macOS
    /// key code, Linux portal trigger description) all fit. The
    /// diagnostics sanitizer's banned-field rule (SAD-077 /
    /// SDD-090) prevents this field from being logged because it
    /// never appears in a tracing record — the audio + bridge
    /// layers consult only `input_class` and the backend `descriptor()`.
    pub platform_key: String,
}

impl PttBinding {
    /// A "no binding" sentinel. Backends never produce a
    /// transmit-active event from this value.
    pub fn none() -> Self {
        Self {
            input_class: PttInputClass::None,
            platform_key: String::new(),
        }
    }

    /// Public coarse string used by the diagnostic export and the
    /// UI badge.
    pub fn class_str(&self) -> &'static str {
        self.input_class.as_str()
    }
}

/// Coarse input class. Stable strings shared by the diagnostics
/// export, the UI capability badge, and the release verification
/// record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PttInputClass {
    /// No binding is active.
    None,
    /// A keyboard key.
    Keyboard,
    /// A mouse side button (Mouse4 / Mouse5).
    MouseSideButton,
}

impl PttInputClass {
    /// Stable identifier for diagnostics. The value is never the
    /// raw key code or scan code — see DEC-027.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::None => "",
            Self::Keyboard => "keyboard",
            Self::MouseSideButton => "mouse-side-button",
        }
    }
}

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

/// Errors raised by a desktop PTT backend.
#[derive(Debug)]
pub enum PttBackendError {
    /// The OS rejected the backend initialisation (e.g. Raw Input
    /// registration failed, event tap creation failed).
    Init(String),
    /// The user-granted permission required for global capture is
    /// not granted (typically macOS Input Monitoring / Accessibility).
    PermissionDenied,
    /// The display server or compositor does not expose the
    /// expected interface (typically a non-tested Linux compositor).
    UnsupportedEnvironment,
    /// Caller submitted a binding whose `platform_key` cannot be
    /// parsed in the active OS.
    InvalidBinding(String),
}

impl fmt::Display for PttBackendError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Init(s) => write!(f, "init failed: {s}"),
            Self::PermissionDenied => f.write_str("permission denied"),
            Self::UnsupportedEnvironment => f.write_str("unsupported environment"),
            Self::InvalidBinding(s) => write!(f, "invalid binding: {s}"),
        }
    }
}

impl std::error::Error for PttBackendError {}

/// Cross-platform desktop PTT backend (SDD-081).
///
/// All implementations call exactly the audio transmit gate's
/// `set(bool)` method to drive `transmit_active`; they never log
/// raw key data.
pub trait DesktopPttBackend: Send {
    /// Privacy-safe descriptor of this backend instance.
    fn descriptor(&self) -> PttBackendDescriptor;

    /// Subscribe to descriptor transitions. Implementations that
    /// cannot transition asynchronously (`FocusedPttBackend`,
    /// `WindowsRawInputBackend`, `WindowsHookBackend`,
    /// `MacOSEventTapBackend` today) return a receiver that
    /// observes only the initial value and never fires again. The
    /// Linux portal backend uses this channel to publish the
    /// post-`BindShortcuts` capability transition (Bound /
    /// Cancelled).
    fn descriptor_watch(&self) -> tokio::sync::watch::Receiver<PttBackendDescriptor> {
        let (_tx, rx) = tokio::sync::watch::channel(self.descriptor());
        // Hold `_tx` alive by leaking it — receivers detect the
        // sender drop via `changed()` returning `Err`, which the
        // core supervisor treats as "no further updates". By
        // leaking we instead keep the receiver "live" indefinitely
        // (no spurious closed-channel events), and the channel is
        // dropped together with the audio engine because the
        // backend Drop releases the box.
        std::mem::forget(_tx);
        rx
    }

    /// Arm the backend. After this call the backend listens for
    /// the bound input and toggles the gate accordingly.
    fn start(
        &mut self,
        gate: AudioTransmitGate,
        binding: PttBinding,
    ) -> Result<(), PttBackendError>;

    /// Release OS-level resources. Idempotent. The backend
    /// instance may be dropped immediately after.
    fn stop(&mut self);

    /// Replace the active binding without restarting the backend.
    /// May fail with `PttBackendError::InvalidBinding` if the new
    /// binding cannot be honoured.
    fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError>;
}

/// Runtime factory (SAD-071 / `platform_input::select`). Returns
/// the highest-capability backend the current OS, permission set,
/// and display server permit, falling back through the ladder
/// described in `desktop-ptt-architecture.md` to the universal
/// `FocusedPttBackend`.
///
/// The factory never fails — `FocusedPttBackend` is always
/// constructible.
pub fn select() -> Box<dyn DesktopPttBackend> {
    #[cfg(target_os = "windows")]
    {
        if let Some(b) = windows::try_select() {
            return b;
        }
    }
    #[cfg(target_os = "macos")]
    {
        if let Some(b) = macos::try_select() {
            return b;
        }
    }
    #[cfg(target_os = "linux")]
    {
        if let Some(b) = linux::try_select() {
            return b;
        }
    }
    Box::new(FocusedPttBackend::new())
}
