pub fn detect_video_onsets(timestamps_s: &[f64], brightness: &[u8]) -> Result<Vec<f64>> {
    Ok(detect_video_segments(timestamps_s, brightness)?
        .into_iter()
        .map(|segment| segment.start_s)
        .collect())
}

/// Keeps `detect_video_segments` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(crate) fn detect_video_segments(
    timestamps_s: &[f64],
    brightness: &[u8],
) -> Result<Vec<PulseSegment>> {
    let frame_count = timestamps_s.len().min(brightness.len());
    if frame_count == 0 {
        bail!("capture did not contain any video frames");
    }

    let slice = &brightness[..frame_count];
    let min = *slice.iter().min().expect("non-empty brightness slice");
    let max = *slice.iter().max().expect("non-empty brightness slice");
    if max.saturating_sub(min) < MIN_VIDEO_CONTRAST {
        bail!("video flash contrast is too low to detect sync pulses");
    }
    let threshold = ((u16::from(min) + u16::from(max)) / 2) as u8;
    let frame_step_s = median_frame_step_seconds(&timestamps_s[..frame_count]).max(1.0 / 120.0);
    let active_frames = slice
        .iter()
        .copied()
        .filter(|level| *level >= threshold)
        .count();
    let mut segments = Vec::new();
    let mut previous_active = false;
    let mut segment_start = 0.0_f64;
    let mut previous_timestamp = None;
    let mut last_active_timestamp = None;
    for (timestamp, level) in timestamps_s.iter().copied().zip(slice.iter().copied()) {
        let active = level >= threshold;
        if active && !previous_active {
            segment_start = previous_timestamp
                .map(|prior| edge_midpoint(prior, timestamp))
                .unwrap_or(timestamp);
        }
        if active {
            last_active_timestamp = Some(timestamp);
        }
        if previous_active && !active {
            let end_s = edge_midpoint(
                last_active_timestamp.unwrap_or(timestamp - frame_step_s),
                timestamp,
            )
            .max(segment_start + frame_step_s / 2.0);
            segments.push(PulseSegment {
                start_s: segment_start,
                end_s,
                duration_s: end_s - segment_start,
            });
        }
        previous_active = active;
        previous_timestamp = Some(timestamp);
    }
    if previous_active {
        let last_timestamp = timestamps_s[frame_count - 1];
        let end_s = last_timestamp + frame_step_s / 2.0;
        segments.push(PulseSegment {
            start_s: segment_start,
            end_s,
            duration_s: end_s - segment_start,
        });
    }

    let active_fraction = active_frames as f64 / frame_count as f64;
    let median_segment_duration_s =
        median(segments.iter().map(|segment| segment.duration_s).collect());
    if active_fraction > MAX_VIDEO_ACTIVE_FRAME_FRACTION
        && median_segment_duration_s <= frame_step_s * MAX_VIDEO_FLICKER_SEGMENT_FRAME_MULTIPLIER
    {
        bail!("video flash trace looks like frame-to-frame flicker, not sync pulses");
    }

    Ok(segments)
}

/// Keeps `detect_color_coded_video_segments` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(crate) fn detect_color_coded_video_segments(
    timestamps_s: &[f64],
    frames: &[VideoColorFrame],
    event_codes: &[u32],
    pulse_width_s: f64,
) -> Result<Vec<PulseSegment>> {
    let frame_count = timestamps_s.len().min(frames.len());
    if frame_count == 0 {
        bail!("capture did not contain any video frames");
    }
    if pulse_width_s <= 0.0 {
        bail!("pulse width must stay positive");
    }
    if event_codes.is_empty() {
        bail!("event code list must not be empty");
    }
    if let Some(unsupported) = event_codes
        .iter()
        .find(|code| color_for_event_code(**code).is_none())
    {
        bail!("event code {unsupported} has no video color signature");
    }

    let frame_step_s = median_frame_step_seconds(&timestamps_s[..frame_count]).max(1.0 / 120.0);
    let mut segments = Vec::new();
    let mut previous_code = None::<u32>;
    let mut segment_start = 0.0_f64;
    let mut previous_timestamp = None;
    let mut last_active_timestamp = None;
    let mut segment_codes = Vec::<u32>::new();

    for (timestamp, frame) in timestamps_s.iter().copied().zip(frames.iter().copied()) {
        let code = color_event_code_for_codes(frame, event_codes);
        if code.is_some() && previous_code.is_none() {
            segment_start = previous_timestamp
                .map(|prior| edge_midpoint(prior, timestamp))
                .unwrap_or(timestamp);
            segment_codes.clear();
        }
        if let Some(code) = code {
            last_active_timestamp = Some(timestamp);
            segment_codes.push(code);
        }
        if previous_code.is_some() && code.is_none() {
            push_color_segment(
                &mut segments,
                segment_start,
                edge_midpoint(
                    last_active_timestamp.unwrap_or(timestamp - frame_step_s),
                    timestamp,
                ),
                pulse_width_s,
                &segment_codes,
                frame_step_s,
            );
            segment_codes.clear();
        }
        previous_code = code;
        previous_timestamp = Some(timestamp);
    }
    if previous_code.is_some() {
        let last_timestamp = timestamps_s[frame_count - 1];
        push_color_segment(
            &mut segments,
            segment_start,
            last_timestamp + frame_step_s / 2.0,
            pulse_width_s,
            &segment_codes,
            frame_step_s,
        );
    }

    if segments.is_empty() {
        bail!("video did not contain any recognizable color-coded sync pulses");
    }

    Ok(segments)
}

