fn synthetic_bundle(args: &Args, sequence: u64, pts_us: u64, data: Vec<u8>) -> UpstreamMediaBundle {
    let video = VideoPacket {
        id: 0,
        pts: pts_us,
        data,
        seq: sequence,
        effective_fps: args.fps,
        client_capture_pts_us: pts_us,
        client_send_pts_us: pts_us,
        ..Default::default()
    };
    let audio = AudioPacket {
        id: 0,
        pts: pts_us,
        data: silence_pcm(args.frame_step_us()),
        seq: sequence,
        client_capture_pts_us: pts_us,
        client_send_pts_us: pts_us,
        encoding: AudioEncoding::PcmS16le as i32,
        sample_rate: DEFAULT_SAMPLE_RATE,
        channels: DEFAULT_CHANNELS,
        frame_duration_us: args.frame_step_us().min(u64::from(u32::MAX)) as u32,
        ..Default::default()
    };
    UpstreamMediaBundle {
        session_id: args.session_id,
        seq: sequence,
        capture_start_us: pts_us,
        capture_end_us: pts_us,
        video: Some(video),
        audio: vec![audio],
        audio_sample_rate: DEFAULT_SAMPLE_RATE,
        audio_channels: DEFAULT_CHANNELS,
        video_width: args.width as u32,
        video_height: args.height as u32,
        video_fps: args.fps,
        audio_encoding: AudioEncoding::PcmS16le as i32,
    }
}

fn silence_pcm(duration_us: u64) -> Vec<u8> {
    let samples = (u64::from(DEFAULT_SAMPLE_RATE).saturating_mul(duration_us) / 1_000_000).max(1);
    let bytes = samples
        .saturating_mul(u64::from(DEFAULT_CHANNELS))
        .saturating_mul(std::mem::size_of::<i16>() as u64)
        .min(usize::MAX as u64) as usize;
    vec![0; bytes]
}

fn synthetic_rgb_frame(width: usize, height: usize, sequence: u64) -> Vec<u8> {
    let mut frame = vec![0u8; width.saturating_mul(height).saturating_mul(3)];
    let moving_width = (width / 10).max(32).min(width.max(1));
    let moving_offset = (sequence as usize).wrapping_mul(13) % width.max(1);
    for y in 0..height {
        for x in 0..width {
            let value = synthetic_luma(
                (width, height),
                x,
                y,
                sequence,
                (moving_width, moving_offset),
            );
            let offset = (y * width + x) * 3;
            frame[offset] = value;
            frame[offset + 1] = value;
            frame[offset + 2] = value;
        }
    }
    draw_sequence_marker(&mut frame, width, height, sequence);
    frame
}

fn synthetic_luma(
    frame_size: (usize, usize),
    x: usize,
    y: usize,
    sequence: u64,
    moving_bar: (usize, usize),
) -> u8 {
    let (width, height) = frame_size;
    let (moving_width, moving_offset) = moving_bar;
    let width = width.max(1);
    let height = height.max(1);
    if let Some(value) = synthetic_band_marker_luma(width, height, x, y, sequence) {
        return value;
    }
    let block_w = (width / 24).max(24);
    let block_h = (height / 18).max(18);
    let base = 44
        + (x.saturating_mul(72) / width)
        + (y.saturating_mul(52) / height)
        + ((sequence as usize).saturating_mul(3) % 28);
    let checker = if (((x / block_w) + (y / block_h) + (sequence as usize / 5)) & 1) == 0 {
        30
    } else {
        0
    };
    let mut value = (base + checker).min(238) as u8;
    let moving = (x + width - moving_offset) % width;
    if moving < moving_width {
        value = (220usize.saturating_sub(y.saturating_mul(54) / height)).min(255) as u8;
    } else if moving < moving_width + 4 {
        value = 28;
    }
    let center_x = width / 2;
    let center_y = height / 2;
    if x.abs_diff(center_x) < width / 9 && y.abs_diff(center_y) < height / 12 {
        value = 255u8.saturating_sub(value / 2);
    }
    value
}

