//! Release-tail timer (SDD-096).
//!
//! When the PTT key is released, audio transmission continues for a
//! configurable tail duration (0–500 ms, default 200 ms) to avoid
//! abrupt cutoff. Cancellation is performed by aborting the pending
//! [`JoinHandle`]; the [`tokio::sync::watch`] channel enables cooperative
//! early exit so the spawned task can skip writing to the gate when
//! cancelled. Drives [`AudioTransmitGate`] directly.

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;

use tokio::sync::watch;
use tokio::task::JoinHandle;

use crate::ptt::AudioTransmitGate;
use crate::transmit_selector::TransmitModeSelector;

/// Maximum configurable release-tail, in milliseconds.
pub const MAX_TAIL_MS: u32 = 500;

/// Default release-tail (SDD-096).
pub const DEFAULT_TAIL_MS: u32 = 200;

/// Coalesces a PTT key release into a deferred gate update.
///
/// Threading model (SDD-096):
///
/// * `tail_ms` is an [`AtomicU32`] so config changes are visible
///   immediately to any in-flight release task.
/// * The timer holds a [`watch::Sender<bool>`] for cooperative early
///   exit alongside the [`JoinHandle`]; the handle is replaced on
///   each `key_up` in a cheap [`std::sync::RwLock`] on PTT edge
///   transitions only — never on the audio frame hot path.
/// * Cancellation is performed by aborting the pending
///   [`JoinHandle`] in `cancel_pending()`. The watch channel allows
///   the spawned task to cooperatively exit without writing to the
///   gate when cancelled: the task races `cancel_rx.changed()`
///   against the tail sleep; if the sender is replaced or dropped,
///   the task returns early without applying the gate.
/// * `unsafe` is never used.
pub struct ReleaseTailTimer {
    /// Protected by RwLock; only the PTT edge-transition methods
    /// (`key_down`, `key_up`, `arm`) acquire it.
    selector: Arc<TransmitModeSelector>,
    /// Watch sender for cooperative early exit within the in-flight
    /// task. The task subscribes via [`watch::Sender::subscribe`] and
    /// races `cancel_rx.changed()` against the tail sleep.
    cancel_tx: std::sync::RwLock<watch::Sender<bool>>,
    /// The in-flight release task, replaced on every `key_up`.
    pending_handle: std::sync::RwLock<Option<JoinHandle<()>>>,
    tail_ms: AtomicU32,
}

impl ReleaseTailTimer {
    /// Construct a release-tail timer bound to a transmit-mode
    /// selector and an initial tail value.
    pub fn new(selector: Arc<TransmitModeSelector>, tail_ms: u32) -> Self {
        Self {
            selector,
            cancel_tx: std::sync::RwLock::new(watch::channel(false).0),
            pending_handle: std::sync::RwLock::new(None),
            tail_ms: AtomicU32::new(tail_ms.min(MAX_TAIL_MS)),
        }
    }

    /// Arm (or re-arm) the timer with a fresh `gate` and `tail_ms`
    /// clamped to `0..=MAX_TAIL_MS` (SDD-096).
    pub fn arm(&self, gate: AudioTransmitGate, tail_ms: u32) {
        self.selector.replace_gate(gate);
        self.tail_ms
            .store(tail_ms.min(MAX_TAIL_MS), Ordering::Relaxed);
    }

    /// Update the configured tail, clamped to `0..=MAX_TAIL_MS`.
    pub fn set_tail_ms(&self, ms: u32) {
        self.tail_ms.store(ms.min(MAX_TAIL_MS), Ordering::Relaxed);
    }

    /// Current configured tail (post-clamp).
    pub fn tail_ms(&self) -> u32 {
        self.tail_ms.load(Ordering::Relaxed)
    }

    /// Notify the timer that the PTT key went down. Cancels any
    /// pending release and immediately sets `transmit_active = true`.
    pub fn key_down(&self) {
        self.cancel_pending();
        self.selector.set_ptt_held(true);
    }

