//! Cross-platform transmit-mode selector (SAD-083).
//!
//! Single writer of `transmit_active` other than the missed-key-up
//! watchdog (SAD-079). Computes the desired gate state from four
//! lock-free inputs:
//!
//! * `mode` — current [`TransmitMode`]
//! * `in_channel` — true when the session is in a voice channel
//! * `hard_mute` — final clamp; forces `false` regardless of mode
//! * `ptt_held` — raw key state (via [`crate::ReleaseTailTimer`]
//!   on PTT mode)
//!
//! Hard-mute is a final clamp; leaving the channel forces the gate
//! to `false`. `VoiceActivity` is driven by Rust-owned VAD state.
//!
//! All four inputs are stored as atomics so any thread can update
//! them without taking a lock. After each update we call
//! [`TransmitModeSelector::recompute`] which writes the resolved
//! desired value through the [`AudioTransmitGate`].

use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};

use tokio::sync::watch;

use crate::ptt::AudioTransmitGate;
use crate::transmit_mode::TransmitMode;

/// Resolved microphone-permission state mirrored from the platform
/// permission requester (SDD-106 §5). The audio-engine side treats
/// any non-`Granted` value as an authoritative clamp on the
/// transmit gate (SDD-106 §6, SRS-209).
///
/// Wire encoding is a plain `u8` so it can live in an
/// `AtomicU8` alongside the rest of [`TransmitModeSelector`]'s
/// inputs without taking a lock.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PermissionGate {
    /// No resolved value seen yet. [`TransmitModeSelector::compute`]
    /// treats this as not-granted, so any platform that publishes
    /// `Unknown` (e.g. the Android bridge before
    /// `checkSelfPermission` resolves) fails safe to listen-only
    /// per SRS-209. Note: the selector's *constructor default* is
    /// [`PermissionGate::Granted`] (see
    /// [`TransmitModeSelector::new`]) to preserve non-Android
    /// desktop behaviour where no permission event is ever
    /// published; the Android bridge overwrites the slot with the
    /// resolved cold-launch state via
    /// [`TransmitModeSelector::set_permission_state`] before
    /// `voice_join`.
    Unknown = 0,
    /// Permission granted; normal PTT / continuous evaluation
    /// resumes.
    Granted = 1,
    /// Permission denied (re-promptable). Transmit clamped to
    /// false (SDD-106 §6).
    Denied = 2,
    /// Permission permanently denied. Transmit clamped to false
    /// (SDD-106 §6); the UI is expected to deep-link to system
    /// settings (SDD-106 §3).
    PermanentlyDenied = 3,
}

impl PermissionGate {
    fn from_u8(v: u8) -> Self {
        match v {
            1 => Self::Granted,
            2 => Self::Denied,
            3 => Self::PermanentlyDenied,
            _ => Self::Unknown,
        }
    }

    fn as_u8(self) -> u8 {
        self as u8
    }

    /// True when the platform reports the microphone permission is
    /// usable. Any other state must clamp the transmit gate
    /// (SDD-106 §6).
    pub fn is_granted(self) -> bool {
        matches!(self, Self::Granted)
    }
}

