use std::env;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

use temp_env::with_var;
use tempfile::tempdir;

/// Keeps `with_fake_media_tools` 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 with_fake_media_tools<T>(
    ffprobe_output: &[u8],
    ffmpeg_video_output: &[u8],
    ffmpeg_audio_output: &[u8],
    test: impl FnOnce(&Path) -> T,
) -> T {
    let temp_dir = tempdir().expect("tempdir");
    fs::write(temp_dir.path().join("ffprobe.out"), ffprobe_output).expect("write ffprobe");
    fs::write(
        temp_dir.path().join("ffmpeg-video.out"),
        ffmpeg_video_output,
    )
    .expect("write ffmpeg video");
    fs::write(
        temp_dir.path().join("ffmpeg-audio.out"),
        ffmpeg_audio_output,
    )
    .expect("write ffmpeg audio");
    write_executable(
        temp_dir.path(),
        "ffprobe",
        "#!/bin/sh\ncat \"$(dirname \"$0\")/ffprobe.out\"\n",
    );
    write_executable(
        temp_dir.path(),
        "ffmpeg",
        "#!/bin/sh\ncase \" $* \" in\n  *\" -map 0:v:0 \"*) cat \"$(dirname \"$0\")/ffmpeg-video.out\" ;;\n  *\" -map 0:a:0 \"*) cat \"$(dirname \"$0\")/ffmpeg-audio.out\" ;;\n  *) printf 'unexpected ffmpeg args: %s\\n' \"$*\" >&2; exit 64 ;;\nesac\n",
    );

    let prior_path = env::var("PATH").unwrap_or_default();
    let merged_path = if prior_path.is_empty() {
        temp_dir.path().display().to_string()
    } else {
        format!("{}:{prior_path}", temp_dir.path().display())
    };
    let capture_path = temp_dir.path().join("capture.mkv");
    fs::write(&capture_path, b"fake-capture").expect("write capture");
    with_var("PATH", Some(merged_path.as_str()), || test(&capture_path))
}

pub(super) fn frame_json(timestamps: &[f64]) -> Vec<u8> {
    let frames = timestamps
        .iter()
        .map(|timestamp| {
            serde_json::json!({
                "best_effort_timestamp_time": format!("{timestamp:.3}")
            })
        })
        .collect::<Vec<_>>();
    serde_json::to_vec(&serde_json::json!({ "frames": frames })).expect("frame json")
}

/// Keeps `click_track_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 click_track_samples(click_times_s: &[f64], total_samples: usize) -> Vec<i16> {
    let mut samples = vec![0i16; total_samples];
    for click_time_s in click_times_s {
        let start = (*click_time_s * 48_000.0).round() as usize;
        for sample in samples.iter_mut().skip(start).take(300) {
            *sample = 18_000;
        }
    }
    samples
}

/// Keeps `thumbnail_video_bytes` 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 thumbnail_video_bytes(brightness_values: &[u8]) -> Vec<u8> {
    const SIDE: usize = 64;
    let mut bytes = Vec::with_capacity(brightness_values.len() * SIDE * SIDE);
    for brightness in brightness_values {
        let mut frame = vec![20u8; SIDE * SIDE];
        for y in SIDE / 4..SIDE - SIDE / 4 {
            for x in SIDE / 4..SIDE - SIDE / 4 {
                frame[y * SIDE + x] = *brightness;
            }
        }
        bytes.extend_from_slice(&frame);
    }
    bytes
}

/// Keeps `thumbnail_rgb_video_bytes` 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 thumbnail_rgb_video_bytes(colors: &[(u8, u8, u8)]) -> Vec<u8> {
    const SIDE: usize = 64;
    let mut bytes = Vec::with_capacity(colors.len() * SIDE * SIDE * 3);
    for (r, g, b) in colors {
        let mut frame = vec![0u8; SIDE * SIDE * 3];
        for y in SIDE / 4..SIDE - SIDE / 4 {
            for x in SIDE / 4..SIDE - SIDE / 4 {
                let offset = (y * SIDE + x) * 3;
                frame[offset] = *r;
                frame[offset + 1] = *g;
                frame[offset + 2] = *b;
            }
        }
        bytes.extend_from_slice(&frame);
    }
    bytes
}

pub(super) fn audio_samples_to_bytes(samples: &[i16]) -> Vec<u8> {
    samples
        .iter()
        .flat_map(|sample| sample.to_le_bytes())
        .collect()
}

fn write_executable(dir: &Path, name: &str, contents: &str) {
    let path = dir.join(name);
    fs::write(&path, contents).expect("write script");
    let mut permissions = fs::metadata(&path).expect("script metadata").permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions).expect("script permissions");
}
