//! Pure-Rust mp3 → OGG/Opus transcode.
//!
//! Bit-exact port of `nexo_microapp_sdk::voice::tts`'s
//! `transcode_mp3_to_opus_ogg`: pure Rust via `symphonia` (mp3
//! decode) + `opus-wave` (encode) + `ogg` (mux). No system ffmpeg
//! subprocess. The output is the wire format WhatsApp PTT renders
//! as a voice-note bubble.

use std::borrow::Cow;
use std::io::Cursor;

use crate::error::{Result, VoiceError};

/// MIME canonical for the muxed output. Both the container AND
/// the codec hint are required — iOS WhatsApp rejects bare
/// `audio/ogg` for PTT.
pub const OUTPUT_MIME: &str = "audio/ogg; codecs=opus";

/// Async wrapper: spawn the blocking decoder/encoder on a worker
/// thread so the caller's tokio runtime doesn't stall.
pub async fn transcode_mp3_to_opus_ogg(mp3: &[u8]) -> Result<Vec<u8>> {
    let mp3_owned = mp3.to_vec();
    tokio::task::spawn_blocking(move || transcode_mp3_to_opus_ogg_blocking(&mp3_owned))
        .await
        .map_err(|e| VoiceError::Transcode(format!("transcode join: {e}")))?
}

/// Synchronous core. Lives here separately so callers driving from
/// inside an existing `spawn_blocking` don't double-jump.
pub fn transcode_mp3_to_opus_ogg_blocking(mp3: &[u8]) -> Result<Vec<u8>> {
    use ogg::PacketWriteEndInfo;
    use opus_wave::{Application, Channels, OpusEncoder, SampleRate};
    use symphonia::core::audio::SampleBuffer;
    use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
    use symphonia::core::errors::Error as SymError;
    use symphonia::core::formats::FormatOptions;
    use symphonia::core::io::MediaSourceStream;
    use symphonia::core::meta::MetadataOptions;
    use symphonia::core::probe::Hint;

    if mp3.is_empty() {
        return Err(VoiceError::Transcode("mp3 input is empty".into()));
    }

    // ── Stage 1: decode mp3 → f32 mono PCM at source rate. ──
    let cursor = Cursor::new(mp3.to_vec());
    let mss = MediaSourceStream::new(Box::new(cursor), Default::default());
    let mut hint = Hint::new();
    hint.with_extension("mp3");
    let probed = symphonia::default::get_probe()
        .format(
            &hint,
            mss,
            &FormatOptions::default(),
            &MetadataOptions::default(),
        )
        .map_err(|e| VoiceError::Transcode(format!("probe mp3: {e}")))?;

    let mut format = probed.format;
    let track = format
        .tracks()
        .iter()
        .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
        .ok_or_else(|| VoiceError::Transcode("no audio track in mp3".into()))?;
    let track_id = track.id;
    let source_rate = track
        .codec_params
        .sample_rate
        .ok_or_else(|| VoiceError::Transcode("mp3 has no sample rate".into()))?;

    let mut decoder = symphonia::default::get_codecs()
        .make(&track.codec_params, &DecoderOptions::default())
        .map_err(|e| VoiceError::Transcode(format!("mp3 decoder init: {e}")))?;

    let mut pcm_at_source: Vec<f32> = Vec::new();
    let mut sample_buf: Option<SampleBuffer<f32>> = None;
    let mut input_channels: usize = 1;
    loop {
        let packet = match format.next_packet() {
            Ok(p) => p,
            Err(SymError::IoError(ref e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
                break;
            }
            Err(SymError::ResetRequired) => break,
            Err(e) => return Err(VoiceError::Transcode(format!("mp3 packet: {e}"))),
        };
        if packet.track_id() != track_id {
            continue;
        }
        let decoded = match decoder.decode(&packet) {
            Ok(d) => d,
            Err(SymError::DecodeError(_)) => continue,
            Err(e) => return Err(VoiceError::Transcode(format!("mp3 decode: {e}"))),
        };
        if sample_buf.is_none() {
            let spec = *decoded.spec();
            input_channels = spec.channels.count().max(1);
            sample_buf = Some(SampleBuffer::<f32>::new(decoded.capacity() as u64, spec));
        }
        let sb = sample_buf.as_mut().unwrap();
        sb.copy_interleaved_ref(decoded);
        let interleaved = sb.samples();
        if input_channels == 1 {
            pcm_at_source.extend_from_slice(interleaved);
        } else {
            for chunk in interleaved.chunks(input_channels) {
                let avg: f32 = chunk.iter().sum::<f32>() / input_channels as f32;
                pcm_at_source.push(avg);
            }
        }
    }

    if pcm_at_source.is_empty() {
        return Err(VoiceError::Transcode("mp3 decoded to empty PCM".into()));
    }

    // Linear resample to 24 kHz if Edge ever ships a different
    // rate. Edge's default mp3 profile is 24 kHz so this is
    // usually a no-op.
    const TARGET_RATE: u32 = 24_000;
    let pcm_24k = if source_rate == TARGET_RATE {
        pcm_at_source
    } else {
        resample_linear(&pcm_at_source, source_rate, TARGET_RATE)
    };

    // ── Stage 2: configure opus encoder. ──
    let mut encoder = OpusEncoder::new(SampleRate::Hz24000, Channels::Mono, Application::Voip)
        .map_err(|e| VoiceError::Transcode(format!("opus encoder init: {e:?}")))?;
    const FRAME_MS: u32 = 20;
    let frame_size_per_channel: i32 = (TARGET_RATE * FRAME_MS / 1000) as i32;
    let frame_size_usize = frame_size_per_channel as usize;

    // ── Stage 3: ogg muxer with OpusHead + OpusTags + audio. ──
    let mut output = Vec::<u8>::new();
    let serial: u32 = 0xCA5C_ADE0;
    {
        let mut writer = ogg::PacketWriter::new(Cursor::new(&mut output));

        // OpusHead — RFC 7845 § 5.1 (19 bytes, mono).
        let mut head = Vec::with_capacity(19);
        head.extend_from_slice(b"OpusHead");
        head.push(1); // version
        head.push(1); // output channel count
        head.extend_from_slice(&312u16.to_le_bytes()); // pre-skip
        head.extend_from_slice(&TARGET_RATE.to_le_bytes());
        head.extend_from_slice(&0i16.to_le_bytes());
        head.push(0); // channel mapping family

        writer
            .write_packet(
                Cow::<[u8]>::Owned(head),
                serial,
                PacketWriteEndInfo::EndPage,
                0,
            )
            .map_err(|e| VoiceError::Transcode(format!("ogg OpusHead: {e}")))?;

        // OpusTags — RFC 7845 § 5.2.
        let vendor = b"nexo-plugin-voice";
        let mut tags = Vec::with_capacity(8 + 4 + vendor.len() + 4);
        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());
        writer
            .write_packet(
                Cow::<[u8]>::Owned(tags),
                serial,
                PacketWriteEndInfo::EndPage,
                0,
            )
            .map_err(|e| VoiceError::Transcode(format!("ogg OpusTags: {e}")))?;

        // Audio packets.
        let total_frames = pcm_24k.len() / frame_size_usize;
        if total_frames == 0 {
            return Err(VoiceError::Transcode(
                "mp3 too short to encode a single 20 ms opus frame".into(),
            ));
        }
        let mut packet_buf = vec![0u8; 4000];
        let packet_buf_cap: i32 = packet_buf.len() as i32;
        let mut granule_48k: u64 = 0;
        for i in 0..total_frames {
            let start = i * frame_size_usize;
            let frame = &pcm_24k[start..start + frame_size_usize];
            let len = encoder
                .encode_float(frame, frame_size_per_channel, &mut packet_buf, packet_buf_cap)
                .map_err(|e| VoiceError::Transcode(format!("opus encode: {e:?}")))?;
            if len <= 0 {
                continue;
            }
            granule_48k += (frame_size_usize as u64) * 2;
            let info = if i + 1 == total_frames {
                PacketWriteEndInfo::EndStream
            } else {
                PacketWriteEndInfo::NormalPacket
            };
            let bytes = packet_buf[..len as usize].to_vec();
            writer
                .write_packet(Cow::<[u8]>::Owned(bytes), serial, info, granule_48k)
                .map_err(|e| VoiceError::Transcode(format!("ogg audio: {e}")))?;
        }
    }

    if output.is_empty() {
        return Err(VoiceError::Transcode("produced 0 bytes".into()));
    }
    Ok(output)
}

