//! Bundled upstream sender for the synthetic sync probe.
//!
//! The runner builds the synthetic capture timeline, while this module owns the
//! transport-specific work of pairing fresh audio with each video pulse and
//! streaming `UpstreamMediaBundle` messages to the server.

use anyhow::{Context, Result};
use lesavka_common::{
    audio_transport::{self, AudioTransportProfile, UpstreamAudioCodec},
    lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket, relay_client::RelayClient},
};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::time::Instant;
use tonic::{Request, transport::Channel};

use crate::input::camera::CameraConfig;
use crate::uplink_fresh_queue::FreshPacketQueue;

mod packet_timing;
use packet_timing::*;

const PROBE_BUNDLE_AUDIO_GRACE: std::time::Duration = std::time::Duration::from_millis(30);
const PROBE_BUNDLE_AUDIO_WINDOW_BEFORE_US: u64 = 120_000;
const PROBE_BUNDLE_AUDIO_WINDOW_AFTER_US: u64 = 40_000;
const PROBE_BUNDLE_MAX_AUDIO_PACKETS: usize = 16;
const PROBE_BUNDLE_SESSION_ID: u64 = 1;

/// Stream synthetic paired media through the same bundled RPC as real webcam calls.
///
/// Inputs: gRPC channel, negotiated camera mode, fresh video/audio queues, and
/// the probe monotonic start instant.
/// Outputs: completed RPC stream or an error from connection, send, or task join.
/// Why: the client-to-RCT probe must exercise post-capture transport without
/// measuring split camera/microphone fallback behavior.
pub async fn run_bundled_probe_stream(
    channel: Channel,
    camera: CameraConfig,
    video_queue: FreshPacketQueue<VideoPacket>,
    audio_queue: FreshPacketQueue<AudioPacket>,
    probe_start: Instant,
) -> Result<()> {
    let bundled_task = tokio::spawn(async move {
        let mut client = RelayClient::new(channel);
        let mut audio_dump = open_debug_dump("LESAVKA_SYNC_PROBE_AUDIO_DUMP")
            .context("opening sync probe audio dump")?;
        let mut send_log = open_debug_dump("LESAVKA_SYNC_PROBE_SEND_LOG")
            .context("opening sync probe send log")?;
        let mut audio_transport = ProbeAudioTransport::from_env();
        let outbound = async_stream::stream! {
            let mut pending_audio = Vec::<AudioPacket>::new();
            let mut audio_done = false;
            let mut video_done = false;
            let mut bundle_seq = 0_u64;
            let mut audio_seq = 0_u64;
            let mut video_seq = 0_u64;

            loop {
                if video_done && audio_done {
                    break;
                }

                tokio::select! {
                    next = video_queue.pop_fresh(), if !video_done => {
                        if next.dropped_stale > 0 {
                            tracing::warn!(
                                dropped_stale = next.dropped_stale,
                                queue_depth = next.queue_depth,
                                "🧪 sync probe video queue dropped stale packets"
                            );
                        }
                        if let Some(mut video) = next.packet {
                            stamp_probe_video_packet(
                                &mut video,
                                &mut video_seq,
                                next.queue_depth,
                                camera.fps,
                                probe_start,
                            );
                            retain_probe_audio_for_video(&mut pending_audio, packet_video_capture_pts_us(&video));
                            collect_probe_audio_grace(
                                &audio_queue,
                                &mut pending_audio,
                                &mut audio_done,
                                &mut audio_seq,
                                &mut audio_transport,
                                audio_dump.as_mut(),
                                probe_start,
                            ).await;
                            retain_probe_audio_for_video(&mut pending_audio, packet_video_capture_pts_us(&video));
                            if pending_audio.is_empty() {
                                tracing::warn!(
                                    video_pts = packet_video_capture_pts_us(&video),
                                    "🧪 sync probe skipped video-only bundle while measuring bundled output delay"
                                );
                                continue;
                            }
                            bundle_seq = bundle_seq.saturating_add(1);
                            let audio = std::mem::take(&mut pending_audio);
                            let (capture_start_us, capture_end_us) =
                                probe_bundle_capture_bounds(Some(&video), &audio);
                            write_probe_send_log(
                                send_log.as_mut(),
                                bundle_seq,
                                probe_start,
                                Some(&video),
                                &audio,
                            );
                            yield build_probe_bundle(
                                PROBE_BUNDLE_SESSION_ID,
                                bundle_seq,
                                &camera,
                                Some(video),
                                audio,
                                capture_start_us,
                                capture_end_us,
                            );
                        } else if next.closed {
                            video_done = true;
                        }
                    }
                    next = audio_queue.pop_fresh(), if !audio_done => {
                        if next.dropped_stale > 0 {
                            tracing::warn!(
                                dropped_stale = next.dropped_stale,
                                queue_depth = next.queue_depth,
                                "🧪 sync probe audio queue dropped stale packets"
                            );
                        }
                        if let Some(mut packet) = next.packet {
                            stamp_probe_audio_packet(
                                &mut packet,
                                &mut audio_seq,
                                next.queue_depth,
                                probe_start,
                            );
                            if let Some(packet) = audio_transport.encode_packet(packet) {
                                write_probe_audio_dump(audio_dump.as_mut(), &packet);
                                pending_audio.push(packet);
                            }
                            retain_newest_probe_audio(&mut pending_audio);
                        } else if next.closed {
                            audio_done = true;
                        }
                    }
                }
            }
            if let Some(file) = audio_dump.as_mut() {
                let _ = file.flush();
            }
            if let Some(file) = send_log.as_mut() {
                let _ = file.flush();
            }
        };
        let mut response = client
            .stream_webcam_media(Request::new(outbound))
            .await
            .context("starting bundled sync probe webcam stream")?;
        while response.get_mut().message().await.transpose().is_some() {}
        Ok::<(), anyhow::Error>(())
    });

    bundled_task
        .await
        .context("joining bundled sync probe stream")?
        .context("bundled sync probe task failed")
}

