//! ffmpeg concat-demuxer worker with honest, real-time progress.
//!
//! Mirrors the proven logic from the Python original: stream-copy (`-c copy`),
//! `+faststart`, and a progress bar that is capped at 99 % until ffmpeg emits
//! `progress=end`, at which point it snaps to 100 % and shows "Finalising…".

use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use serde::Serialize;
use tauri::{AppHandle, Emitter};

use crate::tools::{fmt_dur, hide_window, probe_duration};
use crate::AppState;

#[derive(Clone, Serialize)]
pub struct ConcatProgress {
    pub pct: f64,
    pub status: String,
    pub elapsed: f64,
    pub eta: f64,
    pub speed: f64,
}

#[derive(Clone, Serialize)]
pub struct ConcatResult {
    pub output: String,
    pub size_mb: f64,
    pub elapsed: f64,
}

fn emit(app: &AppHandle, p: ConcatProgress) {
    let _ = app.emit("concat-progress", p);
}

/// Build the ffmpeg concat list file (UTF-8, forward slashes, escaped quotes).
fn write_list(files: &[String], out_dir: &Path) -> Result<std::path::PathBuf, String> {
    let list_path = out_dir.join(format!(".stream_studio_{}.txt", std::process::id()));
    let mut body = String::new();
    for f in files {
        let abs = std::fs::canonicalize(f).unwrap_or_else(|_| Path::new(f).to_path_buf());
        let s = abs
            .to_string_lossy()
            .replace('\\', "/")
            .replace("'", r"'\''")
            // strip the Windows extended-length prefixes canonicalize() adds.
            // UNC paths come back as \\?\UNC\server\share -> //?/UNC/server/share,
            // which must collapse to //server/share (a real network path), not the
            // bare "UNC/server/share" that a blanket strip would leave behind.
            .replace("//?/UNC/", "//")
            .replace("//?/", "");
        body.push_str(&format!("file '{s}'\n"));
    }
    std::fs::write(&list_path, body).map_err(|e| format!("Could not write list file: {e}"))?;
    Ok(list_path)
}

pub fn run(
    app: &AppHandle,
    state: &AppState,
    ffmpeg: &str,
    ffprobe: &str,
    files: Vec<String>,
    output: String,
) -> Result<ConcatResult, String> {
    state.cancel.store(false, std::sync::atomic::Ordering::SeqCst);

    let out_path = Path::new(&output);
    let out_dir = out_path.parent().ok_or("Invalid output path")?.to_path_buf();

    emit(app, ConcatProgress { pct: 0.0, status: "Scanning durations…".into(), elapsed: 0.0, eta: -1.0, speed: 0.0 });
    let total_secs: f64 = files.iter().map(|f| probe_duration(ffprobe, f)).sum();

    let list_path = write_list(&files, &out_dir)?;

    let mut cmd = Command::new(ffmpeg);
    cmd.args([
        "-y",
        "-f", "concat", "-safe", "0",
        "-i", &list_path.to_string_lossy(),
        "-c:v", "copy", "-c:a", "copy",
        "-movflags", "+faststart",
        "-nostdin", "-hide_banner",
        "-loglevel", "error",
        "-progress", "pipe:1",
        &output,
    ]);
    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
    hide_window(&mut cmd);

    let mut child = cmd.spawn().map_err(|e| format!("Failed to launch ffmpeg: {e}"))?;
    let stdout = child.stdout.take().ok_or("No ffmpeg stdout")?;
    let stderr = child.stderr.take();

    // Hand the child to shared state so cancel() can kill it.
    *state.proc.lock().unwrap() = Some(child);

    emit(app, ConcatProgress { pct: 0.0, status: "Concatenating…".into(), elapsed: 0.0, eta: -1.0, speed: 0.0 });
    let t0 = Instant::now();
    let mut cur_us: i64 = 0;

    // When ffmpeg finishes the copy it still has to relocate the moov atom for
    // +faststart (a full-file rewrite that can take a while on large outputs).
    // We hold the bar at 99 % and run this heartbeat so "Finalising…" stays
    // visibly alive instead of looking frozen, and 100 % means *actually done*.
    let fin = Arc::new(AtomicBool::new(false));
    let mut heartbeat: Option<thread::JoinHandle<()>> = None;

    for line in BufReader::new(stdout).lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        let line = line.trim();

        if let Some(v) = line.strip_prefix("out_time_us=").or_else(|| line.strip_prefix("out_time_ms=")) {
            if let Ok(n) = v.parse::<i64>() {
                cur_us = n; // ffmpeg reports microseconds under both keys
            }
        } else if line == "progress=end" {
            // Copy is done; the +faststart rewrite happens now, during which
            // ffmpeg emits nothing. Spawn a heartbeat to keep the UI honest.
            if heartbeat.is_none() {
                let appc = app.clone();
                let finc = fin.clone();
                heartbeat = Some(thread::spawn(move || {
                    while !finc.load(Ordering::SeqCst) {
                        let elapsed = t0.elapsed().as_secs_f64();
                        emit(&appc, ConcatProgress {
                            pct: 0.99,
                            status: "Finalising output file…".into(),
                            elapsed,
                            eta: -1.0,
                            speed: 0.0,
                        });
                        thread::sleep(Duration::from_millis(700));
                    }
                }));
            }
        } else if line.starts_with("progress=continue") {
            if total_secs > 0.0 && cur_us > 0 {
                let played = cur_us as f64 / 1_000_000.0;
                let pct = (played / total_secs).clamp(0.0, 0.99);
                let elapsed = t0.elapsed().as_secs_f64();
                let speed = played / elapsed.max(1e-9);
                let eta = (total_secs - played) / speed.max(1e-9);
                emit(app, ConcatProgress {
                    pct,
                    status: "Concatenating…".into(),
                    elapsed,
                    eta,
                    speed,
                });
            }
        }
    }

    // ffmpeg's stdout has closed — the +faststart rewrite is done too. Stop the
    // finalisation heartbeat before we reclaim the child and report 100 %.
    fin.store(true, Ordering::SeqCst);
    if let Some(h) = heartbeat.take() {
        let _ = h.join();
    }

    // Reclaim the child and finish.
    let mut child = state.proc.lock().unwrap().take().ok_or("Process vanished")?;
    let status = child.wait().map_err(|e| format!("ffmpeg wait failed: {e}"))?;
    let mut stderr_text = String::new();
    if let Some(mut se) = stderr {
        use std::io::Read;
        let _ = se.read_to_string(&mut stderr_text);
    }

    let _ = std::fs::remove_file(&list_path);

    if state.cancel.load(std::sync::atomic::Ordering::SeqCst) {
        let _ = std::fs::remove_file(&output);
        return Err("__cancelled__".into());
    }

    if !status.success() {
        let _ = std::fs::remove_file(&output);
        let msg = stderr_text.trim();
        return Err(if msg.is_empty() { "Unknown ffmpeg error.".into() } else { msg.to_string() });
    }

    let elapsed = t0.elapsed().as_secs_f64();
    let size_mb = std::fs::metadata(&output).map(|m| m.len() as f64 / 1_048_576.0).unwrap_or(0.0);
    emit(app, ConcatProgress {
        pct: 1.0,
        status: format!("Done — {} · {:.1} MB", fmt_dur(elapsed), size_mb),
        elapsed,
        eta: 0.0,
        speed: 0.0,
    });

    Ok(ConcatResult { output, size_mb, elapsed })
}
