//! Runtime entrypoint for the shared-clock upstream sync probe.

use anyhow::{Context, Result, bail};

use crate::app_support;
use crate::handshake;
use crate::sync_probe::capture::SyncProbeCapture;
use crate::sync_probe::config::{ParseOutcome, ProbeConfig, parse_args_outcome_from, usage};
use crate::sync_probe::schedule::PulseSchedule;
#[cfg(not(coverage))]
use std::fs::File;
#[cfg(not(coverage))]
use std::io::Write;
#[cfg(not(coverage))]
use std::path::PathBuf;

#[cfg(not(coverage))]
use lesavka_common::lesavka::relay_client::RelayClient;
#[cfg(not(coverage))]
use tonic::{Request, transport::Channel};

pub async fn run_sync_probe_from_args<I, S>(args: I) -> Result<()>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    match parse_args_outcome_from(args)? {
        ParseOutcome::Run(config) => run_sync_probe(config).await,
        ParseOutcome::Help => {
            println!("{}", usage());
            Ok(())
        }
    }
}

#[cfg(not(coverage))]
async fn run_sync_probe(config: ProbeConfig) -> Result<()> {
    let caps = handshake::negotiate(config.server.as_str()).await;
    if !caps.camera || !caps.microphone {
        bail!("server does not advertise both camera and microphone support");
    }
    let camera = app_support::camera_config_from_caps(&caps)
        .context("server handshake did not include a complete camera profile")?;

    let schedule = PulseSchedule::new(
        config.warmup,
        config.pulse_period,
        config.pulse_width,
        config.marker_tick_period,
    );

    tracing::info!(
        server = %config.server,
        duration_s = config.duration.as_secs(),
        codec = ?camera.codec,
        width = camera.width,
        height = camera.height,
        fps = camera.fps,
        "🧪 A/V sync probe starting"
    );

    let video_channel = connect(config.server.as_str()).await?;
    let audio_channel = connect(config.server.as_str()).await?;
    let capture = SyncProbeCapture::new(camera, schedule, config.duration)?;
    let video_queue = capture.video_queue();
    let audio_queue = capture.audio_queue();

    let video_task = tokio::spawn(async move {
        let mut client = RelayClient::new(video_channel);
        let outbound = async_stream::stream! {
            loop {
                let next = video_queue.pop_fresh().await;
                if let Some(packet) = next.packet {
                    yield packet;
                    continue;
                }
                break;
            }
        };
        let mut response = client
            .stream_camera(Request::new(outbound))
            .await
            .context("starting sync probe camera stream")?;
        while response.get_mut().message().await.transpose().is_some() {}
        Ok::<(), anyhow::Error>(())
    });

    let audio_task = tokio::spawn(async move {
        let mut client = RelayClient::new(audio_channel);
        let mut audio_dump = open_debug_dump("LESAVKA_SYNC_PROBE_AUDIO_DUMP")
            .context("opening sync probe audio dump")?;
        let mut sent_packets = 0u64;
        let outbound = async_stream::stream! {
            loop {
                let next = audio_queue.pop_fresh().await;
                if let Some(packet) = next.packet {
                    sent_packets = sent_packets.saturating_add(1);
                    if sent_packets <= 5 || sent_packets.is_multiple_of(500) {
                        tracing::info!(
                            packet = sent_packets,
                            pts = packet.pts,
                            bytes = packet.data.len(),
                            "🧪 sync probe microphone packet"
                        );
                    }
                    if let Some(file) = audio_dump.as_mut() {
                        let _ = file.write_all(&packet.data);
                    }
                    yield packet;
                    continue;
                }
                break;
            }
            if let Some(file) = audio_dump.as_mut() {
                let _ = file.flush();
            }
        };
        let mut response = client
            .stream_microphone(Request::new(outbound))
            .await
            .context("starting sync probe microphone stream")?;
        while response.get_mut().message().await.transpose().is_some() {}
        Ok::<(), anyhow::Error>(())
    });

    let (video_result, audio_result) =
        tokio::try_join!(video_task, audio_task).context("joining sync probe streams")?;
    video_result.context("sync probe camera task failed")?;
    audio_result.context("sync probe microphone task failed")?;

    tracing::info!("🧪 A/V sync probe finished");
    Ok(())
}

#[cfg(not(coverage))]
async fn connect(server_addr: &str) -> Result<Channel> {
    Channel::from_shared(server_addr.to_string())
        .context("invalid relay server address")?
        .tcp_nodelay(true)
        .connect()
        .await
        .with_context(|| format!("connecting to relay at {server_addr}"))
}

#[cfg(not(coverage))]
fn open_debug_dump(env_var: &str) -> Result<Option<File>> {
    let Some(path) = std::env::var_os(env_var) else {
        return Ok(None);
    };
    let path = PathBuf::from(path);
    let file = File::create(&path)
        .with_context(|| format!("creating debug dump at {}", path.display()))?;
    Ok(Some(file))
}

#[cfg(coverage)]
async fn run_sync_probe(_config: ProbeConfig) -> Result<()> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::parse_args_outcome_from;
    #[cfg(coverage)]
    use super::run_sync_probe_from_args;
    use crate::sync_probe::config::ParseOutcome;

    #[test]
    fn help_passthrough_stays_stable() {
        let outcome = parse_args_outcome_from(["--help"]).expect("help");
        assert_eq!(outcome, ParseOutcome::Help);
    }

    #[cfg(coverage)]
    #[test]
    fn coverage_run_path_accepts_default_probe_args() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        rt.block_on(async {
            run_sync_probe_from_args(std::iter::empty::<&str>())
                .await
                .expect("coverage run path");
        });
    }

    #[cfg(coverage)]
    #[test]
    fn coverage_run_path_accepts_custom_probe_args() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        rt.block_on(async {
            run_sync_probe_from_args([
                "--server",
                "http://lab:50051",
                "--duration-seconds",
                "2",
                "--warmup-seconds",
                "5",
                "--pulse-period-ms",
                "700",
                "--pulse-width-ms",
                "80",
                "--marker-tick-period",
                "4",
            ])
            .await
            .expect("configured coverage run path");
        });
    }
}
