//! Server-side per-peer call recording: Opus frames (as depayloaded by str0m)
//! muxed into an Ogg container, playable by browsers/ffmpeg without transcoding.

use std::fs::File;
use std::io::Error;
use std::path::Path;

use ogg::{PacketWriteEndInfo, PacketWriter};

const SAMPLE_RATE: f64 = 48_000.0;
/// Standard Opus pre-skip (3840 samples = 80ms at 48kHz).
const PRE_SKIP: u16 = 3840;

pub struct OggOpusWriter {
    writer: PacketWriter<'static, File>,
    serial: u32,
    /// RTP media time (seconds) of the first frame — granule positions are
    /// computed relative to it, so pauses (DTX) keep the timeline correct.
    base_time: Option<f64>,
    granule: u64,
    finished: bool,
}

impl OggOpusWriter {
    pub fn create(path: &Path, channels: u8) -> Result<Self, Error> {
        if let Some(dir) = path.parent() {
            std::fs::create_dir_all(dir)?;
        }

        let mut writer = PacketWriter::new(File::create(path)?);
        // Stream serial only needs to be unique within the file; one stream per file.
        let serial: u32 = 0x626c_6178; // "blax"

        writer.write_packet(opus_head(channels), serial, PacketWriteEndInfo::EndPage, 0)?;
        writer.write_packet(opus_tags(), serial, PacketWriteEndInfo::EndPage, 0)?;

        Ok(OggOpusWriter {
            writer,
            serial,
            base_time: None,
            granule: 0,
            finished: false,
        })
    }

    /// Append one Opus frame. `time_seconds` is the frame's RTP media time
    /// (`MediaData::time.as_seconds()`).
    pub fn write_frame(&mut self, frame: &[u8], time_seconds: f64) -> Result<(), Error> {
        if self.finished {
            return Ok(());
        }

        let base = *self.base_time.get_or_insert(time_seconds);
        let elapsed = (time_seconds - base).max(0.0);
        // Granule = absolute decoded sample position at 48kHz. One 20ms frame = 960
        // samples; deriving from RTP time keeps sync even with DTX/packet loss.
        let granule = (elapsed * SAMPLE_RATE).round() as u64 + 960;
        self.granule = granule.max(self.granule);

        self.writer.write_packet(
            frame.to_vec(),
            self.serial,
            PacketWriteEndInfo::NormalPacket,
            self.granule,
        )
    }

    /// Close the logical Ogg stream (writes the end-of-stream page).
    pub fn finish(&mut self) -> Result<(), Error> {
        if self.finished {
            return Ok(());
        }
        self.finished = true;
        self.writer.write_packet(
            Vec::new(),
            self.serial,
            PacketWriteEndInfo::EndStream,
            self.granule,
        )
    }
}

impl Drop for OggOpusWriter {
    fn drop(&mut self) {
        let _ = self.finish();
    }
}

/// <https://datatracker.ietf.org/doc/html/rfc7845#section-5.1>
fn opus_head(channels: u8) -> Vec<u8> {
    let mut head = Vec::with_capacity(19);
    head.extend_from_slice(b"OpusHead");
    head.push(1); // version
    head.push(channels);
    head.extend_from_slice(&PRE_SKIP.to_le_bytes());
    head.extend_from_slice(&48_000u32.to_le_bytes()); // input sample rate
    head.extend_from_slice(&0i16.to_le_bytes()); // output gain
    head.push(0); // channel mapping family
    head
}

fn opus_tags() -> Vec<u8> {
    let vendor = b"blax-webrtc-sfu";
    let mut tags = Vec::new();
    tags.extend_from_slice(b"OpusTags");
    tags.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
    tags.extend_from_slice(vendor);
    tags.extend_from_slice(&0u32.to_le_bytes()); // user comment count
    tags
}

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

    #[test]
    fn writes_a_playable_ogg_opus_skeleton() {
        let dir = std::env::temp_dir().join("blax-webrtc-sfu-test");
        let path = dir.join("rec.ogg");
        let _ = std::fs::remove_file(&path);

        let mut w = OggOpusWriter::create(&path, 1).unwrap();
        // Two fake 20ms frames.
        w.write_frame(&[0xFC, 0xFF, 0xFE], 10.0).unwrap();
        w.write_frame(&[0xFC, 0xFF, 0xFE], 10.02).unwrap();
        w.finish().unwrap();

        let bytes = std::fs::read(&path).unwrap();
        assert_eq!(&bytes[..4], b"OggS", "ogg capture pattern");
        let head = bytes
            .windows(8)
            .position(|win| win == b"OpusHead")
            .expect("OpusHead present");
        let tags = bytes
            .windows(8)
            .position(|win| win == b"OpusTags")
            .expect("OpusTags present");
        assert!(head < tags);
    }

    #[test]
    fn granule_tracks_rtp_time() {
        let dir = std::env::temp_dir().join("blax-webrtc-sfu-test");
        let mut w = OggOpusWriter::create(&dir.join("granule.ogg"), 1).unwrap();
        w.write_frame(&[0], 5.0).unwrap();
        assert_eq!(w.granule, 960);
        w.write_frame(&[0], 6.0).unwrap(); // 1s gap (DTX) => 48000 samples later
        assert_eq!(w.granule, 48_960);
    }
}
