#[derive(Clone, Copy, Debug, PartialEq)]
struct CodedPulseSegment {
    start_s: f64,
    code: u32,
}

/// Keeps `index_coded_segments_by_spacing` 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 index_coded_segments_by_spacing(
    segments: &[PulseSegment],
    pulse_period_s: f64,
    pulse_width_s: f64,
    event_width_codes: &[u32],
) -> BTreeMap<i64, CodedPulseSegment> {
    let mut indexed = BTreeMap::new();
    let Some(first_segment) = segments.first().copied() else {
        return indexed;
    };

    let mut pulse_index = 0_i64;
    let mut previous_start = first_segment.start_s;
    indexed.insert(
        pulse_index,
        CodedPulseSegment {
            start_s: first_segment.start_s,
            code: nearest_event_width_code(
                first_segment.duration_s,
                pulse_width_s,
                event_width_codes,
            ),
        },
    );
    for segment in segments.iter().copied().skip(1) {
        let pulse_steps = ((segment.start_s - previous_start) / pulse_period_s)
            .round()
            .max(1.0) as i64;
        pulse_index += pulse_steps;
        indexed.insert(
            pulse_index,
            CodedPulseSegment {
                start_s: segment.start_s,
                code: nearest_event_width_code(
                    segment.duration_s,
                    pulse_width_s,
                    event_width_codes,
                ),
            },
        );
        previous_start = segment.start_s;
    }
    indexed
}

/// Keeps `nearest_event_width_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 nearest_event_width_code(duration_s: f64, pulse_width_s: f64, event_width_codes: &[u32]) -> u32 {
    let ratio = duration_s / pulse_width_s.max(f64::EPSILON);
    event_width_codes
        .iter()
        .copied()
        .min_by(|left, right| {
            let left_error = (ratio - f64::from(*left)).abs();
            let right_error = (ratio - f64::from(*right)).abs();
            left_error.total_cmp(&right_error)
        })
        .unwrap_or(1)
}

/// Keeps `candidate_index_offsets` 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 candidate_index_offsets(
    video_indexed: &BTreeMap<i64, f64>,
    audio_indexed: &BTreeMap<i64, f64>,
) -> Vec<i64> {
    if video_indexed.is_empty() || audio_indexed.is_empty() {
        return Vec::new();
    }

    let video_min = *video_indexed
        .keys()
        .next()
        .expect("non-empty indexed video onset map has a first key");
    let video_max = *video_indexed
        .keys()
        .next_back()
        .expect("non-empty indexed video onset map has a last key");
    let audio_min = *audio_indexed
        .keys()
        .next()
        .expect("non-empty indexed audio onset map has a first key");
    let audio_max = *audio_indexed
        .keys()
        .next_back()
        .expect("non-empty indexed audio onset map has a last key");

    (audio_min - video_max..=audio_max - video_min).collect()
}

/// Keeps `candidate_coded_index_offsets` 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 candidate_coded_index_offsets(
    video_indexed: &BTreeMap<i64, CodedPulseSegment>,
    audio_indexed: &BTreeMap<i64, CodedPulseSegment>,
) -> Vec<i64> {
    if video_indexed.is_empty() || audio_indexed.is_empty() {
        return Vec::new();
    }

    let video_min = *video_indexed
        .keys()
        .next()
        .expect("non-empty indexed video map has a first key");
    let video_max = *video_indexed
        .keys()
        .next_back()
        .expect("non-empty indexed video map has a last key");
    let audio_min = *audio_indexed
        .keys()
        .next()
        .expect("non-empty indexed audio map has a first key");
    let audio_max = *audio_indexed
        .keys()
        .next_back()
        .expect("non-empty indexed audio map has a last key");

    (audio_min - video_max..=audio_max - video_min).collect()
}