fn synthetic_band_marker_luma(
    width: usize,
    height: usize,
    x: usize,
    y: usize,
    sequence: u64,
) -> Option<u8> {
    let marker_width = BAND_MARKER_BITS * BAND_MARKER_CELL;
    if width < marker_width + 8 || height < BAND_MARKER_ROWS {
        return None;
    }
    let band = y / BAND_MARKER_ROWS;
    let band_y = band * BAND_MARKER_ROWS + 4;
    if !(band_y..band_y + BAND_MARKER_CELL * 2).contains(&y) {
        return None;
    }
    let x0 = width - marker_width - 4;
    if !(x0..x0 + marker_width).contains(&x) {
        return None;
    }
    let bit = (x - x0) / BAND_MARKER_CELL;
    let marker = (sequence as u32 & 0x00ff_ffff) | ((band as u32 & 0xff) << 24);
    Some(if marker & (1 << bit) != 0 { 245 } else { 12 })
}

fn add_jpeg_app4_integrity(jpeg: &[u8], sequence: u64) -> Result<Vec<u8>> {
    lesavka_server::uvc_contract::add_lesavka_app4_integrity(jpeg, sequence)
        .context("synthetic encoder returned an incomplete JPEG")
}

fn marker_cell(width: usize, height: usize) -> usize {
    (width.min(height) / 80).clamp(6, 16)
}

fn draw_sequence_marker(frame: &mut [u8], width: usize, height: usize, sequence: u64) {
    let cell = marker_cell(width, height);
    let rows = MARKER_BITS.div_ceil(MARKER_COLUMNS);
    if width < (MARKER_COLUMNS + 4) * cell || height < (rows + 4) * cell {
        return;
    }
    let x0 = 2 * cell;
    let y0 = 2 * cell;
    fill_rect(
        frame,
        width,
        cell,
        cell,
        (MARKER_COLUMNS + 2) * cell,
        (rows + 2) * cell,
        32,
    );
    fill_rect(frame, width, x0 - cell, y0 - cell, cell, cell, 255);
    fill_rect(
        frame,
        width,
        x0 + MARKER_COLUMNS * cell,
        y0 - cell,
        cell,
        cell,
        0,
    );
    for bit in 0..MARKER_BITS {
        let col = bit % MARKER_COLUMNS;
        let row = bit / MARKER_COLUMNS;
        let value = if ((sequence >> bit) & 1) != 0 { 255 } else { 0 };
        fill_rect(
            frame,
            width,
            x0 + col * cell,
            y0 + row * cell,
            cell,
            cell,
            value,
        );
    }
}

fn fill_rect(frame: &mut [u8], width: usize, x0: usize, y0: usize, w: usize, h: usize, value: u8) {
    let pixels = frame.len() / 3;
    let height = pixels / width.max(1);
    let x1 = (x0 + w).min(width);
    let y1 = (y0 + h).min(height);
    for y in y0..y1 {
        for x in x0..x1 {
            let offset = (y * width + x) * 3;
            if let Some(pixel) = frame.get_mut(offset..offset + 3) {
                pixel[0] = value;
                pixel[1] = value;
                pixel[2] = value;
            }
        }
    }
}

fn parse_mode(value: &str) -> Result<(usize, usize, u32)> {
    let (size, fps) = value
        .split_once('@')
        .with_context(|| format!("mode must look like WIDTHxHEIGHT@FPS, got {value:?}"))?;
    let (width, height) = size
        .split_once('x')
        .with_context(|| format!("mode must look like WIDTHxHEIGHT@FPS, got {value:?}"))?;
    Ok((width.parse()?, height.parse()?, fps.parse()?))
}

