//! Audio playout diagnostic probe.
//!
//! Drives a phase-continuous sine tone through the *real* playback path
//! (PipeWire on Linux, cpal/WASAPI on Windows), using the *same* fill-paced
//! production the production mixer uses (`core/mod.rs`): generate a frame only while the
//! playback ring is below `PLAYBACK_TARGET_SAMPLES`, so production tracks the
//! hardware clock. No network, no microphone — this isolates the local output
//! path so we can confirm the clock-paced playout is glitch-free.
//!
//! Use your ears on the tone (any click/pop is a glitch) together with the
//! `playout-health:` lines tailed to stdout:
//!   - all-zero health lines + clean tone  → local playout is healthy; any
//!     crackle on real calls is upstream (per-peer jitter buffer), not this ring.
//!   - steady `underrun +N samples/s` (or steady `dropped +N frames/s`) with a
//!     slowly drifting `fill`  → clock drift (producer vs hardware clock).
//!   - random bursts correlated with system load  → scheduling jitter.
//!
//! Run:  cargo run --bin audio_probe -- [freq_hz] [seconds] [target_node]
//!   e.g. cargo run --release --bin audio_probe -- 440 30
//!
//! This probe exercises the platform playback backend directly: PipeWire on Linux
//! and cpal/WASAPI on Windows. Other targets use a stub that explains the limitation.

#[cfg(target_os = "linux")]
fn main() {
    unix_probe::run();
}

#[cfg(windows)]
fn main() {
    win_probe::run();
}

#[cfg(not(any(target_os = "linux", windows)))]
fn main() {
    eprintln!(
        "audio_probe is only supported on Linux and Windows builds (it drives the platform playback backend directly)."
    );
}

#[cfg(target_os = "linux")]
mod unix_probe {
    use std::io::{BufRead, BufReader, Seek, SeekFrom};
    use std::sync::Arc;
    use std::sync::atomic::AtomicUsize;
    use std::sync::mpsc;
    use std::time::Duration;

    use peerspeak::audio::AudioBackend;
    use peerspeak::audio::pipewire_impl::PipeWireBackend;
    use peerspeak::core::jitter::FRAME_SAMPLES; // 960 mono frames = 20ms @ 48kHz

    const SAMPLE_RATE: f32 = 48_000.0;

    #[tokio::main]
    pub async fn run() {
        let mut args = std::env::args().skip(1);
        let freq: f32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(440.0);
        let secs: u64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(30);
        let target_node: Option<String> = args.next();

        // The playout-health logger is quiet in normal operation (it only logs
        // glitches); ask it for the full once-per-second heartbeat so the probe can
        // show the steady-state numbers.
        // SAFETY: set before any playback thread starts, so no concurrent env read.
        unsafe { std::env::set_var("PEERSPEAK_AUDIO_VERBOSE", "1") };

        println!("audio_probe: {freq} Hz tone for {secs}s through the real playback path.");
        println!("Listen for clicks/pops; watch the playout-health lines below.\n");

        // Tail the app log (where playout-health lines land) to stdout in the
        // background so it's all in one terminal.
        spawn_log_tailer();

        let backend = PipeWireBackend::new();
        let (tx, rx) = mpsc::channel::<Vec<i16>>();
        let ring_fill = Arc::new(AtomicUsize::new(0));
        if let Err(e) = backend.start_playback(rx, target_node, ring_fill.clone()) {
            eprintln!("failed to start playback: {e}");
            return;
        }

        // Phase-continuous sine, generated one 20ms frame at a time, fill-paced
        // exactly like the production mixer: only produce while the ring is below
        // target, so production tracks the PipeWire hardware clock.
        use std::sync::atomic::Ordering;
        let deadline = tokio::time::Instant::now() + Duration::from_secs(secs);
        let mut n: u64 = 0; // running sample index keeps phase continuous across frames
        while tokio::time::Instant::now() < deadline {
            if ring_fill.load(Ordering::Relaxed) >= peerspeak::audio::PLAYBACK_TARGET_SAMPLES {
                tokio::time::sleep(Duration::from_millis(2)).await;
                continue;
            }
            let mut frame = Vec::with_capacity(FRAME_SAMPLES * peerspeak::audio::PLAYBACK_CHANNELS);
            for _ in 0..FRAME_SAMPLES {
                let t = n as f32 / SAMPLE_RATE;
                // 0.25 amplitude: clearly audible but not harsh.
                let sample =
                    (0.25 * i16::MAX as f32 * (2.0 * std::f32::consts::PI * freq * t).sin()) as i16;
                // Stereo playback bus: duplicate the probe tone to L/R.
                frame.push(sample);
                frame.push(sample);
                n += 1;
            }
            if tx.send(frame).is_err() {
                eprintln!("playback channel closed early");
                break;
            }
        }

        // Let the ring drain, then stop.
        tokio::time::sleep(Duration::from_millis(300)).await;
        let _ = backend.stop();
        println!("\naudio_probe: done.");
    }

