//! CLI parsing for the upstream A/V sync probe.

use anyhow::{Context, Result, bail};
use std::path::PathBuf;
use std::time::Duration;

use crate::app_support::DEFAULT_SERVER_ADDR;
use crate::input::camera::CameraCodec;
use crate::sync_probe::signature::first_unsupported_event_code;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProbeConfig {
    pub server: String,
    pub duration: Duration,
    pub warmup: Duration,
    pub pulse_period: Duration,
    pub pulse_width: Duration,
    pub marker_tick_period: u32,
    pub event_width_codes: Vec<u32>,
    pub timeline_json: Option<PathBuf>,
    pub camera_codec: Option<CameraCodec>,
    pub camera_width: Option<u32>,
    pub camera_height: Option<u32>,
    pub camera_fps: Option<u32>,
}

#[derive(Debug, Eq, PartialEq)]
pub enum ParseOutcome {
    Run(ProbeConfig),
    Help,
}

pub fn usage() -> &'static str {
    "Usage: lesavka-sync-probe [--server http://HOST:50051] [--duration-seconds 10] [--warmup-seconds 4] [--pulse-period-ms 1000] [--pulse-width-ms 120] [--marker-tick-period 5] [--event-width-codes 1,2,3] [--timeline-json PATH] [--camera-codec hevc|mjpeg] [--camera-mode WIDTHxHEIGHT@FPS]"
}

pub fn parse_args_outcome_from<I, S>(args: I) -> Result<ParseOutcome>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    let mut args = args.into_iter().map(Into::into);
    let mut server = DEFAULT_SERVER_ADDR.to_string();
    let mut duration_seconds = 10u64;
    let mut warmup_seconds = 4u64;
    let mut pulse_period_ms = 1_000u64;
    let mut pulse_width_ms = 120u64;
    let mut marker_tick_period = 5u32;
    let mut event_width_codes = Vec::<u32>::new();
    let mut timeline_json = None::<PathBuf>;
    let mut camera_codec = None::<CameraCodec>;
    let mut camera_width = None::<u32>;
    let mut camera_height = None::<u32>;
    let mut camera_fps = None::<u32>;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--server" => {
                server = args
                    .next()
                    .context("missing value after --server")?
                    .trim()
                    .to_string();
            }
            "--duration-seconds" => {
                duration_seconds = parse_u64_arg(
                    args.next(),
                    "--duration-seconds",
                    "duration must be at least one second",
                )?;
                if duration_seconds == 0 {
                    bail!("duration must be at least one second\n{}", usage());
                }
            }
            "--warmup-seconds" => {
                warmup_seconds = parse_u64_arg(
                    args.next(),
                    "--warmup-seconds",
                    "warmup must stay non-negative",
                )?;
            }
            "--pulse-period-ms" => {
                pulse_period_ms = parse_u64_arg(
                    args.next(),
                    "--pulse-period-ms",
                    "pulse period must be positive",
                )?;
                if pulse_period_ms == 0 {
                    bail!("pulse period must be positive\n{}", usage());
                }
            }
            "--pulse-width-ms" => {
                pulse_width_ms = parse_u64_arg(
                    args.next(),
                    "--pulse-width-ms",
                    "pulse width must be positive",
                )?;
                if pulse_width_ms == 0 {
                    bail!("pulse width must be positive\n{}", usage());
                }
            }
            "--marker-tick-period" => {
                marker_tick_period = parse_u32_arg(
                    args.next(),
                    "--marker-tick-period",
                    "marker tick period must be positive",
                )?;
                if marker_tick_period == 0 {
                    bail!("marker tick period must be positive\n{}", usage());
                }
            }
            "--event-width-codes" => {
                event_width_codes = parse_event_width_codes(args.next())?;
            }
            "--timeline-json" => {
                let path = args
                    .next()
                    .context("missing value after --timeline-json")?
                    .trim()
                    .to_string();
                if path.is_empty() {
                    bail!("timeline JSON path must not be empty\n{}", usage());
                }
                timeline_json = Some(PathBuf::from(path));
            }
            "--camera-codec" => {
                let raw = args.next().context("missing value after --camera-codec")?;
                camera_codec = crate::app_support::parse_camera_codec(&raw)
                    .with_context(|| format!("unsupported camera codec `{raw}`\n{}", usage()))
                    .map(Some)?;
            }
            "--camera-mode" => {
                let (width, height, fps) = parse_camera_mode(args.next())?;
                camera_width = Some(width);
                camera_height = Some(height);
                camera_fps = Some(fps);
            }
            "--camera-width" => {
                camera_width = Some(parse_u32_arg(
                    args.next(),
                    "--camera-width",
                    "camera width must be positive",
                )?);
                if camera_width == Some(0) {
                    bail!("camera width must be positive\n{}", usage());
                }
            }
            "--camera-height" => {
                camera_height = Some(parse_u32_arg(
                    args.next(),
                    "--camera-height",
                    "camera height must be positive",
                )?);
                if camera_height == Some(0) {
                    bail!("camera height must be positive\n{}", usage());
                }
            }
            "--camera-fps" => {
                camera_fps = Some(parse_u32_arg(
                    args.next(),
                    "--camera-fps",
                    "camera fps must be positive",
                )?);
                if camera_fps == Some(0) {
                    bail!("camera fps must be positive\n{}", usage());
                }
            }
            "--help" | "-h" => return Ok(ParseOutcome::Help),
            _ => bail!("unexpected argument `{arg}`\n{}", usage()),
        }
    }

    if pulse_width_ms >= pulse_period_ms {
        bail!(
            "pulse width must stay smaller than the pulse period\n{}",
            usage()
        );
    }
    Ok(ParseOutcome::Run(ProbeConfig {
        server,
        duration: Duration::from_secs(duration_seconds),
        warmup: Duration::from_secs(warmup_seconds),
        pulse_period: Duration::from_millis(pulse_period_ms),
        pulse_width: Duration::from_millis(pulse_width_ms),
        marker_tick_period,
        event_width_codes,
        timeline_json,
        camera_codec,
        camera_width,
        camera_height,
        camera_fps,
    }))
}

