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

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

use crate::app_support::DEFAULT_SERVER_ADDR;

#[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,
}

#[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]"
}

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;

    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());
                }
            }
            "--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,
    }))
}

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()))
}

#[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);
    }

    #[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",
        ])
        .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);
    }

    #[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());
    }

    #[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(["--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());
    }
}
