/// Runs only the coverage-mode startup probes and refuses the live control loop.
///
/// Inputs: CLI/env device settings. Output: successful validation until the
/// disabled loop guard. Why: contracts need startup coverage without driving USB.
fn main() -> Result<()> {
    let (dev, _cfg) = parse_args()?;
    let _ = load_interfaces();
    let _ = UvcConfig::from_env();

    let _ = open_with_retry(&dev)?;
    anyhow::bail!("coverage harness: control loop disabled");
}

#[cfg(coverage)]
fn parse_args() -> Result<(String, UvcConfig)> {
    let args: Vec<String> = env::args().skip(1).collect();
    let dev = parse_device_arg(&args)
        .or_else(|| env::var("LESAVKA_UVC_DEV").ok())
        .context("missing --device (or LESAVKA_UVC_DEV)")?;

    Ok((dev, UvcConfig::from_env()))
}

#[cfg(coverage)]
/// Finds the UVC device from `--device`, `-d`, or a positional path argument.
///
/// Inputs: raw CLI arguments. Output: the selected device path when present.
/// Why: manual probes and contract tests both exercise the same device syntax.
fn parse_device_arg(args: &[String]) -> Option<String> {
    args
        .windows(2)
        .find_map(|pair| (pair[0] == "--device" || pair[0] == "-d").then(|| pair[1].clone()))
        .or_else(|| {
            args.iter()
                .rev()
                .find(|arg| {
                    arg.as_str() != "--device" && arg.as_str() != "-d" && arg.starts_with('/')
                })
                .cloned()
        })
}

#[cfg(coverage)]
impl UvcConfig {
    /// Builds a bounded UVC mode from environment overrides.
    ///
    /// Inputs: `LESAVKA_UVC_*` mode variables. Output: a packet-safe mode
    /// snapshot. Why: coverage contracts must pin descriptor math without
    /// requiring the real kernel gadget.
    fn from_env() -> Self {
        let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
        let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
        let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
        let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
        let bulk = uvc_bulk_transfer_enabled();
        let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);

        if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) {
            max_packet = max_packet.min(limit);
        }
        max_packet = if bulk {
            max_packet.min(512)
        } else {
            max_packet.min(1024)
        };
        let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk);

        let interval = if interval == 0 {
            10_000_000 / fps
        } else {
            interval
        };

        Self {
            width,
            height,
            fps,
            interval,
            max_packet,
            frame_size,
            bulk,
        }
    }
}

#[cfg(coverage)]
impl UvcState {
    /// Creates the control-state mirrors used by UVC request handling tests.
    ///
    /// Inputs: one validated UVC mode. Output: default/probe/commit buffers.
    /// Why: tests need deterministic state transitions for host negotiation.
    fn new(cfg: UvcConfig) -> Self {
        let ctrl_len = stream_ctrl_len();
        let default = build_streaming_control(&cfg, ctrl_len);
        Self {
            cfg,
            ctrl_len,
            default,
            probe: default,
            commit: default,
            cfg_snapshot: None,
        }
    }
}

#[cfg(coverage)]
fn load_interfaces() -> UvcInterfaces {
    let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX);
    let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX);
    UvcInterfaces { control, streaming }
}

#[cfg(coverage)]
fn read_interface(path: &str) -> Option<u8> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|v| v.trim().parse::<u8>().ok())
}

#[cfg(coverage)]
/// Opens the UVC control device with safe coverage defaults.
///
/// Inputs: device path and read/write env knobs. Output: opened file handle.
/// Why: tests should default to non-blocking read-only access unless opted in.
fn open_with_retry(path: &str) -> Result<std::fs::File> {
    let read_only = uvc_control_read_only();
    let mut opts = OpenOptions::new();
    opts.read(true);
    if !read_only {
        opts.write(true);
    }
    if env::var("LESAVKA_UVC_BLOCKING").is_err() {
        opts.custom_flags(libc::O_NONBLOCK);
    }
    opts.open(path).with_context(|| format!("open {path}"))
}

#[cfg(coverage)]
/// Keep coverage-mode UVC control opens read-only unless a test opts into writes.
fn uvc_control_read_only() -> bool {
    env::var("LESAVKA_UVC_CONTROL_READ_ONLY")
        .ok()
        .map(|value| {
            let trimmed = value.trim();
            !(trimmed.eq_ignore_ascii_case("0")
                || trimmed.eq_ignore_ascii_case("false")
                || trimmed.eq_ignore_ascii_case("no")
                || trimmed.eq_ignore_ascii_case("off"))
        })
        .unwrap_or(true)
}

