use anyhow::{anyhow, Context, Result};
use std::path::Path;
use std::process::{Command as ProcCommand, Stdio};

use crate::preprocessing::build_frame_extraction_vf;
use crate::{CancelToken, FfmpegConfig, Progress, VideoOptions};

/// Spawn a configured ffmpeg command and wait for it, polling an optional
/// cancellation token. If cancellation is requested the child process is killed
/// and `Cancelled` is returned; otherwise behaves like a blocking wait.
fn run_ffmpeg_cancellable(mut command: ProcCommand, cancel: Option<&CancelToken>, what: &str) -> Result<()> {
    let mut child = command.spawn().with_context(|| format!("spawning {}", what))?;
    loop {
        if let Some(status) = child.try_wait().with_context(|| format!("waiting for {}", what))? {
            if !status.success() {
                return Err(anyhow!("{} failed", what));
            }
            return Ok(());
        }
        if cancel.is_some_and(|c| c.is_cancelled()) {
            let _ = child.kill();
            let _ = child.wait();
            return Err(crate::Cancelled.into());
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn extract_video_frames(input: &Path, out_dir: &Path, columns: u32, fps: u32, start: Option<&str>, end: Option<&str>, preprocess_filter: Option<&str>, ffmpeg_config: &FfmpegConfig, cancel: Option<&CancelToken>) -> Result<()> {
    let out_pattern = out_dir.join("frame_%04d.png");
    let mut ffmpeg_args: Vec<String> = vec!["-loglevel".into(), "error".into()];

    if let Some(s) = start {
        if !s.is_empty() && s != "0" {
            ffmpeg_args.push("-ss".into());
            ffmpeg_args.push(s.to_string());
        }
    }

    ffmpeg_args.push("-i".into());
    ffmpeg_args.push(input.to_str().unwrap().to_string());

    if let Some(e) = end {
        if !e.is_empty() {
            if let Some(s) = start {
                if !s.is_empty() && s != "0" {
                    let start_secs = parse_timestamp(s);
                    let end_secs = parse_timestamp(e);
                    let duration = end_secs - start_secs;
                    if duration > 0.0 {
                        ffmpeg_args.push("-t".into());
                        ffmpeg_args.push(duration.to_string());
                    }
                } else {
                    ffmpeg_args.push("-t".into());
                    ffmpeg_args.push(e.to_string());
                }
            } else {
                ffmpeg_args.push("-t".into());
                ffmpeg_args.push(e.to_string());
            }
        }
    }

    let vf_option = build_frame_extraction_vf(columns, fps, preprocess_filter);
    ffmpeg_args.push("-vf".into());
    ffmpeg_args.push(vf_option);
    ffmpeg_args.push(out_pattern.to_str().unwrap().to_string());

    let mut command = ProcCommand::new(ffmpeg_config.ffmpeg_cmd());
    command.args(&ffmpeg_args);
    run_ffmpeg_cancellable(command, cancel, "ffmpeg")
}

/// Get video duration in microseconds using ffprobe
pub(crate) fn get_video_duration_us(input: &Path, ffmpeg_config: &FfmpegConfig) -> Result<u64> {
    let output = ProcCommand::new(ffmpeg_config.ffprobe_cmd()).args(["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", input.to_str().unwrap()]).output().context("running ffprobe")?;

    if !output.status.success() {
        return Err(anyhow!("ffprobe failed to get duration"));
    }

    let duration_str = String::from_utf8_lossy(&output.stdout);
    let duration_secs: f64 = duration_str.trim().parse().unwrap_or(0.0);
    Ok((duration_secs * 1_000_000.0) as u64)
}

/// Extract video frames with progress reporting
pub(crate) fn extract_video_frames_with_progress<F: Fn(Progress) + Send + Sync>(input: &Path, out_dir: &Path, video_opts: &VideoOptions, ffmpeg_config: &FfmpegConfig, progress_callback: &F, cancel: Option<&CancelToken>) -> Result<()> {
    let columns = video_opts.columns;
    let fps = video_opts.fps;
    let start = video_opts.start.as_deref();
    let end = video_opts.end.as_deref();

    let out_pattern = out_dir.join("frame_%04d.png");

    // Get video duration for progress calculation
    let _total_duration_us = get_video_duration_us(input, ffmpeg_config).unwrap_or(0);

    let mut ffmpeg_args: Vec<String> = vec!["-loglevel".into(), "error".into(), "-progress".into(), "pipe:1".into(), "-nostats".into()];

    if let Some(s) = start {
        if !s.is_empty() && s != "0" {
            ffmpeg_args.push("-ss".into());
            ffmpeg_args.push(s.to_string());
        }
    }

    ffmpeg_args.push("-i".into());
    ffmpeg_args.push(input.to_str().ok_or_else(|| anyhow!("input path is not valid UTF-8"))?.to_string());

    if let Some(e) = end {
        if !e.is_empty() {
            if let Some(s) = start {
                if !s.is_empty() && s != "0" {
                    let start_secs = parse_timestamp(s);
                    let end_secs = parse_timestamp(e);
                    let duration = end_secs - start_secs;
                    if duration > 0.0 {
                        ffmpeg_args.push("-t".into());
                        ffmpeg_args.push(duration.to_string());
                    }
                } else {
                    ffmpeg_args.push("-t".into());
                    ffmpeg_args.push(e.to_string());
                }
            } else {
                ffmpeg_args.push("-t".into());
                ffmpeg_args.push(e.to_string());
            }
        }
    }

    let vf_option = build_frame_extraction_vf(columns, fps, video_opts.preprocess_filter.as_deref());
    ffmpeg_args.push("-vf".into());
    ffmpeg_args.push(vf_option);
    ffmpeg_args.push(out_pattern.to_str().ok_or_else(|| anyhow!("output path is not valid UTF-8"))?.to_string());
    progress_callback(Progress::extracting_frames());

    let mut command = ProcCommand::new(ffmpeg_config.ffmpeg_cmd());
    command.args(&ffmpeg_args).stdout(Stdio::piped()).stderr(Stdio::null());
    run_ffmpeg_cancellable(command, cancel, "ffmpeg")
}

pub(crate) fn extract_audio(input: &Path, out_dir: &Path, start: Option<&str>, end: Option<&str>, ffmpeg_config: &FfmpegConfig, cancel: Option<&CancelToken>) -> Result<()> {
    let out_audio = out_dir.join("audio.mp3");
    let mut ffmpeg_args: Vec<String> = vec!["-loglevel".into(), "error".into(), "-y".into()];

    if let Some(s) = start {
        if !s.is_empty() && s != "0" {
            ffmpeg_args.push("-ss".into());
            ffmpeg_args.push(s.to_string());
        }
    }

    ffmpeg_args.push("-i".into());
    ffmpeg_args.push(input.to_str().unwrap().to_string());

    if let Some(e) = end {
        if !e.is_empty() {
            if let Some(s) = start {
                if !s.is_empty() && s != "0" {
                    let start_secs = parse_timestamp(s);
                    let end_secs = parse_timestamp(e);
                    let duration = end_secs - start_secs;
                    if duration > 0.0 {
                        ffmpeg_args.push("-t".into());
                        ffmpeg_args.push(duration.to_string());
                    }
                } else {
                    ffmpeg_args.push("-t".into());
                    ffmpeg_args.push(e.to_string());
                }
            } else {
                ffmpeg_args.push("-t".into());
                ffmpeg_args.push(e.to_string());
            }
        }
    }

    // Extract audio only, no video
    ffmpeg_args.push("-vn".into());
    ffmpeg_args.push("-acodec".into());
    ffmpeg_args.push("libmp3lame".into());
    ffmpeg_args.push("-q:a".into());
    ffmpeg_args.push("2".into());
    ffmpeg_args.push(out_audio.to_str().unwrap().to_string());

    let mut command = ProcCommand::new(ffmpeg_config.ffmpeg_cmd());
    command.args(&ffmpeg_args);
    run_ffmpeg_cancellable(command, cancel, "ffmpeg audio extraction")?;
    Ok(())
}

pub(crate) fn parse_timestamp(s: &str) -> f64 {
    s.split(':').rev().enumerate().fold(0.0, |acc, (i, v)| acc + v.parse::<f64>().unwrap_or(0.0) * 60f64.powi(i as i32))
}