fn next_value(it: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> {
    it.next()
        .with_context(|| format!("{flag} requires a value"))
}

fn parse_next<T>(it: &mut impl Iterator<Item = String>, flag: &str) -> Result<T>
where
    T: std::str::FromStr,
    T::Err: std::error::Error + Send + Sync + 'static,
{
    Ok(next_value(it, flag)?.parse()?)
}

fn env_usize(name: &str) -> Option<usize> {
    std::env::var(name)
        .ok()
        .and_then(|value| value.trim().parse::<usize>().ok())
}

fn duration_mul(duration: Duration, count: u64) -> Duration {
    Duration::from_nanos(
        duration
            .as_nanos()
            .saturating_mul(u128::from(count))
            .min(u128::from(u64::MAX)) as u64,
    )
}

fn unix_millis() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
        .min(u128::from(u64::MAX)) as u64
}

fn write_summary(
    args: &Args,
    stats: Option<&EncodeStats>,
    exit_reason: Option<&str>,
) -> Result<()> {
    if let Some(dir) = &args.artifact_dir {
        std::fs::write(
            dir.join("summary.json"),
            args_summary_json(args, stats, exit_reason) + "\n",
        )?;
    }
    Ok(())
}

fn json_usize_or_null(value: Option<usize>) -> String {
    value
        .map(|value| value.to_string())
        .unwrap_or_else(|| "null".to_string())
}

fn json_string_or_null(value: Option<&str>) -> String {
    value
        .map(|value| format!("{value:?}"))
        .unwrap_or_else(|| "null".to_string())
}

fn args_summary_json(
    args: &Args,
    stats: Option<&EncodeStats>,
    exit_reason: Option<&str>,
) -> String {
    let frames = stats.map(|stats| stats.frames).unwrap_or(0);
    let sent_frames = stats.map(|stats| stats.sent_frames).unwrap_or(0);
    let min_bytes =
        json_usize_or_null(stats.and_then(|stats| (stats.frames > 0).then_some(stats.min_bytes)));
    let max_bytes =
        json_usize_or_null(stats.and_then(|stats| (stats.frames > 0).then_some(stats.max_bytes)));
    let mean_bytes = json_usize_or_null(
        stats.and_then(|stats| (stats.frames > 0).then_some(stats.mean_bytes())),
    );
    let oversize_frames = stats.map(|stats| stats.oversize_frames).unwrap_or(0);
    let exit_reason = json_string_or_null(exit_reason);
    format!(
        "{{\"schema\":\"lesavka.synthetic-uplink.v1\",\"server\":{server:?},\"width\":{width},\"height\":{height},\"fps\":{fps},\"duration_s\":{duration:.3},\"session_id\":{session},\"tls\":{tls},\"jpeg_quality\":{quality},\"max_frame_bytes\":{max_frame_bytes},\"encoded_frames\":{frames},\"sent_frames\":{sent_frames},\"encoded_min_bytes\":{min_bytes},\"encoded_max_bytes\":{max_bytes},\"encoded_mean_bytes\":{mean_bytes},\"encoded_oversize_frames\":{oversize_frames},\"exit_reason\":{exit_reason}}}",
        server = args.server,
        width = args.width,
        height = args.height,
        fps = args.fps,
        duration = args.duration.as_secs_f64(),
        session = args.session_id,
        tls = is_https(&args.server),
        quality = args.jpeg_quality,
        max_frame_bytes = args.max_frame_bytes,
        frames = frames,
        sent_frames = sent_frames,
        min_bytes = min_bytes,
        max_bytes = max_bytes,
        mean_bytes = mean_bytes,
        oversize_frames = oversize_frames,
        exit_reason = exit_reason,
    )
}

fn print_help() {
    println!(
        "lesavka-synthetic-uplink\n\n\
         Sends sequence-coded synthetic MJPEG plus silent PCM through StreamWebcamMedia.\n\n\
         Options:\n\
           --server URL             gRPC endpoint, default {DEFAULT_SERVER}\n\
           --mode WIDTHxHEIGHT@FPS  shorthand for width/height/fps\n\
           --width N --height N --fps N\n\
           --duration SECONDS       default 300\n\
           --jpeg-quality N         default {DEFAULT_JPEG_QUALITY}\n\
           --max-frame-bytes N      fail fast if an encoded frame exceeds N bytes\n\
           --artifact-dir PATH      write command/summary metadata\n\
           --print-every N          progress interval in frames"
    );
}
