#[cfg_attr(not(test), allow(dead_code))]
/// Keeps `correlate_onsets` 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(super) fn correlate_onsets(
    video_onsets_s: &[f64],
    audio_onsets_s: &[f64],
    pulse_period_s: f64,
    max_pair_gap_s: f64,
) -> Result<SyncAnalysisReport> {
    if video_onsets_s.is_empty() {
        bail!("video onset list is empty");
    }
    if audio_onsets_s.is_empty() {
        bail!("audio onset list is empty");
    }
    if max_pair_gap_s <= 0.0 {
        bail!("max pair gap must stay positive");
    }
    if pulse_period_s <= 0.0 {
        bail!("pulse period must stay positive");
    }

    let raw_first_video_activity_s = video_onsets_s[0];
    let raw_first_audio_activity_s = audio_onsets_s[0];
    let activity_start_delta_ms =
        (raw_first_audio_activity_s - raw_first_video_activity_s) * 1000.0;
    let (video_onsets_s, audio_onsets_s, common_window) =
        trim_onsets_to_common_activity_window(video_onsets_s, audio_onsets_s, max_pair_gap_s);
    let expected_start_skew_ms = (audio_onsets_s[0] - video_onsets_s[0]) * 1000.0;
    let video_pulses = index_onsets_by_spacing(video_onsets_s, pulse_period_s);
    let audio_pulses = index_onsets_by_spacing(audio_onsets_s, pulse_period_s);
    let offset_candidates = candidate_index_offsets(&video_pulses, &audio_pulses);
    let mut pairs = best_pairs_for_index_offsets(
        &video_pulses,
        &audio_pulses,
        &offset_candidates,
        max_pair_gap_s,
        expected_start_skew_ms,
    );

    if pairs.is_empty() && video_onsets_s.len() == 1 && audio_onsets_s.len() == 1 {
        let video_phase_s = estimate_phase(video_onsets_s, pulse_period_s);
        let audio_phase_s = estimate_phase(audio_onsets_s, pulse_period_s);
        let phase_skew_ms =
            shortest_wrapped_difference(audio_phase_s - video_phase_s, pulse_period_s) * 1000.0;
        if phase_skew_ms.abs() <= max_pair_gap_s * 1000.0 {
            pairs.push(MatchedOnsetPair::new(
                video_onsets_s[0],
                audio_onsets_s[0],
                phase_skew_ms,
                max_pair_gap_s,
            ));
        }
    }

    if pairs.is_empty() {
        bail!("no audio/video pulse pairs were close enough to compare");
    }

    Ok(sync_report_from_pairs(
        common_window.filter_onsets(video_onsets_s),
        common_window.filter_onsets(audio_onsets_s),
        false,
        activity_start_delta_ms,
        raw_first_video_activity_s,
        raw_first_audio_activity_s,
        pairs,
    ))
}