    /// Open the app log, seek to the end, and echo new lines (the `playout-health:`
    /// reports) to stdout once they appear.
    fn spawn_log_tailer() {
        let path = peerspeak::log_file_path();
        std::thread::spawn(move || {
            // Wait for the file to exist (first log_msg creates it).
            let file = loop {
                if let Ok(f) = std::fs::File::open(&path) {
                    break f;
                }
                std::thread::sleep(Duration::from_millis(100));
            };
            let mut reader = BufReader::new(file);
            let _ = reader.seek(SeekFrom::End(0));
            loop {
                let mut line = String::new();
                match reader.read_line(&mut line) {
                    Ok(0) => std::thread::sleep(Duration::from_millis(150)),
                    Ok(_) => {
                        if line.contains("playout-health:") {
                            print!("{line}");
                        }
                    }
                    Err(_) => std::thread::sleep(Duration::from_millis(150)),
                }
            }
        });
    }
}

#[cfg(windows)]
mod win_probe {
    use std::io::{BufRead, BufReader, Seek, SeekFrom};
    use std::sync::Arc;
    use std::sync::atomic::AtomicUsize;
    use std::sync::mpsc;
    use std::time::Duration;

    use peerspeak::audio::AudioBackend;
    use peerspeak::audio::cpal_impl::CpalBackend;
    use peerspeak::core::jitter::FRAME_SAMPLES; // 960 mono frames = 20ms @ 48kHz

    const SAMPLE_RATE: f32 = 48_000.0;

    #[tokio::main]
    pub async fn run() {
        let mut args = std::env::args().skip(1);
        let freq: f32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(440.0);
        let secs: u64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(30);
        let target_node: Option<String> = args.next();

        // The playout-health logger is quiet in normal operation (it only logs
        // glitches); ask it for the full once-per-second heartbeat so the probe can
        // show the steady-state numbers.
        // SAFETY: set before any playback thread starts, so no concurrent env read.
        unsafe { std::env::set_var("PEERSPEAK_AUDIO_VERBOSE", "1") };

        println!("audio_probe: {freq} Hz tone for {secs}s through the real playback path.");
        println!("Listen for clicks/pops; watch the playout-health lines below.\n");

        // Tail the app log (where playout-health lines land) to stdout in the
        // background so it's all in one terminal.
        spawn_log_tailer();

        let backend = CpalBackend::new();
        let (tx, rx) = mpsc::channel::<Vec<i16>>();
        let ring_fill = Arc::new(AtomicUsize::new(0));
        if let Err(e) = backend.start_playback(rx, target_node, ring_fill.clone()) {
            eprintln!("failed to start playback: {e}");
            return;
        }

        // Phase-continuous sine, generated one 20ms frame at a time, fill-paced
        // exactly like the production mixer: only produce while the ring is below
        // target, so production tracks the cpal/WASAPI hardware clock.
        use std::sync::atomic::Ordering;
        let deadline = tokio::time::Instant::now() + Duration::from_secs(secs);
        let mut n: u64 = 0; // running sample index keeps phase continuous across frames
        while tokio::time::Instant::now() < deadline {
            if ring_fill.load(Ordering::Relaxed) >= peerspeak::audio::PLAYBACK_TARGET_SAMPLES {
                tokio::time::sleep(Duration::from_millis(2)).await;
                continue;
            }
            let mut frame = Vec::with_capacity(FRAME_SAMPLES * peerspeak::audio::PLAYBACK_CHANNELS);
            for _ in 0..FRAME_SAMPLES {
                let t = n as f32 / SAMPLE_RATE;
                // 0.25 amplitude: clearly audible but not harsh.
                let sample =
                    (0.25 * i16::MAX as f32 * (2.0 * std::f32::consts::PI * freq * t).sin()) as i16;
                // Stereo playback bus: duplicate the probe tone to L/R.
                frame.push(sample);
                frame.push(sample);
                n += 1;
            }
            if tx.send(frame).is_err() {
                eprintln!("playback channel closed early");
                break;
            }
        }

        // Let the ring drain, then stop.
        tokio::time::sleep(Duration::from_millis(300)).await;
        let _ = backend.stop();
        println!("\naudio_probe: done.");
    }

    /// Open the app log, seek to the end, and echo new lines (the `playout-health:`
    /// reports) to stdout once they appear.
    fn spawn_log_tailer() {
        let path = peerspeak::log_file_path();
        std::thread::spawn(move || {
            // Wait for the file to exist (first log_msg creates it).
            let file = loop {
                if let Ok(f) = std::fs::File::open(&path) {
                    break f;
                }
                std::thread::sleep(Duration::from_millis(100));
            };
            let mut reader = BufReader::new(file);
            let _ = reader.seek(SeekFrom::End(0));
            loop {
                let mut line = String::new();
                match reader.read_line(&mut line) {
                    Ok(0) => std::thread::sleep(Duration::from_millis(150)),
                    Ok(_) => {
                        if line.contains("playout-health:") {
                            print!("{line}");
                        }
                    }
                    Err(_) => std::thread::sleep(Duration::from_millis(150)),
                }
            }
        });
    }
}
