//! Async WAV debug dump writer for P1 diagnostics.
//!
//! Captures three streams for offline analysis:
//!   * `raw_mic`          — before AudioProcessor (INV_007: never from callback)
//!   * `render_reference` — remote mixer output before playout
//!   * `processed_mic`    — after AudioProcessor
//!
//! ## Design
//!
//! The realtime callback MUST NOT write to disk (INV_007). Instead it
//! pushes 10 ms f32 frames onto a bounded `std::sync::mpsc` channel.
//! A background `tokio::task` drains the channel and writes WAV data.
//!
//! The channel is bounded (capacity = 500 frames ≈ 5 s of audio per
//! stream). If the writer falls behind, frames are dropped rather than
//! blocking the callback thread.
//!
//! WAV files are written to the OS temp directory with a filename that
//! encodes the stream name, route, backend, and a timestamp so
//! multiple sessions don't overwrite each other.
//!
//! ## Usage
//!
//! ```ignore
//! let writer = WavDebugRecorder::start(route, backend);
//! // In realtime callback (non-blocking):
//! writer.push_raw_mic(&frame);
//! writer.push_render_reference(&frame);
//! writer.push_processed_mic(&frame);
//! // On session end:
//! writer.stop(); // flushes and closes files
//! ```

use std::io::Write;
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::{SystemTime, UNIX_EPOCH};

use tracing::{info, warn};

use crate::audio_processing::{AudioBackend, AudioRoute};
use crate::frame::FRAME_10MS_SAMPLES;

/// Maximum number of 10 ms frames buffered per stream before drops.
const CHANNEL_CAPACITY: usize = 500;

/// Sample rate for WAV output (matches the capture pipeline).
const WAV_SAMPLE_RATE: u32 = 48_000;

/// Identifies which debug stream a frame belongs to.
#[derive(Debug, Clone, Copy)]
enum StreamId {
    RawMic,
    RenderReference,
    ProcessedMic,
}

/// A single 10 ms frame tagged with its stream.
struct DebugFrame {
    stream: StreamId,
    samples: Box<[f32; FRAME_10MS_SAMPLES]>,
}

/// Handle for pushing frames from the realtime callback.
///
/// All push methods are non-blocking: if the channel is full the
/// frame is silently dropped and a counter is incremented.
pub struct WavDebugRecorder {
    tx: mpsc::SyncSender<DebugFrame>,
    /// Frames dropped due to full channel (diagnostic only).
    drops: std::sync::atomic::AtomicU64,
    /// Whether the recorder is active (set to false on stop).
    active: std::sync::atomic::AtomicBool,
}

impl WavDebugRecorder {
    /// Start the async WAV writer task. Returns a handle for pushing
    /// frames from the realtime callback.
    ///
    /// `route` and `backend` are embedded in the output filenames.
    pub fn start(route: AudioRoute, backend: AudioBackend) -> std::sync::Arc<Self> {
        let (tx, rx) = mpsc::sync_channel::<DebugFrame>(CHANNEL_CAPACITY);
        let recorder = std::sync::Arc::new(Self {
            tx,
            drops: std::sync::atomic::AtomicU64::new(0),
            active: std::sync::atomic::AtomicBool::new(true),
        });

        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);
        let route_str = route.as_str().to_string();
        let backend_str = backend.as_str().to_string();

        // Spawn a blocking task so the WAV writer doesn't compete
        // with the tokio async executor for CPU time.
        std::thread::Builder::new()
            .name("chanora-wav-writer".to_string())
            .spawn(move || {
                wav_writer_task(rx, &route_str, &backend_str, ts);
            })
            .ok();

        recorder
    }

    /// Push a raw mic frame (before AudioProcessor). Non-blocking.
    pub fn push_raw_mic(&self, samples: &[f32; FRAME_10MS_SAMPLES]) {
        self.push(StreamId::RawMic, samples);
    }

    /// Push a render-reference frame (remote mixer output before playout).
    /// Non-blocking.
    pub fn push_render_reference(&self, samples: &[f32; FRAME_10MS_SAMPLES]) {
        self.push(StreamId::RenderReference, samples);
    }

    /// Push a processed mic frame (after AudioProcessor). Non-blocking.
    pub fn push_processed_mic(&self, samples: &[f32; FRAME_10MS_SAMPLES]) {
        self.push(StreamId::ProcessedMic, samples);
    }

    /// Stop the recorder. Drops the sender so the writer task drains
    /// and closes the WAV files.
    pub fn stop(&self) {
        self.active
            .store(false, std::sync::atomic::Ordering::Relaxed);
        // The sender is not dropped here because `self` is behind Arc.
        // The writer task will exit when all senders are dropped (i.e.
        // when the Arc is dropped). This is intentional: the task
        // drains any remaining frames before closing files.
    }

    /// Number of frames dropped due to a full channel.
    pub fn drop_count(&self) -> u64 {
        self.drops.load(std::sync::atomic::Ordering::Relaxed)
    }

    fn push(&self, stream: StreamId, samples: &[f32; FRAME_10MS_SAMPLES]) {
        if !self.active.load(std::sync::atomic::Ordering::Relaxed) {
            return;
        }
        let mut boxed = Box::new([0.0_f32; FRAME_10MS_SAMPLES]);
        boxed.copy_from_slice(samples);
        let frame = DebugFrame {
            stream,
            samples: boxed,
        };
        if self.tx.try_send(frame).is_err() {
            self.drops
                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        }
    }
}

// ---------- WAV writer task ----------

struct WavFile {
    path: PathBuf,
    file: std::fs::File,
    samples_written: u32,
}