/// Selector that maps user/session state to the `transmit_active`
/// gate (SAD-083).
pub struct TransmitModeSelector {
    gate: std::sync::RwLock<AudioTransmitGate>,
    mode: AtomicU8,
    in_channel: AtomicBool,
    hard_mute: AtomicBool,
    ptt_held: AtomicBool,
    voice_activity_open: AtomicBool,
    /// SDD-106 §5/§6 / SRS-209: latest resolved microphone
    /// permission state. Stored as a `u8` so writes from the
    /// JNI thread (Android permission requester → bridge) and
    /// reads from the audio thread are lock-free. The
    /// constructor default is [`PermissionGate::Granted`] to
    /// preserve non-Android desktop behaviour (no platform
    /// permission event is ever published there, so the field
    /// stays inert). The Android bridge overwrites this with the
    /// resolved cold-launch state via
    /// [`Self::set_permission_state`] before `voice_join` per
    /// SRS-209; any non-`Granted` value (including a
    /// platform-published `Unknown`) clamps transmit to false
    /// in [`Self::compute`].
    ///
    /// See [`Self::set_permission_state`] for the SDD-106 §6
    /// precedence-order documentation.
    permission_state: AtomicU8,
    /// Watch channel mirroring `ptt_held` transitions. The
    /// missed-key-up watchdog (SAD-079) subscribes here rather
    /// than to the gate, so it only fires when an actual PTT key
    /// press has been "stuck" for the timeout. In Continuous
    /// mode `ptt_held` is never written, so the watchdog never
    /// fires — that is the desired behaviour (Continuous is
    /// supposed to keep transmitting indefinitely).
    ptt_held_tx: watch::Sender<bool>,
}

impl TransmitModeSelector {
    /// Construct a selector wired to `gate`. Defaults:
    /// [`TransmitMode::Ptt`], not in channel, not muted, key
    /// released. The gate's initial value is left untouched until
    /// the first mutating call (which then writes the resolved
    /// value).
    pub fn new(gate: AudioTransmitGate) -> Self {
        let (ptt_held_tx, _rx) = watch::channel(false);
        Self {
            gate: std::sync::RwLock::new(gate),
            mode: AtomicU8::new(TransmitMode::default().as_u8()),
            in_channel: AtomicBool::new(false),
            hard_mute: AtomicBool::new(false),
            ptt_held: AtomicBool::new(false),
            voice_activity_open: AtomicBool::new(false),
            // SDD-106 §5: default to Granted on construction so
            // non-Android hosts (which never publish a permission
            // event) are not silently clamped. The Android bridge
            // overwrites this with the resolved cold-launch state
            // before voice_join per SRS-209.
            permission_state: AtomicU8::new(PermissionGate::Granted.as_u8()),
            ptt_held_tx,
        }
    }

    /// Update the cached microphone-permission state and
    /// re-evaluate the gate (SDD-106 §5/§6, SRS-209). Called from
    /// the Android JNI permission hook in `chanora_bridge`; on
    /// non-Android platforms this method is unused. The check
    /// inside [`Self::compute`] orders the permission clamp BEFORE
    /// hard-mute, channel membership, PTT, and transmit mode — a
    /// permission revocation immediately silences the microphone
    /// even mid-PTT.
    pub fn set_permission_state(&self, state: PermissionGate) {
        self.permission_state
            .store(state.as_u8(), Ordering::Relaxed);
        self.recompute();
    }

    /// Current cached microphone-permission state. Exposed for
    /// diagnostics and tests; the audio engine reads through the
    /// resolved transmit gate, not this field directly.
    pub fn permission_state(&self) -> PermissionGate {
        PermissionGate::from_u8(self.permission_state.load(Ordering::Relaxed))
    }

    /// Rewire the selector to a fresh [`AudioTransmitGate`]
    /// (typically the gate exposed by a newly-started
    /// [`crate::AudioEngine`]). Cached mode / channel / mute /
    /// ptt_held are preserved; the new gate is immediately
    /// updated to the resolved value.
    pub fn replace_gate(&self, gate: AudioTransmitGate) {
        if let Ok(mut g) = self.gate.write() {
            *g = gate;
        }
        self.recompute();
    }

    /// Update the selected mode and re-evaluate the gate.
    pub fn set_mode(&self, m: TransmitMode) {
        self.mode.store(m.as_u8(), Ordering::Relaxed);
        self.recompute();
    }

    /// Current selected mode.
    pub fn mode(&self) -> TransmitMode {
        TransmitMode::from_u8(self.mode.load(Ordering::Relaxed)).unwrap_or_default()
    }

    /// Update channel membership and re-evaluate.
    pub fn set_in_channel(&self, v: bool) {
        self.in_channel.store(v, Ordering::Relaxed);
        self.recompute();
    }