fn parse_u64_arg(value: Option<String>, flag: &str, context: &str) -> Result<u64> {
    value
        .context(format!("missing value after {flag}"))?
        .trim()
        .parse::<u64>()
        .with_context(|| format!("{context}\n{}", usage()))
}

fn parse_u32_arg(value: Option<String>, flag: &str, context: &str) -> Result<u32> {
    value
        .context(format!("missing value after {flag}"))?
        .trim()
        .parse::<u32>()
        .with_context(|| format!("{context}\n{}", usage()))
}

fn parse_camera_mode(value: Option<String>) -> Result<(u32, u32, u32)> {
    let raw = value
        .context("missing value after --camera-mode")?
        .trim()
        .to_string();
    let Some((width, rest)) = raw.split_once('x') else {
        bail!("camera mode must look like WIDTHxHEIGHT@FPS\n{}", usage());
    };
    let Some((height, fps)) = rest.split_once('@') else {
        bail!("camera mode must look like WIDTHxHEIGHT@FPS\n{}", usage());
    };
    let width = width
        .parse::<u32>()
        .with_context(|| format!("camera mode width must be numeric\n{}", usage()))?;
    let height = height
        .parse::<u32>()
        .with_context(|| format!("camera mode height must be numeric\n{}", usage()))?;
    let fps = fps
        .parse::<u32>()
        .with_context(|| format!("camera mode fps must be numeric\n{}", usage()))?;
    if width == 0 || height == 0 || fps == 0 {
        bail!(
            "camera mode dimensions and fps must be positive\n{}",
            usage()
        );
    }
    Ok((width, height, fps))
}

/// Parse the event identity sequence for coded synthetic probes.
///
/// Inputs: optional raw CLI value after `--event-width-codes`.
/// Outputs: validated one-based event code vector.
/// Why: invalid event signatures should fail before transport starts, otherwise
/// the final RCT capture would look like missing media rather than bad config.
fn parse_event_width_codes(value: Option<String>) -> Result<Vec<u32>> {
    let raw = value
        .context("missing value after --event-width-codes")?
        .trim()
        .to_string();
    let codes = raw
        .split(',')
        .filter_map(|part| {
            let trimmed = part.trim();
            (!trimmed.is_empty()).then_some(trimmed)
        })
        .map(|part| {
            let code = part
                .parse::<u32>()
                .with_context(|| format!("parsing event width code `{part}`"))?;
            if code == 0 {
                bail!("event width codes must be positive\n{}", usage());
            }
            Ok(code)
        })
        .collect::<Result<Vec<_>>>()?;
    if codes.is_empty() {
        bail!("event width code list must not be empty\n{}", usage());
    }
    if let Some(code) = first_unsupported_event_code(&codes) {
        bail!(
            "event width code {code} has no probe signature\n{}",
            usage()
        );
    }
    Ok(codes)
}

#[cfg(test)]
mod tests {
    use super::{DEFAULT_SERVER_ADDR, ParseOutcome, parse_args_outcome_from};
    use std::time::Duration;