/// Keeps `marker_index_offsets` 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 marker_index_offsets(
    video_indexed: &BTreeMap<i64, f64>,
    audio_indexed: &BTreeMap<i64, f64>,
    video_marker_onsets: &[f64],
    audio_marker_onsets: &[f64],
) -> Vec<i64> {
    let mut offsets = Vec::new();
    if !video_marker_onsets.is_empty() && !audio_marker_onsets.is_empty() {
        let video_markers = pulse_indices_for_onsets(video_indexed, video_marker_onsets);
        let audio_markers = pulse_indices_for_onsets(audio_indexed, audio_marker_onsets);
        for video_marker in &video_markers {
            for audio_marker in &audio_markers {
                offsets.push(audio_marker - video_marker);
            }
        }
    }

    offsets.extend(candidate_index_offsets(video_indexed, audio_indexed));
    offsets.sort_unstable();
    offsets.dedup();
    offsets
}

fn pulse_indices_for_onsets(indexed: &BTreeMap<i64, f64>, marker_onsets: &[f64]) -> Vec<i64> {
    marker_onsets
        .iter()
        .filter_map(|marker_onset| {
            indexed.iter().find_map(|(pulse_index, onset)| {
                ((onset - marker_onset).abs() < 0.000_001).then_some(*pulse_index)
            })
        })
        .collect()
}

#[derive(Clone, Debug, PartialEq)]
struct MatchedOnsetPair {
    video_time_s: f64,
    audio_time_s: f64,
    skew_ms: f64,
    confidence: f64,
    server_event_id: Option<usize>,
    event_code: Option<u32>,
}

impl MatchedOnsetPair {
    /// Keeps `new` 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 new(video_time_s: f64, audio_time_s: f64, skew_ms: f64, max_pair_gap_s: f64) -> Self {
        let max_pair_gap_ms = max_pair_gap_s * 1000.0;
        let confidence = if max_pair_gap_ms <= 0.0 {
            0.0
        } else {
            (1.0 - (skew_ms.abs() / max_pair_gap_ms)).clamp(0.0, 1.0)
        };
        Self {
            video_time_s,
            audio_time_s,
            skew_ms,
            confidence,
            server_event_id: None,
            event_code: None,
        }
    }

    fn with_identity(mut self, event_width_codes: &[u32], event_code: u32) -> Self {
        self.server_event_id = unique_event_id_for_code(event_width_codes, event_code);
        self.event_code = Some(event_code);
        self
    }
}

/// Keeps `best_pairs_for_index_offsets` 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 best_pairs_for_index_offsets(
    video_indexed: &BTreeMap<i64, f64>,
    audio_indexed: &BTreeMap<i64, f64>,
    offset_candidates: &[i64],
    max_pair_gap_s: f64,
    expected_start_skew_ms: f64,
) -> Vec<MatchedOnsetPair> {
    let max_pair_gap_ms = max_pair_gap_s * 1000.0;
    let startup_phase_anchor_tolerance_ms =
        max_pair_gap_ms * STARTUP_PHASE_ANCHOR_TOLERANCE_FRACTION;
    let mut best: Option<(bool, usize, f64, f64, Vec<MatchedOnsetPair>)> = None;

    for offset in offset_candidates.iter().copied() {
        let pairs = video_indexed
            .iter()
            .filter_map(|(pulse_index, video_time)| {
                audio_indexed
                    .get(&(pulse_index + offset))
                    .map(|audio_time| {
                        let skew_ms = (audio_time - video_time) * 1000.0;
                        MatchedOnsetPair::new(*video_time, *audio_time, skew_ms, max_pair_gap_s)
                    })
            })
            .filter(|pair| pair.skew_ms.abs() <= max_pair_gap_ms)
            .collect::<Vec<_>>();
        if pairs.is_empty() {
            continue;
        }

        let mean_abs_skew_ms =
            pairs.iter().map(|pair| pair.skew_ms.abs()).sum::<f64>() / pairs.len() as f64;
        let startup_phase_anchor_error_ms = (pairs[0].skew_ms - expected_start_skew_ms).abs();
        let startup_phase_anchor_consistent =
            startup_phase_anchor_error_ms <= startup_phase_anchor_tolerance_ms;
        match &best {
            Some((
                best_anchor_consistent,
                best_count,
                best_anchor_error_ms,
                best_mean_abs_skew_ms,
                _,
            )) if (!startup_phase_anchor_consistent && *best_anchor_consistent)
                || (startup_phase_anchor_consistent == *best_anchor_consistent
                    && (pairs.len() < *best_count
                        || (pairs.len() == *best_count
                            && (startup_phase_anchor_error_ms > *best_anchor_error_ms
                                || (startup_phase_anchor_error_ms == *best_anchor_error_ms
                                    && mean_abs_skew_ms >= *best_mean_abs_skew_ms))))) => {}
            _ => {
                best = Some((
                    startup_phase_anchor_consistent,
                    pairs.len(),
                    startup_phase_anchor_error_ms,
                    mean_abs_skew_ms,
                    pairs,
                ))
            }
        }
    }

    best.map(|(_, _, _, _, pairs)| pairs).unwrap_or_default()
}

