use anyhow::{Context, Result, bail};
use serde::Deserialize;
use std::path::Path;
use std::process::Command;

use super::onset_detection::VideoColorFrame;

mod roi;

use roi::{summarize_gray_frames_with_adaptive_roi, summarize_rgb_frames_with_adaptive_roi};

#[cfg(test)]
use roi::{
    adaptive_gray_roi_mask, adaptive_rgb_roi_mask, dark_roi_factor, palette_match_score,
    retain_largest_connected_roi, summarize_frame_brightness, summarize_frame_color,
};

const VIDEO_ANALYSIS_SIDE_PX: usize = 64;
const VIDEO_ANALYSIS_FPS: usize = 60;
const MIN_ADAPTIVE_ROI_PIXELS: usize = 16;
const MAX_ADAPTIVE_ROI_FRACTION: f64 = 0.35;
const ADAPTIVE_ROI_SCORE_FRACTION: f64 = 0.30;
const MIN_RGB_ROI_SCORE: f64 = 24.0;
const MIN_GRAY_ROI_SCORE: f64 = 8.0;

#[derive(Debug, Deserialize)]
struct ProbeFrameResponse {
    #[serde(default)]
    frames: Vec<ProbeFrameEntry>,
}

#[derive(Debug, Deserialize)]
struct ProbeFrameEntry {
    best_effort_timestamp_time: Option<String>,
}

/// Keeps `extract_video_timestamps` 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 extract_video_timestamps(capture_path: &Path) -> Result<Vec<f64>> {
    let output = run_command(
        Command::new("ffprobe")
            .arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-select_streams")
            .arg("v:0")
            .arg("-show_frames")
            .arg("-show_entries")
            .arg("frame=best_effort_timestamp_time")
            .arg("-of")
            .arg("json")
            .arg(capture_path),
        "ffprobe video timestamps",
    )?;
    let response: ProbeFrameResponse =
        serde_json::from_slice(&output).context("parsing ffprobe frame JSON")?;
    let timestamps = response
        .frames
        .into_iter()
        .filter_map(|entry| entry.best_effort_timestamp_time)
        .map(|value| value.parse::<f64>().context("parsing frame timestamp"))
        .collect::<Result<Vec<_>>>()?;
    if timestamps.is_empty() {
        bail!("ffprobe did not return any video frame timestamps");
    }
    Ok(timestamps)
}

/// Keeps `extract_video_brightness` 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 extract_video_brightness(capture_path: &Path) -> Result<Vec<u8>> {
    let output = run_command(
        Command::new("ffmpeg")
            .arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-i")
            .arg(capture_path)
            .arg("-map")
            .arg("0:v:0")
            .arg("-vf")
            .arg(format!(
                "fps={fps},scale={side}:{side}:flags=area,format=gray",
                fps = VIDEO_ANALYSIS_FPS,
                side = VIDEO_ANALYSIS_SIDE_PX
            ))
            .arg("-f")
            .arg("rawvideo")
            .arg("-pix_fmt")
            .arg("gray")
            .arg("-"),
        "ffmpeg video brightness extraction",
    )?;
    if output.is_empty() {
        bail!("ffmpeg did not emit any video brightness data");
    }

    let frame_pixels = VIDEO_ANALYSIS_SIDE_PX * VIDEO_ANALYSIS_SIDE_PX;
    if output.len() % frame_pixels != 0 {
        bail!(
            "ffmpeg emitted {} bytes of video brightness data, which is not divisible by the {}-pixel analysis frame size",
            output.len(),
            frame_pixels
        );
    }
    let extracted_frames = output.len() / frame_pixels;

    Ok(summarize_gray_frames_with_adaptive_roi(
        output.chunks_exact(frame_pixels).take(extracted_frames),
        frame_pixels,
    ))
}

/// Keeps `extract_video_colors` 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 extract_video_colors(capture_path: &Path) -> Result<Vec<VideoColorFrame>> {
    let output = run_command(
        Command::new("ffmpeg")
            .arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-i")
            .arg(capture_path)
            .arg("-map")
            .arg("0:v:0")
            .arg("-vf")
            .arg(format!(
                "fps={fps},scale={side}:{side}:flags=area,format=rgb24",
                fps = VIDEO_ANALYSIS_FPS,
                side = VIDEO_ANALYSIS_SIDE_PX
            ))
            .arg("-f")
            .arg("rawvideo")
            .arg("-pix_fmt")
            .arg("rgb24")
            .arg("-"),
        "ffmpeg video color extraction",
    )?;
    if output.is_empty() {
        bail!("ffmpeg did not emit any video color data");
    }

    let frame_bytes = VIDEO_ANALYSIS_SIDE_PX * VIDEO_ANALYSIS_SIDE_PX * 3;
    if output.len() % frame_bytes != 0 {
        bail!(
            "ffmpeg emitted {} bytes of video color data, which is not divisible by the {}-byte analysis frame size",
            output.len(),
            frame_bytes
        );
    }
    let extracted_frames = output.len() / frame_bytes;

    Ok(summarize_rgb_frames_with_adaptive_roi(
        output.chunks_exact(frame_bytes).take(extracted_frames),
        VIDEO_ANALYSIS_SIDE_PX * VIDEO_ANALYSIS_SIDE_PX,
    ))
}

/// Keeps `extract_audio_samples` 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 extract_audio_samples(capture_path: &Path) -> Result<Vec<i16>> {
    let output = run_command(
        Command::new("ffmpeg")
            .arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-i")
            .arg(capture_path)
            .arg("-map")
            .arg("0:a:0")
            .arg("-ac")
            .arg("1")
            .arg("-ar")
            .arg(super::onset_detection::DEFAULT_AUDIO_SAMPLE_RATE_HZ.to_string())
            .arg("-f")
            .arg("s16le")
            .arg("-acodec")
            .arg("pcm_s16le")
            .arg("-"),
        "ffmpeg audio extraction",
    )?;
    if output.len() < 2 {
        bail!("ffmpeg did not emit enough audio data to analyze");
    }
    Ok(output
        .chunks_exact(2)
        .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
        .collect())
}

/// Keeps `run_command` 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 run_command(command: &mut Command, description: &str) -> Result<Vec<u8>> {
    let output = command
        .output()
        .with_context(|| format!("running {description}"))?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("{description} failed: {}", stderr.trim());
    }
    Ok(output.stdout)
}

#[cfg(test)]
#[path = "media_extract/tests/mod.rs"]
mod tests;