    #[test]
    fn parse_args_uses_operational_defaults() {
        let outcome = parse_args_outcome_from(std::iter::empty::<&str>()).expect("defaults");
        let ParseOutcome::Run(config) = outcome else {
            panic!("expected run config");
        };

        assert_eq!(config.server, DEFAULT_SERVER_ADDR);
        assert_eq!(config.duration, Duration::from_secs(10));
        assert_eq!(config.warmup, Duration::from_secs(4));
        assert_eq!(config.pulse_period, Duration::from_millis(1_000));
        assert_eq!(config.pulse_width, Duration::from_millis(120));
        assert_eq!(config.marker_tick_period, 5);
        assert!(config.event_width_codes.is_empty());
        assert_eq!(config.timeline_json, None);
        assert_eq!(config.camera_codec, None);
        assert_eq!(config.camera_width, None);
        assert_eq!(config.camera_height, None);
        assert_eq!(config.camera_fps, None);
    }

    #[test]
    fn parse_args_accepts_explicit_probe_settings() {
        let outcome = parse_args_outcome_from([
            "--server",
            "http://lab:50051",
            "--duration-seconds",
            "14",
            "--warmup-seconds",
            "6",
            "--pulse-period-ms",
            "750",
            "--pulse-width-ms",
            "90",
            "--marker-tick-period",
            "3",
            "--event-width-codes",
            "1,2,3",
            "--timeline-json",
            "/tmp/client-timeline.json",
            "--camera-codec",
            "hevc",
            "--camera-mode",
            "1920x1080@30",
        ])
        .expect("configured run");
        let ParseOutcome::Run(config) = outcome else {
            panic!("expected run config");
        };

        assert_eq!(config.server, "http://lab:50051");
        assert_eq!(config.duration, Duration::from_secs(14));
        assert_eq!(config.warmup, Duration::from_secs(6));
        assert_eq!(config.pulse_period, Duration::from_millis(750));
        assert_eq!(config.pulse_width, Duration::from_millis(90));
        assert_eq!(config.marker_tick_period, 3);
        assert_eq!(config.event_width_codes, vec![1, 2, 3]);
        assert_eq!(
            config.timeline_json,
            Some("/tmp/client-timeline.json".into())
        );
        assert_eq!(
            config.camera_codec,
            Some(crate::input::camera::CameraCodec::Hevc)
        );
        assert_eq!(config.camera_width, Some(1920));
        assert_eq!(config.camera_height, Some(1080));
        assert_eq!(config.camera_fps, Some(30));
    }

    #[test]
    fn parse_args_rejects_invalid_timing_relationships() {
        assert!(parse_args_outcome_from(["--pulse-width-ms", "1000"]).is_err());
        assert!(parse_args_outcome_from(["--pulse-period-ms", "0"]).is_err());
        assert!(parse_args_outcome_from(["--duration-seconds", "0"]).is_err());
        assert!(parse_args_outcome_from(["--marker-tick-period", "0"]).is_err());
        assert!(parse_args_outcome_from(["--event-width-codes", "0"]).is_err());
        assert!(parse_args_outcome_from(["--event-width-codes", "17"]).is_err());
        assert!(parse_args_outcome_from(["--camera-mode", "1920x1080"]).is_err());
        assert!(parse_args_outcome_from(["--camera-mode", "0x1080@30"]).is_err());
        assert!(parse_args_outcome_from(["--camera-codec", "vp9"]).is_err());
    }

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

    #[test]
    fn parse_args_rejects_missing_values_and_unknown_flags() {
        assert!(parse_args_outcome_from(["--server"]).is_err());
        assert!(parse_args_outcome_from(["--duration-seconds"]).is_err());
        assert!(parse_args_outcome_from(["--marker-tick-period"]).is_err());
        assert!(parse_args_outcome_from(["--timeline-json"]).is_err());
        assert!(parse_args_outcome_from(["--camera-codec"]).is_err());
        assert!(parse_args_outcome_from(["--camera-mode"]).is_err());
        assert!(parse_args_outcome_from(["--wat"]).is_err());
    }

    #[test]
    fn parse_args_rejects_non_numeric_values() {
        assert!(parse_args_outcome_from(["--duration-seconds", "abc"]).is_err());
        assert!(parse_args_outcome_from(["--warmup-seconds", "abc"]).is_err());
        assert!(parse_args_outcome_from(["--pulse-period-ms", "abc"]).is_err());
        assert!(parse_args_outcome_from(["--pulse-width-ms", "abc"]).is_err());
        assert!(parse_args_outcome_from(["--marker-tick-period", "abc"]).is_err());
    }
}