    /// Current channel-membership flag.
    pub fn in_channel(&self) -> bool {
        self.in_channel.load(Ordering::Relaxed)
    }

    /// Final-clamp hard mute. When `true` the gate is forced to
    /// `false` regardless of mode.
    pub fn set_hard_mute(&self, v: bool) {
        self.hard_mute.store(v, Ordering::Relaxed);
        self.recompute();
    }

    /// Current hard-mute flag.
    pub fn hard_mute(&self) -> bool {
        self.hard_mute.load(Ordering::Relaxed)
    }

    /// PTT key state (set by the platform input backend through
    /// [`crate::ReleaseTailTimer`]).
    pub fn set_ptt_held(&self, v: bool) {
        self.ptt_held.store(v, Ordering::Relaxed);
        // Best-effort publish for the missed-key-up watchdog;
        // watch::Sender::send returns Err only when there are no
        // receivers, which is fine.
        let _ = self.ptt_held_tx.send(v);
        self.recompute();
    }

    /// Current PTT-held flag.
    pub fn ptt_held(&self) -> bool {
        self.ptt_held.load(Ordering::Relaxed)
    }

    /// Rust-owned VAD gate input for VoiceActivity mode.
    pub fn set_voice_activity_open(&self, v: bool) {
        self.voice_activity_open.store(v, Ordering::Relaxed);
        self.recompute();
    }

    /// Current Rust-owned VAD gate state.
    pub fn voice_activity_open(&self) -> bool {
        self.voice_activity_open.load(Ordering::Relaxed)
    }

    /// Subscribe to `ptt_held` transitions. Used by the
    /// missed-key-up watchdog (SAD-079) so it fires on the actual
    /// PTT-key-down lifetime, not on the resolved `transmit_active`
    /// (which is supposed to stay `true` indefinitely in
    /// Continuous mode).
    pub fn subscribe_ptt_held(&self) -> watch::Receiver<bool> {
        self.ptt_held_tx.subscribe()
    }

    /// Shared snapshot of the underlying gate. Provided for the
    /// audio engine's hot read path. Returns a clone so callers
    /// don't hold the internal lock.
    pub fn gate(&self) -> AudioTransmitGate {
        self.gate
            .read()
            .expect("selector gate lock poisoned")
            .clone()
    }

    fn compute(&self) -> bool {
        // SDD-106 §6 / SRS-209: permission clamp has the highest
        // precedence. If the platform reports anything other than
        // `Granted` (including the cold-launch `Unknown`), transmit
        // is forced to false regardless of PTT key state, transmit
        // mode, channel membership, or hard-mute. This authoritative
        // clamp lives on the Rust side so a revocation event
        // received mid-PTT silences the microphone even if the Dart
        // UI has not yet re-rendered.
        if !PermissionGate::from_u8(self.permission_state.load(Ordering::Relaxed)).is_granted() {
            return false;
        }
        if self.hard_mute.load(Ordering::Relaxed) {
            return false;
        }
        if !self.in_channel.load(Ordering::Relaxed) {
            return false;
        }
        match self.mode() {
            TransmitMode::Ptt => self.ptt_held.load(Ordering::Relaxed),
            TransmitMode::Continuous => true,
            TransmitMode::VoiceActivity => self.voice_activity_open.load(Ordering::Relaxed),
        }
    }

    /// Recompute the desired gate state and publish it. Exposed
    /// for tests; callers normally trigger this implicitly through
    /// the setter methods.
    pub fn recompute(&self) {
        let desired = self.compute();
        if let Ok(g) = self.gate.read() {
            g.set(desired);
        }
    }
}

#[cfg(test)]
// Permission-clamp tests below cover SDD-106 §6 / DEC-030 /
// SRS-209 via SWE4-UV-053 (Denied/PermanentlyDenied/Unknown
// clamp), SWE4-UV-054 (Granted release), and SWE4-UV-055
// (non-RECORD_AUDIO permissions do not clamp). Precedence and
// replace_gate extension tests cite SWE4-UV-053 as the
// precedence-ordering extension.
mod tests {
    use super::*;