    /// Notify the timer that the PTT key went up. Spawns a task
    /// that waits for the tail to elapse (or cancellation) and then
    /// clears the gate. A subsequent [`Self::key_down`] within the
    /// window cancels this task.
    pub fn key_up(&self) {
        let selector = self.selector.clone();
        let mut cancel_rx = {
            let tx = self.cancel_tx.read().expect("cancel_tx lock poisoned");
            tx.subscribe()
        };
        let tail_ms = self.tail_ms();

        let new_handle = tokio::spawn(async move {
            if tail_ms > 0 {
                match tokio::time::timeout(
                    Duration::from_millis(tail_ms as u64),
                    cancel_rx.changed(),
                )
                .await
                {
                    // Timeout elapsed → timer was NOT cancelled → apply gate.
                    Err(_) => {}
                    // Channel closed (sender dropped) → timer was cancelled → skip gate.
                    Ok(Err(_)) => return,
                    // Received a value → treat as cancellation.
                    Ok(Ok(())) => return,
                }
            }
            selector.set_ptt_held(false);
        });

        if let Ok(mut g) = self.pending_handle.write() {
            if let Some(prev) = g.replace(new_handle) {
                prev.abort();
            }
        }
    }

    /// Cancel any pending release task and leave the gate at whatever
    /// value it currently holds.
    pub fn cancel(&self) {
        self.cancel_pending();
    }

    /// Cancel any pending release and force `ptt_held = false`.
    pub fn force_release(&self) {
        self.cancel_pending();
        self.selector.set_ptt_held(false);
    }

    fn cancel_pending(&self) {
        if let Ok(mut g) = self.pending_handle.write() {
            if let Some(h) = g.take() {
                h.abort();
            }
        }
    }
}

impl Drop for ReleaseTailTimer {
    fn drop(&mut self) {
        self.cancel_pending();
    }
}

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

    fn setup(tail_ms: u32) -> (AudioTransmitGate, ReleaseTailTimer) {
        let gate = AudioTransmitGate::new(false);
        let selector = Arc::new(TransmitModeSelector::new(gate.clone()));
        selector.set_mode(TransmitMode::Ptt);
        selector.set_in_channel(true);
        let timer = ReleaseTailTimer::new(selector, tail_ms);
        (gate, timer)
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn release_clears_after_tail() {
        let (gate, timer) = setup(80);
        timer.key_down();
        assert!(gate.load());
        timer.key_up();
        // Still transmitting during the tail.
        tokio::time::sleep(Duration::from_millis(20)).await;
        assert!(gate.load(), "should still be true during tail");
        // After the tail elapses, gate clears.
        tokio::time::sleep(Duration::from_millis(120)).await;
        assert!(!gate.load(), "gate should clear after tail");
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn redown_within_tail_cancels_release() {
        let (gate, timer) = setup(200);
        timer.key_down();
        timer.key_up();
        tokio::time::sleep(Duration::from_millis(20)).await;
        timer.key_down();
        // Wait past the original tail; gate must still be true.
        tokio::time::sleep(Duration::from_millis(250)).await;
        assert!(
            gate.load(),
            "subsequent key_down should cancel pending release"
        );
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn zero_tail_clears_immediately() {
        let (gate, timer) = setup(0);
        timer.key_down();
        assert!(gate.load());
        timer.key_up();
        tokio::time::sleep(Duration::from_millis(30)).await;
        assert!(!gate.load());
    }

    #[test]
    fn set_tail_ms_clamps_to_max() {
        let (_gate, timer) = setup(100);
        timer.set_tail_ms(99999);
        assert_eq!(timer.tail_ms(), MAX_TAIL_MS);
        timer.set_tail_ms(0);
        assert_eq!(timer.tail_ms(), 0);
        timer.set_tail_ms(MAX_TAIL_MS);
        assert_eq!(timer.tail_ms(), MAX_TAIL_MS);
    }
}
