use anyhow::{Result, bail};

mod correlation;
#[cfg(test)]
mod tests;

pub(crate) use correlation::correlate_segments;

pub(super) const DEFAULT_AUDIO_SAMPLE_RATE_HZ: u32 = 48_000;
// Real HDMI/capture paths can preserve pulse shape while compressing its absolute
// luma swing into a narrower band, so keep this guard modest and let the
// segment logic reject genuinely flat/noisy traces.
const MIN_VIDEO_CONTRAST: u8 = 8;

#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PulseSegment {
    pub start_s: f64,
    pub end_s: f64,
    pub duration_s: f64,
}

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())
}

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 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,
        });
    }

    Ok(segments)
}

pub fn detect_audio_onsets(
    samples: &[i16],
    sample_rate_hz: u32,
    window_ms: u32,
) -> Result<Vec<f64>> {
    Ok(detect_audio_segments(samples, sample_rate_hz, window_ms)?
        .into_iter()
        .map(|segment| segment.start_s)
        .collect())
}

pub(crate) fn detect_audio_segments(
    samples: &[i16],
    sample_rate_hz: u32,
    window_ms: u32,
) -> Result<Vec<PulseSegment>> {
    if samples.is_empty() {
        bail!("capture did not contain any audio samples");
    }
    if sample_rate_hz == 0 {
        bail!("audio sample rate must stay positive");
    }
    if window_ms == 0 {
        bail!("audio analysis window must stay positive");
    }

    let window_samples = ((sample_rate_hz as usize * window_ms as usize) / 1000).max(1);
    let envelope = samples
        .chunks(window_samples)
        .map(|chunk| {
            let total: u64 = chunk
                .iter()
                .map(|sample| i32::from(*sample).unsigned_abs() as u64)
                .sum();
            total as f64 / chunk.len() as f64
        })
        .collect::<Vec<_>>();
    let peak = envelope.iter().copied().fold(0.0_f64, f64::max);
    if peak < 50.0 {
        bail!("audio probe peaks are too quiet to detect sync pulses");
    }
    let baseline = median(envelope.clone());
    let threshold = baseline + ((peak - baseline) * 0.45);
    let sample_abs = samples
        .iter()
        .map(|sample| i32::from(*sample).unsigned_abs() as f64)
        .collect::<Vec<_>>();
    let sample_peak = sample_abs.iter().copied().fold(0.0_f64, f64::max);
    let sample_baseline = median(sample_abs.clone());
    let sample_threshold = sample_baseline + ((sample_peak - sample_baseline) * 0.35);
    let mut segments = Vec::new();
    let mut previous_active = false;
    let mut segment_start = 0usize;
    for (index, level) in envelope.iter().copied().enumerate() {
        let active = level >= threshold;
        if active && !previous_active {
            segment_start = index;
        }
        if previous_active && !active {
            segments.push(window_segment(
                samples,
                sample_rate_hz,
                window_samples,
                segment_start,
                index,
                sample_threshold,
            ));
        }
        previous_active = active;
    }
    if previous_active {
        segments.push(window_segment(
            samples,
            sample_rate_hz,
            window_samples,
            segment_start,
            envelope.len(),
            sample_threshold,
        ));
    }

    Ok(segments)
}

pub(super) fn edge_midpoint(previous_s: f64, current_s: f64) -> f64 {
    previous_s + ((current_s - previous_s) / 2.0)
}

pub(super) fn window_segment(
    samples: &[i16],
    sample_rate_hz: u32,
    window_samples: usize,
    start_window_index: usize,
    end_window_index_exclusive: usize,
    sample_threshold: f64,
) -> PulseSegment {
    let start_sample = start_window_index.saturating_mul(window_samples);
    let end_sample = end_window_index_exclusive
        .saturating_mul(window_samples)
        .min(samples.len());

    let refined_start_sample = samples[start_sample..end_sample]
        .iter()
        .position(|sample| i32::from(*sample).unsigned_abs() as f64 >= sample_threshold)
        .map(|offset| start_sample + offset)
        .unwrap_or(start_sample);
    let refined_end_sample = samples[start_sample..end_sample]
        .iter()
        .rposition(|sample| i32::from(*sample).unsigned_abs() as f64 >= sample_threshold)
        .map(|offset| start_sample + offset + 1)
        .unwrap_or(end_sample);

    let start_s = refined_start_sample as f64 / f64::from(sample_rate_hz);
    let end_s = refined_end_sample.max(refined_start_sample + 1) as f64 / f64::from(sample_rate_hz);
    PulseSegment {
        start_s,
        end_s,
        duration_s: end_s - start_s,
    }
}

pub(super) fn median_frame_step_seconds(timestamps_s: &[f64]) -> f64 {
    let diffs = timestamps_s
        .windows(2)
        .filter_map(|pair| {
            let diff = pair[1] - pair[0];
            (diff.is_finite() && diff > 0.0).then_some(diff)
        })
        .collect::<Vec<_>>();
    median(diffs)
}

pub(super) fn median(mut values: Vec<f64>) -> f64 {
    if values.is_empty() {
        return 0.0;
    }
    values.sort_by(|left, right| left.total_cmp(right));
    let mid = values.len() / 2;
    if values.len() % 2 == 0 {
        (values[mid - 1] + values[mid]) / 2.0
    } else {
        values[mid]
    }
}