/// Build one outgoing synthetic A/V bundle after local pairing is complete.
///
/// Inputs: bundle identity, negotiated camera profile, paired video/audio, and
/// precomputed capture bounds.
/// Outputs: the exact `UpstreamMediaBundle` yielded to the server RPC.
/// Why: the server's HEVC path depends on video codec metadata, audio format,
/// and capture bounds staying attached to the same physical bundle that leaves
/// the client.
fn build_probe_bundle(
    session_id: u64,
    seq: u64,
    camera: &CameraConfig,
    video: Option<VideoPacket>,
    audio: Vec<AudioPacket>,
    capture_start_us: u64,
    capture_end_us: u64,
) -> UpstreamMediaBundle {
    let profile = audio
        .first()
        .map(audio_transport::packet_audio_profile)
        .unwrap_or_else(AudioTransportProfile::pcm_s16le);
    let mut bundle = UpstreamMediaBundle {
        session_id,
        seq,
        capture_start_us,
        capture_end_us,
        video,
        audio,
        audio_sample_rate: profile.sample_rate,
        audio_channels: profile.channels,
        audio_encoding: profile.encoding as i32,
        video_width: camera.width,
        video_height: camera.height,
        video_fps: camera.fps,
    };
    audio_transport::mark_bundle_audio_profile(&mut bundle, profile);
    bundle
}

/// Drain one short audio grace window when a video packet arrives first.
///
/// Inputs: audio queue, pending audio buffer, sequence state, debug dump, and
/// the probe clock.
/// Outputs: at most one newly stamped audio packet in `pending_audio`.
/// Why: real bundled transport pairs nearby mic and camera captures before
/// sending; the synthetic probe needs to mimic that path closely.
async fn collect_probe_audio_grace(
    audio_queue: &FreshPacketQueue<AudioPacket>,
    pending_audio: &mut Vec<AudioPacket>,
    audio_done: &mut bool,
    audio_seq: &mut u64,
    audio_transport: &mut ProbeAudioTransport,
    audio_dump: Option<&mut File>,
    probe_start: Instant,
) {
    if *audio_done || !pending_audio.is_empty() {
        return;
    }
    let Ok(next) = tokio::time::timeout(PROBE_BUNDLE_AUDIO_GRACE, audio_queue.pop_fresh()).await
    else {
        return;
    };
    if let Some(mut packet) = next.packet {
        stamp_probe_audio_packet(&mut packet, audio_seq, next.queue_depth, probe_start);
        if let Some(packet) = audio_transport.encode_packet(packet) {
            write_probe_audio_dump(audio_dump, &packet);
            pending_audio.push(packet);
        }
        retain_newest_probe_audio(pending_audio);
    } else if next.closed {
        *audio_done = true;
    }
}