#[cfg(coverage)]
/// Returns the bounded UVC request-buffer count used by the helper.
///
/// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe
/// range accepted by the helper. Why: coverage contracts need the same backlog
/// bound as the real UVC binary without opening a gadget device.
fn uvc_buffer_count() -> u32 {
    env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
}

#[cfg(coverage)]
/// Returns the idle frame-pump sleep interval used when no fresh frame is ready.
///
/// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds.
/// Why: this value controls how quickly the UVC helper retries the freshness
/// spool when browsers are already streaming.
fn uvc_idle_pump_sleep() -> std::time::Duration {
    std::time::Duration::from_millis(env_u64(
        "LESAVKA_UVC_IDLE_PUMP_MS",
        DEFAULT_UVC_IDLE_PUMP_MS,
    ))
}

#[cfg(coverage)]
/// Returns the frame queue pacing period for the negotiated UVC frame rate.
///
/// Inputs: `LESAVKA_UVC_QUEUE_PACING` plus the active frame rate. Output:
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
/// not be overfed faster than the advertised descriptor cadence.
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
    if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", DEFAULT_UVC_QUEUE_PACING) {
        return None;
    }
    let fps = fps.max(1);
    Some(std::time::Duration::from_nanos(1_000_000_000 / u64::from(fps)))
}

#[cfg(coverage)]
/// Bound MJPEG frames before they enter the USB UVC helper.
///
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
/// byte length. Why: coverage tests should lock the artifact-prevention budget
/// that turns oversized UVC frames into freezes instead of grey half-frames.
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
    if !uvc_frame_size_guard_enabled() {
        return MAX_MJPEG_FRAME_BYTES;
    }
    if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") {
        return if limit == 0 {
            derived_uvc_frame_max_bytes(cfg)
        } else {
            (limit as usize).min(MAX_MJPEG_FRAME_BYTES)
        };
    }

    derived_uvc_frame_max_bytes(cfg)
}

#[cfg(coverage)]
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
    derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
}

#[cfg(coverage)]
fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
    let fps = fps.max(1);
    let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
    let per_frame = (budget_per_sec / fps).max(64 * 1024);
    per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
}

#[cfg(coverage)]
/// Returns the effective MJPEG byte budget for the selected USB transport.
///
/// Inputs: negotiated max packet size and bulk/isochronous mode. Output: bytes
/// per second. Why: frame-size checks should mirror the transport budget.
fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
    let configured = env_u32(
        "LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
        DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
    )
    .max(1);
    if bulk {
        return configured;
    }

    configured
        .min(uvc_isochronous_budget_bytes_per_sec(max_packet))
        .max(1)
}

#[cfg(coverage)]
/// Computes the high-speed isochronous payload budget.
///
/// Inputs: endpoint packet size and `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT`.
/// Output: capped bytes per second. Why: coverage locks the safety margin.
fn uvc_isochronous_budget_bytes_per_sec(max_packet: u32) -> u32 {
    let pct = env_u32(
        "LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
        DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
    )
    .clamp(1, 100);
    let bytes = u64::from(max_packet)
        .saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
        .saturating_mul(u64::from(pct))
        / 100;
    bytes.min(u64::from(u32::MAX)) as u32
}

#[cfg(coverage)]
/// Parses a boolean environment flag with a default fallback.
///
/// Inputs: variable name and default value. Output: parsed boolean. Why:
/// UVC safety switches must handle common true/false spellings consistently.
fn env_flag_enabled(name: &str, default: bool) -> bool {
    env::var(name)
        .ok()
        .map(|value| {
            let trimmed = value.trim();
            if trimmed.eq_ignore_ascii_case("0")
                || trimmed.eq_ignore_ascii_case("false")
                || trimmed.eq_ignore_ascii_case("no")
                || trimmed.eq_ignore_ascii_case("off")
            {
                false
            } else if trimmed.eq_ignore_ascii_case("1")
                || trimmed.eq_ignore_ascii_case("true")
                || trimmed.eq_ignore_ascii_case("yes")
                || trimmed.eq_ignore_ascii_case("on")
            {
                true
            } else {
                default
            }
        })
        .unwrap_or(default)
}

#[cfg(coverage)]
fn uvc_frame_size_guard_enabled() -> bool {
    env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true)
}

