//! macOS desktop PTT backend (SDD-085).
//!
//! Two-level ladder per SAD-073: `MacOSEventTapBackend` when the
//! Input Monitoring (or Accessibility, depending on the macOS
//! release) permission is granted, with `FocusedPttBackend` as
//! the terminal fallback when the permission is denied,
//! undetermined, or revoked.
//!
//! The audio engine must start without blocking on the permission
//! prompt (SRS-198). The backend therefore queries the permission
//! state synchronously, immediately reports the corresponding
//! capability level, and re-queries the permission state on a
//! background thread so a runtime grant or revocation upgrades or
//! downgrades the descriptor through the `descriptor_watch`
//! channel — the supervisor in `chanora_core` forwards the
//! transition to `SessionEvent::PttCapability` and the Flutter
//! capability badge updates without a restart.

use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use tokio::sync::watch;
use tracing::{info, warn};

use super::{
    AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding,
};
use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PermissionState {
    Granted,
    Denied,
    Undetermined,
}

impl PermissionState {
    /// Encode as a u8 for `AtomicU8` storage. The encoding lets
    /// the descriptor-watch worker compare-and-swap permission
    /// state across thread boundaries without a Mutex.
    const fn to_u8(self) -> u8 {
        match self {
            Self::Granted => 0,
            Self::Denied => 1,
            Self::Undetermined => 2,
        }
    }

    fn from_u8(v: u8) -> Self {
        match v {
            0 => Self::Granted,
            1 => Self::Denied,
            _ => Self::Undetermined,
        }
    }
}

fn query_permission() -> PermissionState {
    // Live `IOHIDCheckAccess` query lands in the macOS platform
    // verification commit (it needs the IOKit framework link).
    // Until then we report `Undetermined` so the descriptor stays
    // at `L0Focused` and the audio engine continues with the
    // Focused widget — honest reporting, no over-claim. The
    // periodic re-query loop below still exercises the watch
    // sender path so a future replacement of this function
    // automatically engages the runtime upgrade.
    PermissionState::Undetermined
}

/// Try to construct the macOS event-tap backend. Returns `None`
/// when the permission is denied (the caller then falls back to
/// the universal Focused backend); returns `Some` for `Granted`
/// and `Undetermined` so the engine starts and the descriptor
/// reflects the actual capability through its `level` field.
pub fn try_select() -> Option<Box<dyn DesktopPttBackend>> {
    let state = query_permission();
    match state {
        PermissionState::Denied => {
            warn!(
                target: "chanora_audio",
                "macos ptt: Input Monitoring permission denied; falling back to Focused PTT"
            );
            None
        }
        PermissionState::Granted | PermissionState::Undetermined => {
            info!(
                target: "chanora_audio",
                permission = ?state,
                "macos ptt: selecting Event Tap backend"
            );
            Some(Box::new(MacOSEventTapBackend::new(state)))
        }
    }
}

pub struct MacOSEventTapBackend {
    binding: PttBinding,
    gate: Option<AudioTransmitGate>,
    /// Shared permission state — written by the re-query worker
    /// thread, read by `descriptor()` so synchronous descriptor
    /// queries always return the live value.
    permission: Arc<AtomicU8>,
    desc_tx: watch::Sender<PttBackendDescriptor>,
    desc_rx: watch::Receiver<PttBackendDescriptor>,
    stop: Arc<AtomicBool>,
    worker: Option<thread::JoinHandle<()>>,
    permission_worker: Option<thread::JoinHandle<()>>,
}

impl MacOSEventTapBackend {
    fn new(permission: PermissionState) -> Self {
        let initial = PttBackendDescriptor {
            level: match permission {
                PermissionState::Granted => PttCapabilityLevel::L2GlobalHoldToTalk,
                _ => PttCapabilityLevel::L0Focused,
            },
            backend_id: "event-tap",
            bound_input_class: None,
        };
        let (desc_tx, desc_rx) = watch::channel(initial);
        Self {
            binding: PttBinding::none(),
            gate: None,
            permission: Arc::new(AtomicU8::new(permission.to_u8())),
            desc_tx,
            desc_rx,
            stop: Arc::new(AtomicBool::new(false)),
            worker: None,
            permission_worker: None,
        }
    }

