//! 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;
use crate::sync_probe::timeline::ProbeTimeline;
#[cfg(not(coverage))]
mod bundled_transport;

#[cfg(not(coverage))]
use bundled_transport::run_bundled_probe_stream;
#[cfg(not(coverage))]
use tonic::transport::Channel;

/// Keeps `run_sync_probe_from_args` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
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))]
/// Keeps `run_sync_probe` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
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");
    }
    if !caps.bundled_webcam_media {
        bail!("server does not advertise bundled webcam media; refusing to measure split upstream");
    }
    let mut camera = app_support::camera_config_from_caps(&caps)
        .context("server handshake did not include a complete camera profile")?;
    if let Some(codec) = config.camera_codec {
        camera.codec = codec;
    }
    if let Some(width) = config.camera_width {
        camera.width = width;
    }
    if let Some(height) = config.camera_height {
        camera.height = height;
    }
    if let Some(fps) = config.camera_fps {
        camera.fps = fps;
    }

    let schedule = if config.event_width_codes.is_empty() {
        PulseSchedule::new(
            config.warmup,
            config.pulse_period,
            config.pulse_width,
            config.marker_tick_period,
        )
    } else {
        PulseSchedule::with_event_width_codes(
            config.warmup,
            config.pulse_period,
            config.pulse_width,
            config.marker_tick_period,
            config.event_width_codes.clone(),
        )
    };

    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 bundled_channel = connect(config.server.as_str()).await?;
    let capture = SyncProbeCapture::new(camera, schedule.clone(), config.duration)?;
    let client_start_unix_ns = capture.start_unix_ns();
    let probe_start = capture.probe_start();
    if let Some(path) = &config.timeline_json {
        ProbeTimeline::new(camera, &schedule, config.duration, client_start_unix_ns)
            .write_to(path)
            .with_context(|| format!("writing client sync probe timeline {}", path.display()))?;
    }
    let video_queue = capture.video_queue();
    let audio_queue = capture.audio_queue();

    run_bundled_probe_stream(
        bundled_channel,
        camera,
        video_queue,
        audio_queue,
        probe_start,
    )
    .await?;

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

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

#[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]
    /// Keeps `coverage_run_path_accepts_custom_probe_args` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
    /// Inputs are the typed parameters; output is the return value or side effect.
    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",
                "--timeline-json",
                "/tmp/client-sync-timeline.json",
            ])
            .await
            .expect("configured coverage run path");
        });
    }
}