#[cfg(coverage)]
/// Decides whether the coverage helper may use bulk transport.
///
/// Inputs: env override plus configfs capabilities. Output: effective mode.
/// Why: tests should not advertise bulk if the gadget tree lacks support.
fn uvc_bulk_transfer_enabled() -> bool {
    if !env_flag_enabled("LESAVKA_UVC_BULK", true) {
        return false;
    }
    let base = std::path::Path::new(CONFIGFS_UVC_BASE);
    if base.exists() && !base.join("streaming_bulk").exists() {
        return false;
    }
    true
}

#[cfg(coverage)]
fn uvc_frame_size_for_active_mode(
    width: u32,
    height: u32,
    fps: u32,
    max_packet: u32,
    bulk: bool,
) -> u32 {
    env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
        .unwrap_or_else(|| {
            derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
                .min(u32::MAX as usize) as u32
        })
        .max((width.saturating_mul(height) / 32).max(64 * 1024))
}

#[cfg(coverage)]
/// Returns the optional JSON stats path for the UVC coverage helper.
///
/// Inputs: `LESAVKA_UVC_STATS_PATH`. Output: path or disabled state. Why:
/// tests and probes need a cheap health artifact without forcing writes.
fn uvc_stats_path() -> Option<std::path::PathBuf> {
    match std::env::var("LESAVKA_UVC_STATS_PATH") {
        Ok(value) => {
            let trimmed = value.trim();
            if trimmed.is_empty() || trimmed == "0" {
                None
            } else {
                Some(std::path::PathBuf::from(trimmed))
            }
        }
        Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)),
    }
}

#[cfg(coverage)]
/// Writes a text artifact through a temporary file and atomic rename.
///
/// Inputs: target path and payload. Output: persisted file. Why: readers should
/// never observe partially-written UVC stats JSON.
fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
    std::fs::write(&tmp, text)?;
    std::fs::rename(&tmp, path)?;
    Ok(())
}

#[cfg(coverage)]
/// Serializes the compact UVC video-stat snapshot consumed by probes.
///
/// Inputs: current counters and frame cap. Output: one JSON object string. Why:
/// shell probes need stable fields without depending on Rust serialization.
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
    format!(
        "{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
        stats.queued,
        stats.reloaded,
        stats.replayed_stale,
        stats.rejected_oversize,
        stats.rejected_invalid,
        stats.fallback_idle,
        stats.latest_bytes,
        frame_cap,
        stats.last_rejected_oversize_bytes,
        stats.last_rejected_oversize_cap,
        stats.paced_sleeps,
        stats.paced_sleep_ms
    )
}

#[cfg(coverage)]
/// Returns the optional periodic stats write interval.
///
/// Inputs: `LESAVKA_UVC_STATS_INTERVAL_MS`. Output: duration or disabled state.
/// Why: probes can ask for telemetry without slowing every coverage test.
fn uvc_stats_interval() -> Option<std::time::Duration> {
    match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) {
        0 => None,
        value => Some(std::time::Duration::from_millis(value)),
    }
}

#[cfg(coverage)]
/// Returns the optional maximum age for a spooled MJPEG frame.
///
/// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is
/// disabled. Why: stale-frame replay must be explicit because it trades
/// smoothness against freshness during browser stream recovery.
fn frame_spool_max_age() -> Option<std::time::Duration> {
    match env_u64(
        "LESAVKA_UVC_FRAME_MAX_AGE_MS",
        DEFAULT_UVC_FRAME_MAX_AGE_MS,
    ) {
        0 => None,
        value => Some(std::time::Duration::from_millis(value)),
    }
}

#[cfg(coverage)]
/// Determines whether a spooled MJPEG frame is too old to replay.
///
/// Inputs: frame path and optional age limit. Outputs: true when the frame is
/// missing or older than the limit. Why: the coverage harness should guard the
/// same freshness cut-off that prevents seconds-old video from re-entering UVC.
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<std::time::Duration>) -> bool {
    let Some(max_age) = max_age else {
        return false;
    };
    let Ok(metadata) = std::fs::metadata(path) else {
        return true;
    };
    let Ok(modified) = metadata.modified() else {
        return false;
    };
    std::time::SystemTime::now()
        .duration_since(modified)
        .map(|age| age > max_age)
        .unwrap_or(false)
}

#[cfg(coverage)]
/// Parses an unsigned 64-bit environment value with a fallback.
///
/// Inputs: variable name and default value. Outputs: parsed value or default.
/// Why: UVC freshness settings use millisecond durations that should not be
/// truncated to 32-bit just because the coverage harness is lightweight.
fn env_u64(name: &str, default: u64) -> u64 {
    env::var(name)
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .unwrap_or(default)
}