    fn fresh() -> (AudioTransmitGate, TransmitModeSelector) {
        let g = AudioTransmitGate::new(false);
        let s = TransmitModeSelector::new(g.clone());
        (g, s)
    }

    /// SWE4-UV-037 (PTT + gate): PTT requires both in-channel and key-held.
    #[test]
    fn ptt_requires_channel_and_key() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Ptt);
        s.set_ptt_held(true);
        assert!(!g.load(), "no channel -> false");
        s.set_in_channel(true);
        assert!(g.load(), "channel + key -> true");
        s.set_ptt_held(false);
        assert!(!g.load(), "key released -> false");
    }

    /// SWE4-UV-037: continuous-transmit mode ignores PTT key state.
    #[test]
    fn continuous_ignores_ptt_held() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Continuous);
        s.set_in_channel(true);
        assert!(g.load(), "channel + continuous -> true");
        s.set_ptt_held(true);
        assert!(g.load());
        s.set_ptt_held(false);
        assert!(g.load(), "continuous independent of key state");
    }

    /// SWE4-UV-037: voice-activity mode follows VAD state.
    #[test]
    fn voice_activity_requires_vad_open() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::VoiceActivity);
        s.set_in_channel(true);
        assert!(!g.load());
        s.set_voice_activity_open(true);
        assert!(g.load());
        s.set_voice_activity_open(false);
        assert!(!g.load());
    }

    /// SWE4-UV-037 / SWE4-UV-041: hard-mute clamps the transmit
    /// gate to false (mirrors the Android permission-revoked clamp).
    #[test]
    fn hard_mute_clamps() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Continuous);
        s.set_in_channel(true);
        assert!(g.load());
        s.set_hard_mute(true);
        assert!(!g.load(), "hard mute clamps to false");
        s.set_hard_mute(false);
        assert!(g.load());
    }

    /// SWE4-UV-037: leaving the channel forces the gate to false.
    #[test]
    fn leaving_channel_forces_false() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Continuous);
        s.set_in_channel(true);
        assert!(g.load());
        s.set_in_channel(false);
        assert!(!g.load());
    }

    /// SDD-106 §6 / SRS-209 / DEC-030 / SWE4-UV-053: a `Denied`
    /// (or `PermanentlyDenied`, or cold-launch `Unknown`)
    /// permission state clamps transmit to false even with PTT
    /// held, channel joined, and hard-mute released. The clamp
    /// must hold from the instant the event is published.
    #[test]
    fn permission_state_denied_clamps_transmit_to_false() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Ptt);
        s.set_in_channel(true);
        s.set_ptt_held(true);
        // Sanity: with default Granted the gate is hot.
        assert!(g.load(), "baseline: PTT + channel + granted -> true");

        s.set_permission_state(PermissionGate::Denied);
        assert!(
            !g.load(),
            "SDD-106 §6: Denied clamps transmit regardless of PTT/channel"
        );

        s.set_permission_state(PermissionGate::PermanentlyDenied);
        assert!(
            !g.load(),
            "SDD-106 §6: PermanentlyDenied clamps transmit regardless of PTT/channel"
        );

        // Continuous + permission denied is also clamped — the
        // mode does not override the permission check.
        s.set_mode(TransmitMode::Continuous);
        assert!(
            !g.load(),
            "SDD-106 §6: permission clamp wins over Continuous mode"
        );

        // And the Unknown cold-launch state is treated as not
        // granted (fail-safe per SRS-209).
        s.set_permission_state(PermissionGate::Unknown);
        assert!(
            !g.load(),
            "SDD-106 §6 / SRS-209: Unknown defaults to listen-only"
        );
    }

    /// SDD-106 §6 / SWE4-UV-054: after a Denied state, a
    /// subsequent Granted transition releases the clamp and
    /// normal PTT-driven evaluation resumes on the next state
    /// tick.
    #[test]
    fn permission_state_granted_releases_clamp() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Ptt);
        s.set_in_channel(true);
        s.set_ptt_held(true);

        s.set_permission_state(PermissionGate::Denied);
        assert!(!g.load(), "precondition: clamped under Denied");

        s.set_permission_state(PermissionGate::Granted);
        assert!(
            g.load(),
            "SDD-106 §6: Granted restores normal PTT-driven evaluation"
        );

        // Releasing the key drops the gate as usual — the clamp is
        // no longer in effect so PTT semantics apply.
        s.set_ptt_held(false);
        assert!(!g.load());
    }

    /// SDD-106 §5/§6 / SWE4-UV-055: a permission event for a
    /// non-microphone permission (e.g. `POST_NOTIFICATIONS`)
    /// must not affect the transmit gate. The selector only
    /// exposes a single permission slot — by contract the
    /// bridge filters to `RECORD_AUDIO` before calling
    /// [`set_permission_state`]. This test pins the selector's
    /// contract: never-written means never-clamped from this
    /// code path.
    #[test]
    fn permission_state_for_other_permission_does_not_clamp() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Continuous);
        s.set_in_channel(true);
        assert!(g.load());

        // The selector exposes no setter for non-RECORD_AUDIO
        // permissions; demonstrating contractually that without a
        // call to set_permission_state, the gate is unaffected.
        // (The bridge JNI hook is responsible for filtering.) The
        // selector remains in its Granted default.
        assert_eq!(s.permission_state(), PermissionGate::Granted);
        assert!(
            g.load(),
            "SDD-106 §5: unrelated permission events leave transmit unaffected"
        );
    }

    /// SDD-106 §6 / SWE4-UV-053 (precedence-ordering extension):
    /// the permission clamp and hard-mute clamp compose
    /// independently. When both are active the gate is false;
    /// releasing the permission clamp alone leaves the gate
    /// false because hard-mute still holds; releasing both with
    /// PTT held + in-channel restores transmit.
    #[test]
    fn permission_clamp_takes_precedence_over_hard_mute() {
        let (g, s) = fresh();
        s.set_mode(TransmitMode::Ptt);
        s.set_in_channel(true);
        s.set_ptt_held(true);
        s.set_hard_mute(true);
        s.set_permission_state(PermissionGate::Denied);
        assert!(!g.load(), "both clamps active -> false");

        // Release the permission clamp; hard-mute still holds.
        s.set_permission_state(PermissionGate::Granted);
        assert!(
            !g.load(),
            "SDD-106 §6: hard-mute continues to clamp after permission release"
        );

        // Release hard-mute too; PTT + channel + granted -> true.
        s.set_hard_mute(false);
        assert!(g.load(), "both clamps cleared, PTT held -> true");
    }

    /// SDD-106 §6 / SWE4-UV-053 (precedence-ordering extension):
    /// the permission clamp survives a [`TransmitModeSelector::
    /// replace_gate`] hot-swap. After the audio engine restarts
    /// and a fresh gate is wired in, the clamp must still be
    /// observed by the new gate immediately.
    #[test]
    fn permission_clamp_survives_replace_gate() {
        let (_g, s) = fresh();
        s.set_mode(TransmitMode::Continuous);
        s.set_in_channel(true);
        s.set_permission_state(PermissionGate::Denied);

        // Hot-swap to a fresh gate (simulating an audio-engine
        // restart). The new gate must observe the clamp on the
        // first recompute.
        let new_gate = AudioTransmitGate::new(true);
        s.replace_gate(new_gate.clone());
        assert!(
            !new_gate.load(),
            "SDD-106 §6: clamp survives replace_gate hot-swap"
        );

        // Releasing the clamp re-enables transmit on the new gate.
        s.set_permission_state(PermissionGate::Granted);
        assert!(new_gate.load(), "Granted on new gate -> true");
    }
}
