//! Video Concat — Tauri backend.
//! A focused tool: stream-copy concat of split recordings via ffmpeg, with a
//! small command surface to the web UI.

mod concat;
mod tools;

use std::process::Child;
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
use std::thread;

use serde::Serialize;
use tauri::{AppHandle, Manager, State};

use concat::ConcatResult;

pub struct AppState {
    pub proc: Mutex<Option<Child>>,
    pub cancel: AtomicBool,
    pub ffmpeg: Option<String>,
    pub ffprobe: Option<String>,
}

impl AppState {
    fn new() -> Self {
        Self {
            proc: Mutex::new(None),
            cancel: AtomicBool::new(false),
            ffmpeg: tools::find_tool("ffmpeg"),
            ffprobe: tools::find_tool("ffprobe"),
        }
    }
}

#[derive(Serialize)]
struct ToolsInfo {
    ffmpeg: Option<String>,
    ffprobe: Option<String>,
}

#[derive(Serialize)]
struct DurationInfo {
    path: String,
    duration: f64,
}

// ── Commands ─────────────────────────────────────────────────────────────────

#[tauri::command]
fn get_tools(state: State<AppState>) -> ToolsInfo {
    ToolsInfo {
        ffmpeg: state.ffmpeg.clone(),
        ffprobe: state.ffprobe.clone(),
    }
}

/// Probe several files in parallel; returns durations in seconds.
#[tauri::command]
fn probe_durations(state: State<AppState>, paths: Vec<String>) -> Vec<DurationInfo> {
    let ffprobe = match &state.ffprobe {
        Some(p) => p.clone(),
        None => return paths.into_iter().map(|p| DurationInfo { path: p, duration: 0.0 }).collect(),
    };
    let handles: Vec<_> = paths
        .into_iter()
        .map(|p| {
            let ff = ffprobe.clone();
            thread::spawn(move || DurationInfo {
                duration: tools::probe_duration(&ff, &p),
                path: p,
            })
        })
        .collect();
    handles.into_iter().filter_map(|h| h.join().ok()).collect()
}

#[tauri::command]
fn is_video(path: String) -> bool {
    tools::is_video(&path)
}

// Concat runs ffmpeg for a long time, so it MUST run off the main thread,
// otherwise the native window stops pumping messages and Windows paints the
// app "Not responding". `spawn_blocking` keeps the UI responsive while the
// command still resolves with the final result.
#[tauri::command]
async fn start_concat(
    app: AppHandle,
    files: Vec<String>,
    output: String,
) -> Result<ConcatResult, String> {
    tauri::async_runtime::spawn_blocking(move || {
        let state = app.state::<AppState>();
        let ffmpeg = state.ffmpeg.clone().ok_or("ffmpeg not found.")?;
        let ffprobe = state.ffprobe.clone().unwrap_or_else(|| "ffprobe".into());
        concat::run(&app, &state, &ffmpeg, &ffprobe, files, output)
    })
    .await
    .map_err(|e| format!("concat task failed: {e}"))?
}

#[tauri::command]
fn cancel(state: State<AppState>) {
    state.cancel.store(true, std::sync::atomic::Ordering::SeqCst);
    if let Some(child) = state.proc.lock().unwrap().as_mut() {
        // Kill the whole tree, not just the direct child.
        tools::kill_process_tree(child.id());
        let _ = child.kill();
    }
}

#[tauri::command]
fn delete_files(paths: Vec<String>) -> usize {
    paths.iter().filter(|p| std::fs::remove_file(p).is_ok()).count()
}

/// Convert a BiliBili Live recording stem to a readable name, or null.
/// `[2026-03-14 22-46-37][纳豆nado][title]` → `2026年03月14日【直播回放】title`
#[tauri::command]
fn bililive_rename(stem: String) -> Option<String> {
    let test = stem.strip_suffix("_concat").unwrap_or(&stem).trim();
    let s = test.strip_prefix('[')?;
    let (datetime, rest) = s.split_once(']')?;
    let (date, _time) = datetime.split_once(' ')?;
    let parts: Vec<&str> = date.split('-').collect();
    if parts.len() != 3 {
        return None;
    }
    let (y, m, d) = (parts[0], parts[1], parts[2]);
    if y.len() != 4 || m.len() != 2 || d.len() != 2 {
        return None;
    }
    if !y.bytes().chain(m.bytes()).chain(d.bytes()).all(|b| b.is_ascii_digit()) {
        return None;
    }
    let rest = rest.strip_prefix('[')?;
    let (_uploader, rest) = rest.split_once(']')?;
    let title = rest.strip_prefix('[')?.strip_suffix(']')?;
    if title.is_empty() {
        return None;
    }
    Some(format!("{y}年{m}月{d}日【直播回放】{title}"))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_dialog::init())
        .setup(|app| {
            app.manage(AppState::new());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            get_tools,
            probe_durations,
            is_video,
            start_concat,
            cancel,
            delete_files,
            bililive_rename,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