/// Keeps `correlate_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 correlate_segments(
    video_segments: &[PulseSegment],
    audio_segments: &[PulseSegment],
    pulse_period_s: f64,
    pulse_width_s: f64,
    marker_tick_period: u32,
    max_pair_gap_s: f64,
) -> Result<SyncAnalysisReport> {
    if video_segments.is_empty() {
        bail!("video onset list is empty");
    }
    if audio_segments.is_empty() {
        bail!("audio onset list is empty");
    }
    if pulse_period_s <= 0.0 {
        bail!("pulse period must stay positive");
    }
    if pulse_width_s <= 0.0 {
        bail!("pulse width must stay positive");
    }
    if marker_tick_period == 0 {
        bail!("marker tick period must stay positive");
    }
    if max_pair_gap_s <= 0.0 {
        bail!("max pair gap must stay positive");
    }

    let raw_first_video_activity_s = video_segments
        .first()
        .expect("validated video segment list is not empty")
        .start_s;
    let raw_first_audio_activity_s = audio_segments
        .first()
        .expect("validated audio segment list is not empty")
        .start_s;
    let activity_start_delta_ms =
        (raw_first_audio_activity_s - raw_first_video_activity_s) * 1000.0;

    let phase_tolerance_s = segment_phase_tolerance(pulse_period_s, pulse_width_s, max_pair_gap_s);
    let video_segments =
        collapse_segments_by_phase(video_segments, pulse_period_s, phase_tolerance_s);
    let audio_segments =
        collapse_segments_by_phase(audio_segments, pulse_period_s, phase_tolerance_s);

    let video_onsets_s = video_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    let audio_onsets_s = audio_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    let (video_onsets_s, audio_onsets_s, common_window) =
        trim_onsets_to_common_activity_window(&video_onsets_s, &audio_onsets_s, max_pair_gap_s);
    let expected_start_skew_ms = (audio_onsets_s[0] - video_onsets_s[0]) * 1000.0;
    let video_marker_onsets = marker_onsets(&video_segments, pulse_width_s);
    let audio_marker_onsets = marker_onsets(&audio_segments, pulse_width_s);
    let video_marker_onsets = common_window.filter_onsets(&video_marker_onsets);
    let audio_marker_onsets = common_window.filter_onsets(&audio_marker_onsets);
    let video_indexed = index_onsets_by_spacing(video_onsets_s, pulse_period_s);
    let audio_indexed = index_onsets_by_spacing(audio_onsets_s, pulse_period_s);
    let offset_candidates = marker_index_offsets(
        &video_indexed,
        &audio_indexed,
        video_marker_onsets,
        audio_marker_onsets,
    );
    let mut pairs = best_pairs_for_index_offsets(
        &video_indexed,
        &audio_indexed,
        &offset_candidates,
        max_pair_gap_s,
        expected_start_skew_ms,
    );

    if pairs.is_empty() && video_onsets_s.len() == 1 && audio_onsets_s.len() == 1 {
        let video_phase_s = estimate_phase(video_onsets_s, pulse_period_s);
        let audio_phase_s = estimate_phase(audio_onsets_s, pulse_period_s);
        let phase_skew_ms =
            shortest_wrapped_difference(audio_phase_s - video_phase_s, pulse_period_s) * 1000.0;
        if phase_skew_ms.abs() <= max_pair_gap_s * 1000.0 {
            pairs.push(MatchedOnsetPair::new(
                video_onsets_s[0],
                audio_onsets_s[0],
                phase_skew_ms,
                max_pair_gap_s,
            ));
        }
    }

    if pairs.is_empty() {
        bail!("no audio/video pulse pairs were close enough to compare");
    }

    Ok(sync_report_from_pairs(
        video_onsets_s,
        audio_onsets_s,
        false,
        activity_start_delta_ms,
        raw_first_video_activity_s,
        raw_first_audio_activity_s,
        pairs,
    ))
}

