use serde::Serialize;

const DEFAULT_AUDIO_WINDOW_MS: u32 = 5;
const DEFAULT_MAX_PAIR_GAP_S: f64 = 0.5;
const DEFAULT_PULSE_PERIOD_S: f64 = 1.0;
const DEFAULT_PULSE_WIDTH_S: f64 = 0.12;
const DEFAULT_MARKER_TICK_PERIOD: u32 = 5;
const CALIBRATION_MIN_PAIRED_EVENTS: usize = 8;
const CALIBRATION_MAX_DRIFT_MS: f64 = 40.0;
const CALIBRATION_SETTLED_SKEW_MS: f64 = 5.0;

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SyncAnalysisReport {
    pub video_event_count: usize,
    pub audio_event_count: usize,
    pub paired_event_count: usize,
    pub first_skew_ms: f64,
    pub last_skew_ms: f64,
    pub mean_skew_ms: f64,
    pub median_skew_ms: f64,
    pub max_abs_skew_ms: f64,
    pub drift_ms: f64,
    pub skews_ms: Vec<f64>,
    pub video_onsets_s: Vec<f64>,
    pub audio_onsets_s: Vec<f64>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct SyncCalibrationRecommendation {
    pub ready: bool,
    pub recommended_audio_offset_adjust_us: i64,
    pub recommended_video_offset_adjust_us: i64,
    pub note: String,
}

impl SyncAnalysisReport {
    #[must_use]
    pub fn calibration_recommendation(&self) -> SyncCalibrationRecommendation {
        if self.paired_event_count < CALIBRATION_MIN_PAIRED_EVENTS {
            return SyncCalibrationRecommendation {
                ready: false,
                recommended_audio_offset_adjust_us: 0,
                recommended_video_offset_adjust_us: 0,
                note: format!(
                    "need at least {CALIBRATION_MIN_PAIRED_EVENTS} paired pulses; saw {}",
                    self.paired_event_count
                ),
            };
        }

        if self.drift_ms.abs() > CALIBRATION_MAX_DRIFT_MS {
            return SyncCalibrationRecommendation {
                ready: false,
                recommended_audio_offset_adjust_us: 0,
                recommended_video_offset_adjust_us: 0,
                note: format!(
                    "drift {:.1} ms exceeds the {:.1} ms calibration limit",
                    self.drift_ms, CALIBRATION_MAX_DRIFT_MS
                ),
            };
        }

        let recommended_audio_offset_adjust_us = (-(self.median_skew_ms * 1_000.0)).round() as i64;
        let recommended_video_offset_adjust_us = -recommended_audio_offset_adjust_us;
        let note = if self.median_skew_ms.abs() <= CALIBRATION_SETTLED_SKEW_MS {
            format!(
                "median skew {:.1} ms is already within the settled {:.1} ms band",
                self.median_skew_ms, CALIBRATION_SETTLED_SKEW_MS
            )
        } else {
            format!(
                "apply the audio offset adjustment to move median skew from {:+.1} ms toward 0.0 ms",
                self.median_skew_ms
            )
        };

        SyncCalibrationRecommendation {
            ready: true,
            recommended_audio_offset_adjust_us,
            recommended_video_offset_adjust_us,
            note,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct SyncAnalysisOptions {
    pub audio_window_ms: u32,
    pub max_pair_gap_s: f64,
    pub pulse_period_s: f64,
    pub pulse_width_s: f64,
    pub marker_tick_period: u32,
}

impl Default for SyncAnalysisOptions {
    fn default() -> Self {
        Self {
            audio_window_ms: DEFAULT_AUDIO_WINDOW_MS,
            max_pair_gap_s: DEFAULT_MAX_PAIR_GAP_S,
            pulse_period_s: DEFAULT_PULSE_PERIOD_S,
            pulse_width_s: DEFAULT_PULSE_WIDTH_S,
            marker_tick_period: DEFAULT_MARKER_TICK_PERIOD,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{SyncAnalysisOptions, SyncAnalysisReport};

    #[test]
    fn default_options_match_live_probe_expectations() {
        let options = SyncAnalysisOptions::default();
        assert_eq!(options.audio_window_ms, 5);
        assert!((options.max_pair_gap_s - 0.5).abs() < f64::EPSILON);
        assert!((options.pulse_period_s - 1.0).abs() < f64::EPSILON);
        assert!((options.pulse_width_s - 0.12).abs() < f64::EPSILON);
        assert_eq!(options.marker_tick_period, 5);
    }

    #[test]
    fn calibration_recommendation_requires_enough_pairs() {
        let report = SyncAnalysisReport {
            video_event_count: 4,
            audio_event_count: 4,
            paired_event_count: 4,
            first_skew_ms: 20.0,
            last_skew_ms: 20.0,
            mean_skew_ms: 20.0,
            median_skew_ms: 20.0,
            max_abs_skew_ms: 20.0,
            drift_ms: 0.0,
            skews_ms: vec![20.0; 4],
            video_onsets_s: vec![],
            audio_onsets_s: vec![],
        };

        let recommendation = report.calibration_recommendation();
        assert!(!recommendation.ready);
        assert_eq!(recommendation.recommended_audio_offset_adjust_us, 0);
        assert!(
            recommendation
                .note
                .contains("need at least 8 paired pulses")
        );
    }

    #[test]
    fn calibration_recommendation_rejects_unstable_drift() {
        let report = SyncAnalysisReport {
            video_event_count: 12,
            audio_event_count: 12,
            paired_event_count: 12,
            first_skew_ms: 10.0,
            last_skew_ms: 70.0,
            mean_skew_ms: 40.0,
            median_skew_ms: 35.0,
            max_abs_skew_ms: 70.0,
            drift_ms: 60.0,
            skews_ms: vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0],
            video_onsets_s: vec![],
            audio_onsets_s: vec![],
        };

        let recommendation = report.calibration_recommendation();
        assert!(!recommendation.ready);
        assert_eq!(recommendation.recommended_audio_offset_adjust_us, 0);
        assert!(recommendation.note.contains("drift 60.0 ms exceeds"));
    }

    #[test]
    fn calibration_recommendation_maps_median_skew_to_audio_and_video_offsets() {
        let report = SyncAnalysisReport {
            video_event_count: 14,
            audio_event_count: 14,
            paired_event_count: 12,
            first_skew_ms: 28.0,
            last_skew_ms: 32.0,
            mean_skew_ms: 30.0,
            median_skew_ms: 30.0,
            max_abs_skew_ms: 32.0,
            drift_ms: 4.0,
            skews_ms: vec![28.0, 30.0, 32.0],
            video_onsets_s: vec![],
            audio_onsets_s: vec![],
        };

        let recommendation = report.calibration_recommendation();
        assert!(recommendation.ready);
        assert_eq!(recommendation.recommended_audio_offset_adjust_us, -30_000);
        assert_eq!(recommendation.recommended_video_offset_adjust_us, 30_000);
        assert!(recommendation.note.contains("move median skew"));
    }

    #[test]
    fn calibration_recommendation_reports_when_skew_is_already_settled() {
        let report = SyncAnalysisReport {
            video_event_count: 14,
            audio_event_count: 14,
            paired_event_count: 12,
            first_skew_ms: 3.0,
            last_skew_ms: 4.0,
            mean_skew_ms: 3.5,
            median_skew_ms: 4.0,
            max_abs_skew_ms: 4.0,
            drift_ms: 1.0,
            skews_ms: vec![3.0, 4.0],
            video_onsets_s: vec![],
            audio_onsets_s: vec![],
        };

        let recommendation = report.calibration_recommendation();
        assert!(recommendation.ready);
        assert_eq!(recommendation.recommended_audio_offset_adjust_us, -4_000);
        assert!(recommendation.note.contains("already within the settled"));
    }
}
