//! Cross-platform Push-to-Talk controller (SDD-088).
//!
//! The controller is the orchestration seam between the platform
//! input backend (selected by `chanora_audio::ptt_backends::select`)
//! and the audio engine's `AudioTransmitGate`. It owns the active
//! `Box<dyn DesktopPttBackend>`, a clone of the gate, the current
//! `PttBinding`, and the `watch::Sender<PttCapabilityLevel>` the
//! bridge subscribes to for the UI capability badge (SDD-091).
//!
//! Source: SAD-071 (cross-platform PTT plumbing), SAD-076 (binding
//! lifecycle), SDD-088.
//!
//! The previous implementation kept the backend slot inside
//! `AudioEngine` and split the binding / capability publication
//! between the engine and `ChanoraSession`. SDD-088 names a
//! dedicated `PttController` software unit; this module is that
//! unit. The audio engine retains ownership only of the cpal
//! streams and the missed-key-up watchdog (SDD-092).

use std::sync::Arc;

use chanora_audio::ptt_backends::{
    select as select_ptt_backend, DesktopPttBackend, PttBackendError, PttBinding,
};
use chanora_audio::{AudioTransmitGate, PttBackendDescriptor, PttCapabilityLevel};
use tokio::sync::{watch, Mutex};
use tracing::{info, warn};

/// Errors raised by [`PttController`]. Thin wrapper over
/// [`PttBackendError`] plus a "not armed" variant for callers that
/// touch the controller after `stop`.
#[derive(Debug, thiserror::Error)]
pub enum PttControllerError {
    /// The wrapped platform backend reported an error.
    #[error("ptt backend: {0}")]
    Backend(#[from] PttBackendError),
    /// `set_binding` or `descriptor` was called after `stop`.
    #[error("ptt controller not armed")]
    NotArmed,
}

/// Cross-platform PTT controller (SDD-088).
///
/// Constructed by `ChanoraSession::start_audio` after the audio
/// engine is up. Owns:
///
/// * `Box<dyn DesktopPttBackend>` — the selected platform backend.
///   The factory in [`chanora_audio::ptt_backends::select`] never
///   fails; on any platform that lacks a global-capture path the
///   universal `FocusedPttBackend` is returned, so this slot is
///   always populated between `new()` and `stop()`.
/// * `AudioTransmitGate` — a clone of the engine's gate, handed to
///   the backend on arm and held here so re-arms after `rebind` do
///   not need a reference back to the engine.
/// * `tokio::sync::Mutex<PttBinding>` — the active binding. The
///   binding capture dialog in Flutter calls into the bridge which
///   funnels here; the mutex guards the rebind path against
///   concurrent updates.
/// * `watch::Sender<PttCapabilityLevel>` — fans out the
///   live-resolved capability for SDD-091 (UI badge). The Linux
///   GNOME-Wayland portal backend resolves its capability
///   asynchronously after the user accepts the BindShortcuts
///   dialog; the controller's forwarder task observes the
///   backend's own descriptor-watch and republishes the
///   capability through this channel.
pub struct PttController {
    backend: Mutex<Option<Box<dyn DesktopPttBackend>>>,
    binding: Mutex<PttBinding>,
    gate: AudioTransmitGate,
    capability_tx: watch::Sender<PttCapabilityLevel>,
    descriptor_tx: watch::Sender<PttBackendDescriptor>,
    forwarder: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
}

impl PttController {
    /// Construct the controller and arm the selected platform
    /// backend. The factory in
    /// [`chanora_audio::ptt_backends::select`] never fails, but the
    /// arming `start` call may; on failure the controller still
    /// returns successfully with a Focused-fallback backend already
    /// armed (matching the previous engine behaviour) so the audio
    /// path is never blocked by a PTT-arm failure.
    pub fn new(gate: AudioTransmitGate) -> Arc<Self> {
        let mut backend = select_ptt_backend();
        let initial_binding = PttBinding::none();
        match backend.start(gate.clone(), initial_binding.clone()) {
            Ok(()) => {
                let d = backend.descriptor();
                info!(
                    target: "chanora_core",
                    capability_level = %d.level,
                    backend_id = d.backend_id,
                    bound_input_class = ?d.bound_input_class,
                    "ptt controller armed"
                );
            }
            Err(e) => {
                warn!(
                    target: "chanora_core",
                    error = %e,
                    "ptt backend start failed; controller continues with current backend instance"
                );
            }
        }
        let descriptor = backend.descriptor();
        let capability = descriptor.level;
        let (capability_tx, _capability_rx) = watch::channel(capability);
        let (descriptor_tx, _descriptor_rx) = watch::channel(descriptor.clone());

        // Subscribe to the backend's own descriptor-watch and
        // re-publish to both the descriptor sender (consumed by
        // `ChanoraSession` for `BridgeEvent::PttCapability`) and
        // the capability sender (SDD-088 public surface).
        let mut backend_rx = backend.descriptor_watch();
        let cap_tx_for_task = capability_tx.clone();
        let desc_tx_for_task = descriptor_tx.clone();
        let forwarder = tokio::spawn(async move {
            while backend_rx.changed().await.is_ok() {
                let d = backend_rx.borrow_and_update().clone();
                let _ = cap_tx_for_task.send(d.level);
                let _ = desc_tx_for_task.send(d);
            }
        });

        Arc::new(Self {
            backend: Mutex::new(Some(backend)),
            binding: Mutex::new(initial_binding),
            gate,
            capability_tx,
            descriptor_tx,
            forwarder: std::sync::Mutex::new(Some(forwarder)),
        })
    }