impl WavFile {
    fn create(dir: &std::path::Path, name: &str) -> Option<Self> {
        let path = dir.join(name);
        match std::fs::File::create(&path) {
            Ok(mut file) => {
                // Write a placeholder WAV header; we'll patch it on close.
                if write_wav_header(&mut file, 0).is_ok() {
                    Some(Self {
                        path,
                        file,
                        samples_written: 0,
                    })
                } else {
                    None
                }
            }
            Err(e) => {
                warn!(target: "chanora_audio", error = %e, path = %path.display(), "wav debug: failed to create file");
                None
            }
        }
    }

    fn write_samples(&mut self, samples: &[f32]) {
        for &s in samples {
            let i16_val = (s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
            let _ = self.file.write_all(&i16_val.to_le_bytes());
        }
        self.samples_written += samples.len() as u32;
    }

    fn finalize(mut self) {
        // Seek back to the start and rewrite the header with the
        // correct data size.
        use std::io::Seek;
        if self.file.seek(std::io::SeekFrom::Start(0)).is_ok() {
            let _ = write_wav_header(&mut self.file, self.samples_written);
        }
        info!(
            target: "chanora_audio",
            path = %self.path.display(),
            samples = self.samples_written,
            "wav debug: file closed"
        );
    }
}

fn write_wav_header(file: &mut std::fs::File, num_samples: u32) -> std::io::Result<()> {
    // PCM WAV header: 44 bytes.
    // Channels: 1 (mono), sample rate: 48000, bit depth: 16.
    let channels: u16 = 1;
    let sample_rate: u32 = WAV_SAMPLE_RATE;
    let bits_per_sample: u16 = 16;
    let byte_rate = sample_rate * channels as u32 * bits_per_sample as u32 / 8;
    let block_align = channels * bits_per_sample / 8;
    let data_size = num_samples * channels as u32 * bits_per_sample as u32 / 8;
    let chunk_size = 36 + data_size;

    file.write_all(b"RIFF")?;
    file.write_all(&chunk_size.to_le_bytes())?;
    file.write_all(b"WAVE")?;
    file.write_all(b"fmt ")?;
    file.write_all(&16u32.to_le_bytes())?; // subchunk1 size
    file.write_all(&1u16.to_le_bytes())?; // PCM format
    file.write_all(&channels.to_le_bytes())?;
    file.write_all(&sample_rate.to_le_bytes())?;
    file.write_all(&byte_rate.to_le_bytes())?;
    file.write_all(&block_align.to_le_bytes())?;
    file.write_all(&bits_per_sample.to_le_bytes())?;
    file.write_all(b"data")?;
    file.write_all(&data_size.to_le_bytes())?;
    Ok(())
}

fn wav_writer_task(rx: mpsc::Receiver<DebugFrame>, route: &str, backend: &str, ts: u64) {
    let dir = std::env::temp_dir();
    let prefix = format!("chanora_debug_{route}_{backend}_{ts}");

    let mut raw_mic = WavFile::create(&dir, &format!("{prefix}_raw_mic.wav"));
    let mut render_ref = WavFile::create(&dir, &format!("{prefix}_render_reference.wav"));
    let mut processed = WavFile::create(&dir, &format!("{prefix}_processed_mic.wav"));

    info!(
        target: "chanora_audio",
        dir = %dir.display(),
        prefix = %prefix,
        "wav debug: writer started"
    );

    for frame in rx {
        match frame.stream {
            StreamId::RawMic => {
                if let Some(f) = raw_mic.as_mut() {
                    f.write_samples(&*frame.samples);
                }
            }
            StreamId::RenderReference => {
                if let Some(f) = render_ref.as_mut() {
                    f.write_samples(&*frame.samples);
                }
            }
            StreamId::ProcessedMic => {
                if let Some(f) = processed.as_mut() {
                    f.write_samples(&*frame.samples);
                }
            }
        }
    }

    // Channel closed — finalize all files.
    if let Some(f) = raw_mic {
        f.finalize();
    }
    if let Some(f) = render_ref {
        f.finalize();
    }
    if let Some(f) = processed {
        f.finalize();
    }

    info!(target: "chanora_audio", "wav debug: writer task exited");
}

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

    #[test]
    fn recorder_starts_and_stops_without_panic() {
        let rec =
            WavDebugRecorder::start(AudioRoute::Speaker, AudioBackend::PlatformVoiceProcessing);
        let frame = [0.1_f32; FRAME_10MS_SAMPLES];
        rec.push_raw_mic(&frame);
        rec.push_render_reference(&frame);
        rec.push_processed_mic(&frame);
        rec.stop();
        // Drop the Arc to let the writer task drain.
        drop(rec);
        // Give the writer thread a moment to finish.
        std::thread::sleep(std::time::Duration::from_millis(100));
    }

    #[test]
    fn drop_count_increments_when_channel_full() {
        // Use a tiny channel by creating a recorder and flooding it.
        // We can't easily test the bounded channel directly, but we
        // can verify the drop counter starts at zero.
        let rec = WavDebugRecorder::start(AudioRoute::Speaker, AudioBackend::Noop);
        assert_eq!(rec.drop_count(), 0);
        rec.stop();
    }

    #[test]
    fn wav_header_is_44_bytes() {
        // Write to a temp file to test the header.
        let tmp = std::env::temp_dir().join("chanora_test_wav_header.wav");
        let mut f = std::fs::File::create(&tmp).unwrap();
        write_wav_header(&mut f, 960).unwrap();
        drop(f);
        let data = std::fs::read(&tmp).unwrap();
        assert_eq!(data.len(), 44, "WAV header must be 44 bytes");
        assert_eq!(&data[0..4], b"RIFF");
        assert_eq!(&data[8..12], b"WAVE");
        let _ = std::fs::remove_file(&tmp);
    }
}