/// Keeps `correlate_coded_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 correlate_coded_segments(
    video_segments: &[PulseSegment],
    audio_segments: &[PulseSegment],
    pulse_period_s: f64,
    pulse_width_s: f64,
    event_width_codes: &[u32],
    max_pair_gap_s: f64,
) -> Result<SyncAnalysisReport> {
    if event_width_codes.is_empty() {
        bail!("event width code sequence must not be empty");
    }
    if event_width_codes.contains(&0) {
        bail!("event width codes must stay positive");
    }
    if pulse_period_s <= 0.0 {
        bail!("pulse period must stay positive");
    }
    if pulse_width_s <= 0.0 {
        bail!("pulse width must stay positive");
    }
    if max_pair_gap_s <= 0.0 {
        bail!("max pair gap must stay positive");
    }

    let raw_first_video_activity_s = video_segments
        .first()
        .expect("validated video segment list is not empty")
        .start_s;
    let raw_first_audio_activity_s = audio_segments
        .first()
        .expect("validated audio segment list is not empty")
        .start_s;
    let activity_start_delta_ms =
        (raw_first_audio_activity_s - raw_first_video_activity_s) * 1000.0;

    let raw_video_segments = video_segments.to_vec();
    let raw_audio_segments = audio_segments.to_vec();
    let phase_tolerance_s = segment_phase_tolerance(pulse_period_s, pulse_width_s, max_pair_gap_s);
    let video_segments =
        collapse_segments_by_phase(video_segments, pulse_period_s, phase_tolerance_s);
    let audio_segments =
        collapse_segments_by_phase(audio_segments, pulse_period_s, phase_tolerance_s);
    let video_onsets_s = video_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    let audio_onsets_s = audio_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    let (_, _, common_window) =
        trim_onsets_to_common_activity_window(&video_onsets_s, &audio_onsets_s, max_pair_gap_s);
    let filtered_video_segments = filter_segments_to_window(&video_segments, common_window);
    let filtered_audio_segments = filter_segments_to_window(&audio_segments, common_window);
    if filtered_video_segments.is_empty() || filtered_audio_segments.is_empty() {
        bail!(
            "coded pulse common window removed one stream entirely; refusing cadence-only fallback for coded proof (video={} audio={} raw activity delta {activity_start_delta_ms:+.1} ms)",
            video_segments.len(),
            audio_segments.len(),
        );
    }

    let expected_start_skew_ms =
        (filtered_audio_segments[0].start_s - filtered_video_segments[0].start_s) * 1000.0;
    let video_indexed = index_coded_segments_by_spacing(
        &filtered_video_segments,
        pulse_period_s,
        pulse_width_s,
        event_width_codes,
    );
    let audio_indexed = index_coded_segments_by_spacing(
        &filtered_audio_segments,
        pulse_period_s,
        pulse_width_s,
        event_width_codes,
    );
    let offset_candidates = candidate_coded_index_offsets(&video_indexed, &audio_indexed);
    let pairs = best_coded_pairs_for_index_offsets(
        &video_indexed,
        &audio_indexed,
        &offset_candidates,
        event_width_codes,
        max_pair_gap_s,
        expected_start_skew_ms,
    );

    if pairs.len() < MIN_CODED_PAIRS {
        let time_pairs = best_coded_pairs_by_time(
            &filtered_video_segments,
            &filtered_audio_segments,
            pulse_width_s,
            event_width_codes,
            max_pair_gap_s,
        );
        if time_pairs.len() >= MIN_CODED_PAIRS {
            let video_onsets_s = filtered_video_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            let audio_onsets_s = filtered_audio_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            return Ok(sync_report_from_pairs(
                &video_onsets_s,
                &audio_onsets_s,
                true,
                activity_start_delta_ms,
                raw_first_video_activity_s,
                raw_first_audio_activity_s,
                time_pairs,
            ));
        }

        if let Some((raw_filtered_video_segments, raw_filtered_audio_segments, raw_pairs)) =
            best_coded_pairs_for_raw_segments(
                &raw_video_segments,
                &raw_audio_segments,
                pulse_period_s,
                pulse_width_s,
                event_width_codes,
                max_pair_gap_s,
            )
            && raw_pairs.len() >= MIN_CODED_PAIRS
        {
            let video_onsets_s = raw_filtered_video_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            let audio_onsets_s = raw_filtered_audio_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            return Ok(sync_report_from_pairs(
                &video_onsets_s,
                &audio_onsets_s,
                true,
                activity_start_delta_ms,
                raw_first_video_activity_s,
                raw_first_audio_activity_s,
                raw_pairs,
            ));
        }

        let raw_full_video_indexed = index_coded_segments_by_spacing(
            &raw_video_segments,
            pulse_period_s,
            pulse_width_s,
            event_width_codes,
        );
        let raw_full_audio_indexed = index_coded_segments_by_spacing(
            &raw_audio_segments,
            pulse_period_s,
            pulse_width_s,
            event_width_codes,
        );
        let diagnostic_pairs = diagnostic_coded_pairs_for_index_offsets(
            &raw_full_video_indexed,
            &raw_full_audio_indexed,
            &candidate_coded_index_offsets(&raw_full_video_indexed, &raw_full_audio_indexed),
            event_width_codes,
            DIAGNOSTIC_CODED_MAX_PAIR_GAP_S,
            activity_start_delta_ms,
        );
        if diagnostic_pairs.len() >= MIN_CODED_PAIRS {
            let raw_full_video_onsets_s = raw_video_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            let raw_full_audio_onsets_s = raw_audio_segments
                .iter()
                .map(|segment| segment.start_s)
                .collect::<Vec<_>>();
            return Ok(sync_report_from_pairs(
                &raw_full_video_onsets_s,
                &raw_full_audio_onsets_s,
                true,
                activity_start_delta_ms,
                raw_first_video_activity_s,
                raw_first_audio_activity_s,
                diagnostic_pairs,
            ));
        }

        bail!(
            "need at least {MIN_CODED_PAIRS} matching coded pulse pairs; saw {}; raw activity delta was {activity_start_delta_ms:+.1} ms (video={raw_first_video_activity_s:.3}s audio={raw_first_audio_activity_s:.3}s)",
            pairs.len()
        );
    }

    let video_onsets_s = filtered_video_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    let audio_onsets_s = filtered_audio_segments
        .iter()
        .map(|segment| segment.start_s)
        .collect::<Vec<_>>();
    Ok(sync_report_from_pairs(
        &video_onsets_s,
        &audio_onsets_s,
        true,
        activity_start_delta_ms,
        raw_first_video_activity_s,
        raw_first_audio_activity_s,
        pairs,
    ))
}