/// Keeps `best_coded_pairs_for_index_offsets` 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 best_coded_pairs_for_index_offsets(
    video_indexed: &BTreeMap<i64, CodedPulseSegment>,
    audio_indexed: &BTreeMap<i64, CodedPulseSegment>,
    offset_candidates: &[i64],
    event_width_codes: &[u32],
    max_pair_gap_s: f64,
    expected_start_skew_ms: f64,
) -> Vec<MatchedOnsetPair> {
    let max_pair_gap_ms = max_pair_gap_s * 1000.0;
    let startup_phase_anchor_tolerance_ms =
        max_pair_gap_ms * STARTUP_PHASE_ANCHOR_TOLERANCE_FRACTION;
    let mut best: Option<(bool, usize, f64, f64, Vec<MatchedOnsetPair>)> = None;

    for offset in offset_candidates.iter().copied() {
        let pairs = video_indexed
            .iter()
            .filter_map(|(pulse_index, video)| {
                audio_indexed
                    .get(&(pulse_index + offset))
                    .filter(|audio| audio.code == video.code)
                    .map(|audio| {
                        let skew_ms = (audio.start_s - video.start_s) * 1000.0;
                        MatchedOnsetPair::new(video.start_s, audio.start_s, skew_ms, max_pair_gap_s)
                            .with_identity(event_width_codes, video.code)
                    })
            })
            .filter(|pair| pair.skew_ms.abs() <= max_pair_gap_ms)
            .collect::<Vec<_>>();
        if pairs.is_empty() {
            continue;
        }

        let mean_abs_skew_ms =
            pairs.iter().map(|pair| pair.skew_ms.abs()).sum::<f64>() / pairs.len() as f64;
        let startup_phase_anchor_error_ms = (pairs[0].skew_ms - expected_start_skew_ms).abs();
        let startup_phase_anchor_consistent =
            startup_phase_anchor_error_ms <= startup_phase_anchor_tolerance_ms;
        match &best {
            Some((
                best_anchor_consistent,
                best_count,
                best_anchor_error_ms,
                best_mean_abs_skew_ms,
                _,
            )) if (!startup_phase_anchor_consistent && *best_anchor_consistent)
                || (startup_phase_anchor_consistent == *best_anchor_consistent
                    && (pairs.len() < *best_count
                        || (pairs.len() == *best_count
                            && (startup_phase_anchor_error_ms > *best_anchor_error_ms
                                || (startup_phase_anchor_error_ms == *best_anchor_error_ms
                                    && mean_abs_skew_ms >= *best_mean_abs_skew_ms))))) => {}
            _ => {
                best = Some((
                    startup_phase_anchor_consistent,
                    pairs.len(),
                    startup_phase_anchor_error_ms,
                    mean_abs_skew_ms,
                    pairs,
                ))
            }
        }
    }

    best.map(|(_, _, _, _, pairs)| pairs).unwrap_or_default()
}

