//! Voice-activity state machine for P1 transmit gating.
//!
//! Implements the full P1 VAD gate policy:
//!   * backend/model speech decisions — no custom probability thresholds.
//!   * `open_after_ms` — speech must be detected for this long before
//!     the gate opens (prevents false opens on transients). Default 40 ms.
//!   * `hangover_ms` — gate stays open for this long after speech drops
//!     out of the backend decision (prevents choppy transmit close). Default 500 ms.
//!   * `min_tx_ms` — minimum transmit duration after gate opens. Default 200 ms.
//!
//! Pre-roll (first-syllable preservation) is handled in the capture
//! pipeline, not here. The state machine only decides whether the gate
//! is open or closed.

/// Shared VAD timing constants and gate state machine.
///
/// Exposing these values here keeps the audio config and platform
/// capture paths aligned without repeating the same magic numbers in
/// multiple modules.
/// Default confirmation window before the gate opens, in milliseconds.
pub const VAD_OPEN_AFTER_MS: u32 = 40;
/// Default hangover duration in milliseconds.
pub const VAD_HANGOVER_MS: u32 = 500;
/// Default minimum transmit duration in milliseconds.
pub const VAD_MIN_TX_MS: u32 = 200;

/// Hangover/open-after/minimum-transmit state machine.
#[derive(Debug, Clone)]
pub struct VoiceActivityStateMachine {
    /// Frames of continuous speech required before gate opens.
    open_after_frames: u32,
    hangover_frames: u32,
    min_tx_frames: u32,
    active: bool,
    hangover_remaining: u32,
    min_tx_remaining: u32,
    /// Frames of continuous speech seen since last open attempt.
    open_confirm_frames: u32,
    /// Frames spent open without a strong speech score. This keeps
    /// stale or borderline VAD output from holding the mic open forever.
    weak_hold_frames: u32,
}

impl VoiceActivityStateMachine {
    /// Create a state machine. Frame duration is 10 ms.
    pub fn new(open_after_ms: u32, hangover_ms: u32, min_tx_ms: u32) -> Self {
        Self {
            open_after_frames: open_after_ms / 10,
            hangover_frames: hangover_ms / 10,
            min_tx_frames: min_tx_ms / 10,
            active: false,
            hangover_remaining: 0,
            min_tx_remaining: 0,
            open_confirm_frames: 0,
            weak_hold_frames: 0,
        }
    }

    /// Update timers without forcing a close. Used by
    /// live settings changes while audio is already running.
    pub fn configure(&mut self, open_after_ms: u32, hangover_ms: u32, min_tx_ms: u32) {
        self.open_after_frames = open_after_ms / 10;
        self.hangover_frames = hangover_ms / 10;
        self.min_tx_frames = min_tx_ms / 10;
        self.hangover_remaining = self.hangover_remaining.min(self.hangover_frames);
        self.min_tx_remaining = self.min_tx_remaining.min(self.min_tx_frames);
    }

    /// Advance by one 10 ms backend speech decision and return whether
    /// transmit should be open for VoiceActivity mode.
    pub fn update(&mut self, speech: bool) -> bool {
        if self.active {
            if self.min_tx_remaining > 0 {
                self.min_tx_remaining -= 1;
            }
            if speech {
                self.weak_hold_frames = 0;
            } else {
                self.weak_hold_frames = self.weak_hold_frames.saturating_add(1);
            }
            if self.weak_hold_frames >= self.weak_hold_limit_frames() && self.min_tx_remaining == 0
            {
                self.close();
                return false;
            }
            if speech {
                // Speech still present — reset hangover.
                self.hangover_remaining = self.hangover_frames;
            } else if self.hangover_remaining > 0 {
                self.hangover_remaining -= 1;
            } else if self.min_tx_remaining == 0 {
                // Hangover expired and min-tx elapsed — close gate.
                self.close();
            }
        } else {
            // Gate is closed. Accumulate confirmation frames.
            if speech {
                self.open_confirm_frames += 1;
                if self.open_confirm_frames >= self.open_after_frames.max(1) {
                    // Confirmed speech — open gate.
                    self.active = true;
                    self.hangover_remaining = self.hangover_frames;
                    self.min_tx_remaining = self.min_tx_frames;
                    self.open_confirm_frames = 0;
                    self.weak_hold_frames = 0;
                }
            } else {
                // Speech is no longer detected — reset confirmation.
                self.open_confirm_frames = 0;
            }
        }
        self.active
    }