/// Linear interpolation resampler. Quality fine for voice-note use
/// case; whisper-grade resampling is overkill for a phone speaker.
fn resample_linear(input: &[f32], from_hz: u32, to_hz: u32) -> Vec<f32> {
    if from_hz == to_hz || input.is_empty() {
        return input.to_vec();
    }
    let ratio = from_hz as f64 / to_hz as f64;
    let out_len = ((input.len() as f64) / ratio).floor() as usize;
    let mut out = Vec::with_capacity(out_len);
    let last_idx = input.len() - 1;
    for i in 0..out_len {
        let src = i as f64 * ratio;
        let i0 = src.floor() as usize;
        let i1 = (i0 + 1).min(last_idx);
        let frac = (src - i0 as f64) as f32;
        let s0 = input[i0];
        let s1 = input[i1];
        out.push(s0 + (s1 - s0) * frac);
    }
    out
}

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

    #[test]
    fn empty_mp3_rejects() {
        let err = transcode_mp3_to_opus_ogg_blocking(&[]).unwrap_err();
        assert!(matches!(err, VoiceError::Transcode(_)));
    }

    #[test]
    fn garbage_mp3_rejects() {
        let err =
            transcode_mp3_to_opus_ogg_blocking(b"not an mp3 file just plain text").unwrap_err();
        assert!(matches!(err, VoiceError::Transcode(_)));
    }

    #[test]
    fn resample_no_op_when_rates_match() {
        let pcm = vec![0.1, 0.2, 0.3, 0.4];
        let r = resample_linear(&pcm, 24_000, 24_000);
        assert_eq!(r, pcm);
    }

    #[test]
    fn resample_24_to_48_doubles_length() {
        let pcm = vec![0.0, 1.0, 0.0, -1.0];
        let r = resample_linear(&pcm, 24_000, 48_000);
        assert!((r.len() as i64 - 8).abs() <= 1);
    }

    #[test]
    fn output_mime_is_whatsapp_ptt_compatible() {
        assert_eq!(OUTPUT_MIME, "audio/ogg; codecs=opus");
    }
}