/// Keeps `best_coded_pairs_by_time` 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 best_coded_pairs_by_time(
    video_segments: &[PulseSegment],
    audio_segments: &[PulseSegment],
    pulse_width_s: f64,
    event_width_codes: &[u32],
    max_pair_gap_s: f64,
) -> Vec<MatchedOnsetPair> {
    let max_pair_gap_ms = max_pair_gap_s * 1000.0;
    let mut used_audio = vec![false; audio_segments.len()];
    let mut pairs = Vec::new();

    for video in video_segments {
        let video_code =
            nearest_event_width_code(video.duration_s, pulse_width_s, event_width_codes);
        let best_audio = audio_segments
            .iter()
            .enumerate()
            .filter(|(index, audio)| {
                !used_audio[*index]
                    && nearest_event_width_code(audio.duration_s, pulse_width_s, event_width_codes)
                        == video_code
            })
            .map(|(index, audio)| {
                let skew_ms = (audio.start_s - video.start_s) * 1000.0;
                (index, audio, skew_ms)
            })
            .filter(|(_, _, skew_ms)| skew_ms.abs() <= max_pair_gap_ms)
            .min_by(|(_, _, left_skew), (_, _, right_skew)| {
                left_skew.abs().total_cmp(&right_skew.abs())
            });

        if let Some((audio_index, audio, skew_ms)) = best_audio {
            used_audio[audio_index] = true;
            pairs.push(
                MatchedOnsetPair::new(video.start_s, audio.start_s, skew_ms, max_pair_gap_s)
                    .with_identity(event_width_codes, video_code),
            );
        }
    }

    pairs
}

/// Keeps `diagnostic_coded_pairs_for_index_offsets` 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 diagnostic_coded_pairs_for_index_offsets(
    video_indexed: &BTreeMap<i64, CodedPulseSegment>,
    audio_indexed: &BTreeMap<i64, CodedPulseSegment>,
    offset_candidates: &[i64],
    event_width_codes: &[u32],
    max_pair_gap_s: f64,
    expected_start_skew_ms: f64,
) -> Vec<MatchedOnsetPair> {
    let max_pair_gap_ms = max_pair_gap_s * 1000.0;
    let mut best: Option<(f64, usize, f64, Vec<MatchedOnsetPair>)> = None;

    for offset in offset_candidates.iter().copied() {
        let pairs = video_indexed
            .iter()
            .filter_map(|(pulse_index, video)| {
                audio_indexed
                    .get(&(pulse_index + offset))
                    .filter(|audio| audio.code == video.code)
                    .map(|audio| {
                        let skew_ms = (audio.start_s - video.start_s) * 1000.0;
                        MatchedOnsetPair::new(video.start_s, audio.start_s, skew_ms, max_pair_gap_s)
                            .with_identity(event_width_codes, video.code)
                    })
            })
            .filter(|pair| pair.skew_ms.abs() <= max_pair_gap_ms)
            .collect::<Vec<_>>();
        if pairs.is_empty() {
            continue;
        }

        let anchor_error_ms = (pairs[0].skew_ms - expected_start_skew_ms).abs();
        let mean_abs_skew_ms =
            pairs.iter().map(|pair| pair.skew_ms.abs()).sum::<f64>() / pairs.len() as f64;
        match &best {
            Some((best_anchor_error_ms, best_count, best_mean_abs_skew_ms, _))
                if anchor_error_ms > *best_anchor_error_ms
                    || (anchor_error_ms == *best_anchor_error_ms
                        && (pairs.len() < *best_count
                            || (pairs.len() == *best_count
                                && mean_abs_skew_ms >= *best_mean_abs_skew_ms))) => {}
            _ => best = Some((anchor_error_ms, pairs.len(), mean_abs_skew_ms, pairs)),
        }
    }

    best.map(|(_, _, _, pairs)| pairs).unwrap_or_default()
}

pub(super) fn marker_onsets(segments: &[PulseSegment], pulse_width_s: f64) -> Vec<f64> {
    let threshold = pulse_width_s * MARKER_WIDTH_MULTIPLIER;
    segments
        .iter()
        .filter(|segment| segment.duration_s >= threshold)
        .map(|segment| segment.start_s)
        .collect()
}

pub(super) fn shortest_wrapped_difference(delta_s: f64, pulse_period_s: f64) -> f64 {
    let half_period = pulse_period_s / 2.0;
    ((delta_s + half_period).rem_euclid(pulse_period_s)) - half_period
}
