use std::{
    fs,
    io::BufRead,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{anyhow, Context, Result};

pub struct AudioPipeline;

impl AudioPipeline {
    pub fn extract_to_wav<F: Fn(f32)>(
        ffmpeg_path: &Path,
        input_path: &str,
        workspace: &Path,
        on_progress: F,
    ) -> Result<PathBuf> {
        fs::create_dir_all(workspace)
            .with_context(|| format!("failed to create workspace: {}", workspace.display()))?;

        let output_path = workspace.join("normalized.wav");
        let mut command = Command::new(ffmpeg_path);
        #[cfg(target_os = "macos")]
        if let Some(lib_dir) = ffmpeg_path.parent().and_then(|bin_dir| bin_dir.parent()).map(|root| root.join("lib")) {
            if lib_dir.exists() {
                command.env("DYLD_FALLBACK_LIBRARY_PATH", &lib_dir);
            }
        }

        let mut child = command
            .arg("-y")
            .arg("-i")
            .arg(input_path)
            .arg("-ac")
            .arg("1")
            .arg("-ar")
            .arg("16000")
            .arg("-f")
            .arg("wav")
            .arg(&output_path)
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()
            .with_context(|| format!("failed to launch ffmpeg: {}", ffmpeg_path.display()))?;

        let stderr = child.stderr.take().unwrap();
        let reader = std::io::BufReader::new(stderr);

        let mut total_duration_secs: Option<f64> = None;
        let mut last_progress = 0.0f32;

        for line in reader.lines() {
            let line = line?;

            if total_duration_secs.is_none() {
                if let Some(dur) = parse_ffmpeg_duration(&line) {
                    total_duration_secs = Some(dur);
                }
            }

            if let Some(current_time) = parse_ffmpeg_time(&line) {
                if let Some(total) = total_duration_secs {
                    let ratio = (current_time / total).clamp(0.0, 1.0) as f32;
                    if (ratio - last_progress).abs() >= 0.01 {
                        last_progress = ratio;
                        on_progress(ratio);
                    }
                }
            }
        }

        let status = child.wait().with_context(|| "ffmpeg process failed to wait")?;
        if !status.success() {
            return Err(anyhow!("ffmpeg exited with status: {}", status));
        }

        on_progress(1.0);
        Ok(output_path)
    }

    pub fn load_wav_f32(path: &Path) -> Result<Vec<f32>> {
        let mut reader =
            hound::WavReader::open(path).with_context(|| format!("failed to open {}", path.display()))?;
        let spec = reader.spec();

        if spec.channels != 1 {
            return Err(anyhow!(
                "expected mono wav, found {} channels in {}",
                spec.channels,
                path.display()
            ));
        }

        let samples = reader
            .samples::<i16>()
            .map(|sample| {
                sample
                    .map(|value| value as f32 / i16::MAX as f32)
                    .map_err(anyhow::Error::from)
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(samples)
    }
}

fn parse_ffmpeg_duration(line: &str) -> Option<f64> {
    let pos = line.find("Duration: ")?;
    let rest = &line[pos + 10..];
    let end = rest.find(|c: char| c == ',' || c == ' ')?;
    let time_str = &rest[..end];
    let parts: Vec<&str> = time_str.split(':').collect();
    if parts.len() == 3 {
        let h: f64 = parts[0].parse().ok()?;
        let m: f64 = parts[1].parse().ok()?;
        let s: f64 = parts[2].parse().ok()?;
        Some(h * 3600.0 + m * 60.0 + s)
    } else {
        None
    }
}

fn parse_ffmpeg_time(line: &str) -> Option<f64> {
    let pos = line.find("time=")?;
    let rest = &line[pos + 5..];
    let end = rest.find(|c: char| !c.is_digit(10) && c != ':' && c != '.').unwrap_or(rest.len());
    let time_str = &rest[..end];
    let parts: Vec<&str> = time_str.split(':').collect();
    if parts.len() == 3 {
        let h: f64 = parts[0].parse().ok()?;
        let m: f64 = parts[1].parse().ok()?;
        let s: f64 = parts[2].parse().ok()?;
        Some(h * 3600.0 + m * 60.0 + s)
    } else {
        None
    }
}