    /// Current active state.
    pub fn active(&self) -> bool {
        self.active
    }

    fn weak_hold_limit_frames(&self) -> u32 {
        (self.hangover_frames + self.min_tx_frames + self.open_after_frames).clamp(30, 100)
    }

    fn close(&mut self) {
        self.active = false;
        self.hangover_remaining = 0;
        self.min_tx_remaining = 0;
        self.open_confirm_frames = 0;
        self.weak_hold_frames = 0;
    }

    /// Reset all state (call on session restart / voice_leave).
    pub fn reset(&mut self) {
        self.close();
    }
}

impl Default for VoiceActivityStateMachine {
    fn default() -> Self {
        Self::new(VAD_OPEN_AFTER_MS, VAD_HANGOVER_MS, VAD_MIN_TX_MS)
    }
}

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

    #[test]
    fn hangover_keeps_gate_open_after_close() {
        // open_after_ms=0 so gate opens immediately on first frame.
        let mut sm = VoiceActivityStateMachine::new(0, 30, 0);
        assert!(sm.update(true));
        assert!(sm.update(false));
        assert!(sm.update(false));
        assert!(sm.update(false));
        assert!(!sm.update(false));
    }

    #[test]
    fn open_after_requires_confirmation_frames() {
        // open_after_ms=20 → 2 frames required.
        let mut sm = VoiceActivityStateMachine::new(20, 0, 0);
        // First frame: not yet open.
        assert!(!sm.update(true));
        // Second frame: now open.
        assert!(sm.update(true));
    }

    #[test]
    fn open_after_resets_on_silence() {
        // open_after_ms=20 → 2 frames required.
        let mut sm = VoiceActivityStateMachine::new(20, 0, 0);
        assert!(!sm.update(true)); // 1 frame
        assert!(!sm.update(false)); // silence resets counter
        assert!(!sm.update(true)); // 1 frame again
        assert!(sm.update(true)); // 2nd frame → open
    }

    #[test]
    fn min_tx_keeps_gate_open_briefly() {
        // open_after_ms=0, hangover=0, min_tx=20ms (2 frames).
        // After opening: min_tx_remaining decrements each frame.
        // Gate closes on the frame where it reaches 0.
        let mut sm = VoiceActivityStateMachine::new(0, 0, 20);
        assert!(sm.update(true)); // opens; min_tx_remaining=2
        assert!(sm.update(false)); // min_tx_remaining=1; still open
        assert!(!sm.update(false)); // min_tx_remaining=0; gate closes
    }

    #[test]
    fn reset_clears_all_state() {
        let mut sm = VoiceActivityStateMachine::new(0, 100, 0);
        assert!(sm.update(true)); // open
        sm.reset();
        assert!(!sm.active());
        // After reset, gate should not be open even with hangover pending.
        assert!(!sm.update(false));
    }

    #[test]
    fn default_uses_p1_spec_values() {
        let sm = VoiceActivityStateMachine::default();
        assert_eq!(sm.open_after_frames, VAD_OPEN_AFTER_MS / 10);
        assert_eq!(sm.hangover_frames, VAD_HANGOVER_MS / 10);
        assert_eq!(sm.min_tx_frames, VAD_MIN_TX_MS / 10);
    }

    #[test]
    fn live_config_update_shortens_existing_hangover() {
        let mut sm = VoiceActivityStateMachine::new(0, 1000, 0);
        assert!(sm.update(true));
        assert!(sm.update(false));
        sm.configure(0, 100, 0);
        for _ in 0..10 {
            assert!(sm.update(false));
        }
        assert!(!sm.update(false));
    }

    #[test]
    fn stale_closed_decisions_cannot_hold_gate_forever() {
        let mut sm = VoiceActivityStateMachine::new(0, 500, 0);
        assert!(sm.update(true));
        for _ in 0..49 {
            assert!(sm.update(false));
        }
        assert!(!sm.update(false));
    }

    #[test]
    fn speech_decision_resets_weak_hold_limit() {
        let mut sm = VoiceActivityStateMachine::new(0, 500, 0);
        assert!(sm.update(true));
        for _ in 0..40 {
            assert!(sm.update(false));
        }
        assert!(sm.update(true));
        for _ in 0..40 {
            assert!(sm.update(false));
        }
        assert!(sm.active());
    }
}