    /// Build the descriptor for a given permission + binding pair
    /// (used by `descriptor()` and the re-query worker).
    fn build_descriptor(
        permission: PermissionState,
        class: super::PttInputClass,
    ) -> PttBackendDescriptor {
        let level = match permission {
            PermissionState::Granted => match class {
                super::PttInputClass::MouseSideButton => {
                    PttCapabilityLevel::L3GlobalWithMouseButtons
                }
                _ => PttCapabilityLevel::L2GlobalHoldToTalk,
            },
            // Undetermined or Denied (we wouldn't be here for
            // Denied but the match is exhaustive) reports
            // Focused so capability advertising matches actual
            // runtime behaviour (SRS-196 / SRS-198).
            _ => PttCapabilityLevel::L0Focused,
        };
        PttBackendDescriptor {
            level,
            backend_id: "event-tap",
            bound_input_class: match class {
                super::PttInputClass::None => None,
                super::PttInputClass::MouseSideButton => Some("mouse-side-button"),
                _ => Some("keyboard"),
            },
        }
    }
}

impl DesktopPttBackend for MacOSEventTapBackend {
    fn descriptor(&self) -> PttBackendDescriptor {
        let perm = PermissionState::from_u8(self.permission.load(Ordering::Relaxed));
        Self::build_descriptor(perm, self.binding.input_class)
    }

    fn descriptor_watch(&self) -> watch::Receiver<PttBackendDescriptor> {
        self.desc_rx.clone()
    }

    fn start(
        &mut self,
        gate: AudioTransmitGate,
        binding: PttBinding,
    ) -> Result<(), PttBackendError> {
        self.gate = Some(gate.clone());
        self.binding = binding;
        let stop = self.stop.clone();

        // Spawn the event-tap worker. Live `CGEventTapCreate` +
        // run-loop wiring lands in the macOS platform
        // verification commit; the scaffolding keeps the
        // lifecycle correct so the watchdog + capability event
        // are exercisable now.
        let handle = thread::Builder::new()
            .name("chanora-eventtap".into())
            .spawn(move || {
                while !stop.load(Ordering::Relaxed) {
                    thread::sleep(Duration::from_millis(50));
                }
            })
            .map_err(|e| PttBackendError::Init(format!("eventtap thread: {e}")))?;
        self.worker = Some(handle);

        // Spawn the permission re-query worker (SRS-198 runtime
        // upgrade hook). Polls every 1.5 s; on transition,
        // republishes the descriptor through the watch sender so
        // `chanora_core::ChanoraSession::start_audio`'s forwarder
        // re-emits `SessionEvent::PttCapability` and the Flutter
        // capability badge updates without a restart.
        //
        // Polling rather than KVO / notifications because the
        // macOS Input-Monitoring permission has no public change
        // notification API; polling at 1.5 s is acceptable for a
        // user-action follow-up (the user grants then returns to
        // Chanora, and the badge updates within ~1.5 s).
        let perm_stop = self.stop.clone();
        let perm_atomic = self.permission.clone();
        let perm_desc_tx = self.desc_tx.clone();
        let perm_binding_class = self.binding.input_class;
        let perm_handle = thread::Builder::new()
            .name("chanora-perm-watch".into())
            .spawn(move || {
                let mut last = PermissionState::from_u8(perm_atomic.load(Ordering::Relaxed));
                while !perm_stop.load(Ordering::Relaxed) {
                    thread::sleep(Duration::from_millis(1500));
                    let now = query_permission();
                    if now != last {
                        perm_atomic.store(now.to_u8(), Ordering::Relaxed);
                        let desc = MacOSEventTapBackend::build_descriptor(
                            now,
                            perm_binding_class,
                        );
                        let _ = perm_desc_tx.send(desc);
                        info!(
                            target: "chanora_audio",
                            from = ?last,
                            to = ?now,
                            "macos ptt: permission state transition"
                        );
                        last = now;
                    }
                }
            })
            .map_err(|e| PttBackendError::Init(format!("perm-watch thread: {e}")))?;
        self.permission_worker = Some(perm_handle);

        // Publish the initial descriptor synchronously so
        // consumers see the current value immediately rather
        // than after the first re-query tick.
        let _ = self.desc_tx.send(self.descriptor());

        let _ = &gate; // captured for future live wiring
        Ok(())
    }