    /// Replace the active binding (SDD-088 public surface).
    ///
    /// Returns the freshly-published descriptor so callers can
    /// emit the corresponding `BridgeEvent::PttCapability` event.
    /// On success the new binding is recorded under the binding
    /// mutex and the capability watch is republished.
    pub async fn set_binding(
        &self,
        binding: PttBinding,
    ) -> Result<PttBackendDescriptor, PttControllerError> {
        let mut backend_guard = self.backend.lock().await;
        let backend = backend_guard
            .as_mut()
            .ok_or(PttControllerError::NotArmed)?;
        backend.rebind(binding.clone())?;
        let descriptor = backend.descriptor();
        // Record the new binding under its own mutex so the
        // SDD-088 surface (held binding) reflects the resolved
        // value. The backend mutex is held first to preserve the
        // documented lock order (backend before binding).
        {
            let mut binding_guard = self.binding.lock().await;
            *binding_guard = binding;
        }
        // Republish the capability so subscribers that did not
        // wire the backend's own descriptor-watch still observe
        // the transition.
        let _ = self.capability_tx.send(descriptor.level);
        let _ = self.descriptor_tx.send(descriptor.clone());
        Ok(descriptor)
    }

    /// Current resolved capability (SDD-088 public surface).
    pub fn current_capability(&self) -> PttCapabilityLevel {
        *self.capability_tx.borrow()
    }

    /// Subscribe to capability transitions (SDD-088 public surface).
    /// Used by the bridge to drive the Flutter `PttCapabilityBadge`
    /// stream (SDD-091).
    pub fn subscribe_capability(&self) -> watch::Receiver<PttCapabilityLevel> {
        self.capability_tx.subscribe()
    }

    /// Privacy-safe descriptor of the active backend. Returns the
    /// universal Focused fallback when the controller has been
    /// stopped (the slot is empty between `stop()` and Drop).
    pub async fn descriptor(&self) -> PttBackendDescriptor {
        let guard = self.backend.lock().await;
        match guard.as_ref() {
            Some(b) => b.descriptor(),
            None => PttBackendDescriptor::focused(),
        }
    }

    /// Subscribe to descriptor transitions. Backends that resolve
    /// asynchronously (Linux portal) republish through here.
    pub fn descriptor_watch(&self) -> watch::Receiver<PttBackendDescriptor> {
        self.descriptor_tx.subscribe()
    }

    /// Reference to the underlying gate. Diagnostic / test use only.
    pub fn gate(&self) -> &AudioTransmitGate {
        &self.gate
    }

    /// Release OS-level resources held by the platform backend
    /// (Raw Input message loop, Event Tap run loop, portal D-Bus
    /// session, …). Idempotent.
    pub async fn stop(&self) {
        if let Some(h) = self.forwarder.lock().ok().and_then(|mut g| g.take()) {
            h.abort();
        }
        let mut guard = self.backend.lock().await;
        if let Some(mut b) = guard.take() {
            b.stop();
        }
    }
}

impl Drop for PttController {
    fn drop(&mut self) {
        if let Some(h) = self.forwarder.get_mut().ok().and_then(|g| g.take()) {
            h.abort();
        }
        // The canonical shutdown path is `PttController::stop().await`
        // invoked by the session before drop. We cannot lock the
        // tokio Mutex synchronously here; if the controller is
        // dropped without an explicit stop, the backend's own
        // `Drop` runs through the `Box<dyn DesktopPttBackend>`
        // and is responsible for releasing OS resources.
    }
}

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

    #[tokio::test]
    async fn controller_arms_and_reports_capability() {
        // The focused fallback is always selectable; the factory
        // never fails on a host without any global capture path.
        let gate = AudioTransmitGate::new(false);
        let controller = PttController::new(gate);
        // Every backend returns a non-null backend_id and a
        // capability level that round-trips through the watch.
        let desc = controller.descriptor().await;
        assert!(!desc.backend_id.is_empty());
        let level = controller.current_capability();
        // The descriptor's level must match the watch.
        assert_eq!(level, desc.level);
        // Subscribing must yield the same value without blocking.
        let mut rx = controller.subscribe_capability();
        assert_eq!(*rx.borrow_and_update(), level);
    }

    #[tokio::test]
    async fn set_binding_updates_descriptor_watch() {
        let gate = AudioTransmitGate::new(false);
        let controller = PttController::new(gate);
        let mut desc_rx = controller.descriptor_watch();
        // Drain the initial value.
        let _ = desc_rx.borrow_and_update();
        // The focused fallback accepts every binding (it's just a
        // diagnostic carrier; the actual press detection happens
        // inside the Flutter Listener path).
        let binding = PttBinding {
            input_class: chanora_audio::PttInputClass::Keyboard,
            platform_key: "space".to_string(),
        };
        let new_desc = controller.set_binding(binding.clone()).await.unwrap();
        // The descriptor watch must have received the post-rebind
        // value. We don't assert a specific level here because the
        // focused backend's level does not change on rebind, but
        // the descriptor send is observable.
        assert_eq!(new_desc.backend_id, controller.descriptor().await.backend_id);
    }
}