/// Keeps `merge_nearby_audio_segments` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn merge_nearby_audio_segments(segments: Vec<PulseSegment>) -> Vec<PulseSegment> {
    let mut merged = Vec::<PulseSegment>::new();
    for segment in segments {
        match merged.last_mut() {
            Some(prior) if segment.start_s - prior.end_s <= MAX_AUDIO_PULSE_INTERNAL_GAP_S => {
                prior.end_s = segment.end_s;
                prior.duration_s = prior.end_s - prior.start_s;
            }
            _ => merged.push(segment),
        }
    }
    merged
}

/// Keeps `push_color_segment` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn push_color_segment(
    segments: &mut Vec<PulseSegment>,
    start_s: f64,
    observed_end_s: f64,
    pulse_width_s: f64,
    codes: &[u32],
    frame_step_s: f64,
) {
    let Some(code) = dominant_event_code(codes) else {
        return;
    };
    let observed_duration_s = observed_end_s - start_s;
    let max_observed_duration_s = (pulse_width_s * MAX_COLOR_OBSERVED_DURATION_MULTIPLIER)
        + MAX_COLOR_OBSERVED_DURATION_SLACK_S;
    if observed_duration_s > max_observed_duration_s {
        return;
    }
    let encoded_duration_s = pulse_width_s * f64::from(code);
    segments.push(PulseSegment {
        start_s,
        end_s: observed_end_s.max(start_s + frame_step_s / 2.0),
        duration_s: encoded_duration_s,
    });
}

/// Keeps `dominant_event_code` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn dominant_event_code(codes: &[u32]) -> Option<u32> {
    let mut counts = std::collections::BTreeMap::<u32, usize>::new();
    for code in codes {
        *counts.entry(*code).or_default() += 1;
    }
    counts
        .into_iter()
        .max_by(|(left_code, left_count), (right_code, right_count)| {
            left_count
                .cmp(right_count)
                .then_with(|| right_code.cmp(left_code))
        })
        .map(|(code, _)| code)
}

/// Keeps `color_event_code_for_codes` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn color_event_code_for_codes(frame: VideoColorFrame, event_codes: &[u32]) -> Option<u32> {
    let max = frame.r.max(frame.g).max(frame.b);
    let min = frame.r.min(frame.g).min(frame.b);
    if max < MIN_COLOR_PULSE_VALUE || max.saturating_sub(min) < MIN_COLOR_PULSE_SATURATION {
        return None;
    }

    event_codes
        .iter()
        .copied()
        .filter_map(|code| {
            color_for_event_code(code).map(|color| (code, color_distance_squared(frame, color)))
        })
        .min_by_key(|(_, distance)| *distance)
        .and_then(|(code, distance)| (distance <= MAX_COLOR_DISTANCE_SQUARED).then_some(code))
        .or_else(|| dominant_color_event_code(frame).filter(|code| event_codes.contains(code)))
}

/// Keeps `dominant_color_event_code` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn dominant_color_event_code(frame: VideoColorFrame) -> Option<u32> {
    let r = i16::from(frame.r);
    let g = i16::from(frame.g);
    let b = i16::from(frame.b);

    if r - b >= DOMINANT_COLOR_MARGIN
        && g - b >= DOMINANT_COLOR_MARGIN
        && (r - g).abs() <= DOMINANT_COLOR_MARGIN * 3
    {
        return Some(4);
    }
    if r - g >= DOMINANT_COLOR_MARGIN && r - b >= DOMINANT_COLOR_MARGIN {
        return Some(1);
    }
    if g - r >= DOMINANT_COLOR_MARGIN && g - b >= DOMINANT_COLOR_MARGIN {
        return Some(2);
    }
    if b - r >= DOMINANT_COLOR_MARGIN && b - g >= DOMINANT_COLOR_MARGIN {
        return Some(3);
    }
    None
}

fn color_for_event_code(code: u32) -> Option<VideoColorFrame> {
    probe_color_for_event_code(code).map(|color| VideoColorFrame {
        r: color.r,
        g: color.g,
        b: color.b,
    })
}

fn color_distance_squared(left: VideoColorFrame, right: VideoColorFrame) -> u32 {
    let dr = i32::from(left.r) - i32::from(right.r);
    let dg = i32::from(left.g) - i32::from(right.g);
    let db = i32::from(left.b) - i32::from(right.b);
    (dr * dr + dg * dg + db * db) as u32
}