    fn stop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(h) = self.worker.take() {
            let _ = h.join();
        }
        if let Some(h) = self.permission_worker.take() {
            let _ = h.join();
        }
        if let Some(g) = self.gate.take() {
            g.set(false);
        }
    }

    fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> {
        self.binding = binding;
        // Re-publish the descriptor so the bound_input_class
        // change (keyboard → mouse-side-button, or vice versa)
        // reaches the UI capability badge promptly.
        let _ = self.desc_tx.send(self.descriptor());
        Ok(())
    }
}

impl Drop for MacOSEventTapBackend {
    fn drop(&mut self) {
        self.stop();
    }
}

#[cfg(test)]
mod tests {
    //! Unit tests for the macOS permission state machine and the
    //! descriptor builder. The live `IOHIDCheckAccess` query is
    //! covered by the macOS platform-verification commit; here we
    //! exercise the parts that are platform-independent.

    use super::*;

    #[test]
    fn build_descriptor_undetermined_reports_focused() {
        let d = MacOSEventTapBackend::build_descriptor(
            PermissionState::Undetermined,
            super::super::PttInputClass::Keyboard,
        );
        assert_eq!(d.level, PttCapabilityLevel::L0Focused);
        assert_eq!(d.backend_id, "event-tap");
    }

    #[test]
    fn build_descriptor_denied_reports_focused() {
        let d = MacOSEventTapBackend::build_descriptor(
            PermissionState::Denied,
            super::super::PttInputClass::Keyboard,
        );
        assert_eq!(d.level, PttCapabilityLevel::L0Focused);
    }

    #[test]
    fn build_descriptor_granted_keyboard_reports_L2() {
        let d = MacOSEventTapBackend::build_descriptor(
            PermissionState::Granted,
            super::super::PttInputClass::Keyboard,
        );
        assert_eq!(d.level, PttCapabilityLevel::L2GlobalHoldToTalk);
        assert_eq!(d.bound_input_class, Some("keyboard"));
    }

    #[test]
    fn build_descriptor_granted_mouse_reports_L3() {
        let d = MacOSEventTapBackend::build_descriptor(
            PermissionState::Granted,
            super::super::PttInputClass::MouseSideButton,
        );
        assert_eq!(d.level, PttCapabilityLevel::L3GlobalWithMouseButtons);
        assert_eq!(d.bound_input_class, Some("mouse-side-button"));
    }

    #[test]
    fn build_descriptor_granted_none_reports_L2_keyboard() {
        // `PttInputClass::None` is a sentinel for "no binding".
        // The descriptor still reports L2 because the permission
        // is granted; the bound_input_class is None.
        let d = MacOSEventTapBackend::build_descriptor(
            PermissionState::Granted,
            super::super::PttInputClass::None,
        );
        assert_eq!(d.level, PttCapabilityLevel::L2GlobalHoldToTalk);
        assert_eq!(d.bound_input_class, None);
    }

    #[test]
    fn permission_state_atomic_roundtrip() {
        for s in [
            PermissionState::Granted,
            PermissionState::Denied,
            PermissionState::Undetermined,
        ] {
            assert_eq!(PermissionState::from_u8(s.to_u8()), s);
        }
    }
}