/// Runtime audio transport for synthetic probe packets.
///
/// Inputs: `LESAVKA_UPLINK_AUDIO_CODEC` and installed GStreamer plugins.
/// Outputs: packet bytes and metadata matching the selected upstream audio
/// route. Why: the blind client->server->RCT test must exercise the same Opus
/// compression path as the live microphone, not silently measure PCM.
struct ProbeAudioTransport {
    encoder: Option<crate::input::audio_codec::OpusPacketEncoder>,
}

impl ProbeAudioTransport {
    fn from_env() -> Self {
        match crate::input::audio_codec::requested_upstream_audio_codec_from_env() {
            UpstreamAudioCodec::PcmS16le => Self { encoder: None },
            UpstreamAudioCodec::Opus => match crate::input::audio_codec::OpusPacketEncoder::new() {
                Ok(encoder) => {
                    tracing::info!("🧪 sync probe Opus audio transport enabled");
                    Self {
                        encoder: Some(encoder),
                    }
                }
                Err(err) => {
                    tracing::warn!(
                        "🧪⚠️ sync probe Opus requested but unavailable ({err:#}); falling back to PCM"
                    );
                    Self { encoder: None }
                }
            },
        }
    }

    fn encode_packet(&mut self, mut packet: AudioPacket) -> Option<AudioPacket> {
        audio_transport::mark_packet_pcm_s16le(&mut packet);
        let Some(encoder) = self.encoder.as_mut() else {
            return Some(packet);
        };
        match encoder.encode_packet(packet.clone()) {
            Ok(Some(encoded)) => Some(encoded),
            Ok(None) => None,
            Err(err) => {
                tracing::warn!(
                    "🧪⚠️ sync probe Opus encode failed; sending PCM fallback for this packet: {err:#}"
                );
                Some(packet)
            }
        }
    }
}

/// Stamp one synthetic audio packet with client-side transport telemetry.
///
/// Inputs: mutable packet, sequence counter, queue depth, and probe clock.
/// Outputs: packet sidecar fields used by server freshness telemetry.
/// Why: server diagnostics compare capture, send, receive, and sink timing, so
/// synthetic packets need the same metadata as physical capture packets.
fn stamp_probe_audio_packet(
    packet: &mut AudioPacket,
    seq: &mut u64,
    queue_depth: usize,
    probe_start: Instant,
) {
    *seq = seq.saturating_add(1);
    let capture_pts_us = packet.pts;
    let send_pts_us = probe_elapsed_us(probe_start).max(capture_pts_us);
    packet.seq = *seq;
    packet.client_capture_pts_us = capture_pts_us;
    packet.client_send_pts_us = send_pts_us;
    packet.client_queue_depth = queue_depth.try_into().unwrap_or(u32::MAX);
    packet.client_queue_age_ms = packet_age_ms(capture_pts_us, send_pts_us);
}

/// Stamp one synthetic video packet with client-side transport telemetry.
///
/// Inputs: mutable packet, sequence counter, queue depth, negotiated FPS, and
/// probe clock.
/// Outputs: packet sidecar fields used by server freshness telemetry.
/// Why: video freshness must be measured against generated capture PTS, not a
/// later wall-clock moment after MJPEG encoding or gRPC buffering.
fn stamp_probe_video_packet(
    packet: &mut VideoPacket,
    seq: &mut u64,
    queue_depth: usize,
    fps: u32,
    probe_start: Instant,
) {
    *seq = seq.saturating_add(1);
    let capture_pts_us = packet.pts;
    let send_pts_us = probe_elapsed_us(probe_start).max(capture_pts_us);
    packet.seq = *seq;
    packet.effective_fps = fps;
    packet.client_capture_pts_us = capture_pts_us;
    packet.client_send_pts_us = send_pts_us;
    packet.client_queue_depth = queue_depth.try_into().unwrap_or(u32::MAX);
    packet.client_queue_age_ms = packet_age_ms(capture_pts_us, send_pts_us);
}

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