//! Client-side upstream audio codec helpers.
//!
//! Why: the capture path stays raw PCM so monitor/playback behavior remains
//! predictable, while this module optionally compresses packets just before
//! they enter the client-to-server transport bundle.

use anyhow::{Context, Result, bail};
use gst::prelude::*;
use gstreamer as gst;
use gstreamer_app as gst_app;
use lesavka_common::{
    audio_transport::{
        self, AudioTransportProfile, UpstreamAudioCodec, parse_upstream_audio_codec,
    },
    lesavka::AudioPacket,
};
use std::collections::VecDeque;
use std::time::Duration;

const AUDIO_CODEC_ENV: &str = "LESAVKA_UPLINK_AUDIO_CODEC";
const AUDIO_CODEC_LEGACY_ENV: &str = "LESAVKA_AUDIO_CODEC";
const OPUS_PULL_TIMEOUT: Duration = Duration::from_millis(25);
const MAX_PENDING_OPUS_METADATA: usize = 16;

/// Resolve the requested upstream audio codec from runtime environment.
///
/// Inputs: `LESAVKA_UPLINK_AUDIO_CODEC` or legacy `LESAVKA_AUDIO_CODEC`.
/// Output: PCM by default, Opus when explicitly requested. Why: Opus is still
/// under active calibration; the launcher must boot into the known-good route.
#[must_use]
pub fn requested_upstream_audio_codec_from_env() -> UpstreamAudioCodec {
    std::env::var(AUDIO_CODEC_ENV)
        .ok()
        .or_else(|| std::env::var(AUDIO_CODEC_LEGACY_ENV).ok())
        .as_deref()
        .and_then(parse_upstream_audio_codec)
        .unwrap_or(UpstreamAudioCodec::PcmS16le)
}

/// Low-latency Opus packet encoder for already-framed 48 kHz stereo PCM.
pub struct OpusPacketEncoder {
    _pipeline: gst::Pipeline,
    appsrc: gst_app::AppSrc,
    appsink: gst_app::AppSink,
    pending_packets: VecDeque<AudioPacket>,
}

impl OpusPacketEncoder {
    /// Build the GStreamer Opus encoder used by live upstream transport.
    ///
    /// Inputs: installed GStreamer plugins. Output: a running appsrc/appsink
    /// encoder. Why: keeping the encoder persistent avoids per-packet pipeline
    /// startup latency and lets PCM remain the fallback when plugins are absent.
    pub fn new() -> Result<Self> {
        gst::init().context("gst init")?;
        if gst::ElementFactory::find("opusenc").is_none() {
            bail!("GStreamer opusenc plugin is not available");
        }

        let desc = "\
            appsrc name=src is-live=true block=false format=time \
                caps=audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
            queue max-size-buffers=4 max-size-time=80000000 leaky=downstream ! \
            opusenc audio-type=restricted-lowdelay bitrate=96000 bitrate-type=cbr complexity=7 frame-size=20 perfect-timestamp=true hard-resync=true ! \
            appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
        let pipeline: gst::Pipeline = gst::parse::launch(desc)?.downcast().expect("pipeline");
        let appsrc = pipeline
            .by_name("src")
            .context("missing opus encoder appsrc")?
            .downcast::<gst_app::AppSrc>()
            .expect("opus encoder appsrc");
        let appsink = pipeline
            .by_name("sink")
            .context("missing opus encoder appsink")?
            .downcast::<gst_app::AppSink>()
            .expect("opus encoder appsink");
        pipeline
            .set_state(gst::State::Playing)
            .context("start opus encoder pipeline")?;

        Ok(Self {
            _pipeline: pipeline,
            appsrc,
            appsink,
            pending_packets: VecDeque::new(),
        })
    }

    /// Encode one PCM packet, preserving capture timing on the returned packet.
    pub fn encode_packet(&mut self, packet: AudioPacket) -> Result<Option<AudioPacket>> {
        if !audio_transport::packet_is_raw_pcm_s16le(&packet) {
            return Ok(Some(packet));
        }

        push_pending_packet(&mut self.pending_packets, packet_metadata(&packet));
        let mut buffer = gst::Buffer::from_slice(packet.data.clone());
        if let Some(meta) = buffer.get_mut() {
            let pts = gst::ClockTime::from_useconds(packet.pts);
            meta.set_pts(Some(pts));
            meta.set_dts(Some(pts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(u64::from(
                packet.frame_duration_us.max(1),
            ))));
        }
        self.appsrc
            .push_buffer(buffer)
            .context("push pcm into opus encoder")?;

        let Some(sample) = self.appsink.try_pull_sample(gst::ClockTime::from_nseconds(
            OPUS_PULL_TIMEOUT.as_nanos().min(u128::from(u64::MAX)) as u64,
        )) else {
            return Ok(None);
        };
        let sample_buffer = sample
            .buffer()
            .context("opus encoder sample missing buffer")?;
        let sample_pts_us = sample_buffer.pts().map(|pts| pts.nseconds() / 1_000);
        let sample_duration_us = sample_buffer
            .duration()
            .map(|duration| duration.nseconds() / 1_000);
        let encoded = sample_buffer
            .map_readable()
            .context("map opus packet")?
            .to_vec();
        if encoded.is_empty() {
            return Ok(None);
        }

        let mut output = self
            .take_pending_packet(sample_pts_us)
            .unwrap_or_else(|| packet_metadata(&packet));
        output.data = encoded;
        if let Some(pts_us) = sample_pts_us {
            output.pts = pts_us;
        }
        if let Some(duration_us) = sample_duration_us {
            output.frame_duration_us = duration_us.min(u64::from(u32::MAX)) as u32;
        }
        audio_transport::mark_packet_opus(&mut output);
        Ok(Some(output))
    }

    /// Return the compressed profile this encoder produces.
    #[must_use]
    pub const fn profile(&self) -> AudioTransportProfile {
        AudioTransportProfile::opus_voice()
    }

    fn take_pending_packet(&mut self, sample_pts_us: Option<u64>) -> Option<AudioPacket> {
        if let Some(pts_us) = sample_pts_us
            && let Some(index) = self
                .pending_packets
                .iter()
                .position(|packet| packet.pts == pts_us)
        {
            return self.pending_packets.drain(..=index).next_back();
        }
        self.pending_packets.pop_front()
    }
}

fn packet_metadata(packet: &AudioPacket) -> AudioPacket {
    let mut metadata = packet.clone();
    metadata.data.clear();
    metadata
}

fn push_pending_packet(pending: &mut VecDeque<AudioPacket>, packet: AudioPacket) {
    pending.push_back(packet);
    while pending.len() > MAX_PENDING_OPUS_METADATA {
        pending.pop_front();
    }
}

impl Drop for OpusPacketEncoder {
    fn drop(&mut self) {
        let _ = self._pipeline.set_state(gst::State::Null);
    }
}

#[cfg(test)]
#[path = "audio_codec/tests/mod.rs"]
mod tests;
