use tauri_plugin_shell::ShellExt;
use crate::StringResultExt;
use tauri_plugin_shell::process::CommandEvent;
use tauri::{AppHandle, Emitter};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tokio::sync::Semaphore;

/// 预览转码并发锁：保证同时只有一个 FFmpeg 进程在转码，避免 CPU/磁盘争抢
static PREVIEW_SEMAPHORE: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1));

/// 视频元数据（由 ffprobe 解析）
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoMetadata {
    pub width: u32,
    pub height: u32,
    pub duration: f64,
    pub fps: f64,
}

#[derive(Serialize, Clone)]
struct PhaseInfo {
    step: u8,
    total: u8,
}

/// FFmpeg concat 列表文件中的路径转义（单引号用 `'\''` 包裹转义）
pub fn escape_ffmpeg_path(path: &str) -> String {
    // 去掉 Windows UNC 前缀（\\?\）
    let path = if path.starts_with(r"\\?\") {
        &path[4..]
    } else {
        path
    };
    // concat 列表中路径被单引号包裹，需要将单引号转义为 '\''
    path.replace("'", "'\\''")
}

/// FFmpeg filter 字符串中的路径转义（用于单引号包裹的字符串内部）
fn escape_ffmpeg_filter_path(path: &str) -> String {
    // 去掉 Windows UNC 前缀（\\?\）
    let path = if path.starts_with(r"\\?\") {
        &path[4..]
    } else {
        path
    };
    // 使用正斜杠，避免反斜杠在 filter 字符串中被当作转义符
    let path = path.replace('\\', "/");
    // 单引号内需要将 ' 转义为 \'
    path.replace("'", "\\'")
}

/// 去掉 Windows UNC 前缀（\\?\），使 convertFileSrc 能正确生成 asset:// URL
fn normalize_path(path: &std::path::Path) -> String {
    let s = path.to_string_lossy().to_string();
    if s.starts_with(r"\\?\") {
        std::path::PathBuf::from(&s[4..]).to_string_lossy().to_string()
    } else {
        s
    }
}

/// 验证路径在允许的目录内，防止路径遍历攻击
/// 允许的目录：应用数据目录（app_local_data_dir）
fn validate_safe_path(path: &str) -> Result<String, String> {
    let path = std::path::Path::new(path);

    // 获取绝对路径
    let abs_path = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .map_err(|e| format!("无法获取当前目录: {}", e))?
            .join(path)
    };

    // 检查是否在允许的目录内
    let allowed_dir = crate::storage::paths::get_app_data_dir()
        .map_err(|e| format!("无法获取应用数据目录: {}", e))?;

    // 规范化路径
    let canonical = abs_path.canonicalize()
        .unwrap_or(abs_path.clone());

    // 同时规范化允许目录（Windows 上 canonicalize 会添加 \\?\ 前缀，
    // 必须与输入路径保持一致的规范化格式才能正确比较）
    let allowed_canonical = allowed_dir.canonicalize()
        .unwrap_or(allowed_dir.clone());

    // 检查是否在允许目录下
    if !canonical.starts_with(&allowed_canonical) {
        return Err(format!("路径不在允许目录内: {}", path.display()));
    }

    Ok(canonical.to_string_lossy().to_string())
}

/// 清理并验证输出路径
pub fn sanitize_output_path(path: &str) -> Result<String, String> {
    let path = std::path::Path::new(path);
    
    // 获取父目录并验证
    if let Some(parent) = path.parent() {
        validate_safe_path(&parent.to_string_lossy())?;
    }
    
    // 确保是绝对路径
    if path.is_absolute() {
        Ok(path.to_string_lossy().to_string())
    } else {
        std::env::current_dir()
            .map_err(|e| format!("无法获取当前目录: {}", e))?
            .join(path)
            .to_str()
            .map(|s| s.to_string())
            .ok_or_else(|| "无效的路径".to_string())
    }
}

/**
 * 封装 FFmpeg Sidecar 调用
 *
 * 使用 Tauri 官方 sidecar API，自动处理开发/生产环境的 sidecar 查找。
 */
pub async fn run_ffmpeg(app: &AppHandle, args: Vec<String>) -> Result<String, String> {
    run_ffmpeg_in_dir(app, args, None).await
}

pub async fn run_ffmpeg_in_dir(
    app: &AppHandle,
    args: Vec<String>,
    current_dir: Option<&std::path::Path>,
) -> Result<String, String> {
    let cmd = app.shell()
        .sidecar("ffmpeg")
        .map_err(|e| format!("Failed to find ffmpeg sidecar: {e}"))?;
    let cmd = if let Some(dir) = current_dir {
        cmd.current_dir(dir)
    } else {
        cmd
    };
    let (mut rx, child) = cmd
        .args(args)
        .spawn()
        .map_err(|e| format!("Failed to spawn ffmpeg: {e}"))?;

    let mut output = String::new();
    let mut stderr_output = String::new();

    let ffmpeg_future = async {
        while let Some(event) = rx.recv().await {
            match event {
                CommandEvent::Stdout(line) => {
                    output.push_str(&String::from_utf8_lossy(&line));
                }
                CommandEvent::Stderr(line) => {
                    let log = String::from_utf8_lossy(&line);
                    log::debug!("[ffmpeg stderr] {}", log.trim());
                    stderr_output.push_str(&log);
                    stderr_output.push('\n');

                    // 尝试解析进度: time=00:00:05.12
                    if let Some(pos) = log.find("time=") {
                        let time_part = &log[pos + 5..].split_whitespace().next().unwrap_or("");
                        if !time_part.is_empty() {
                            // 发送进度事件到前端
                            let _ = app.emit("ffmpeg-progress", time_part);
                        }
                    }
                }
                CommandEvent::Terminated(status) => {
                    match status.code {
                        Some(0) => {
                            if !stderr_output.is_empty() {
                                log::debug!("[ffmpeg] stderr:\n{}", stderr_output.trim());
                            }
                            return Ok(());
                        }
                        Some(code) => {
                            let err_detail = if stderr_output.is_empty() {
                                "(no stderr output)".to_string()
                            } else {
                                format!("stderr: {}", stderr_output.trim())
                            };
                            return Err(format!("FFmpeg exited with status {}. {}", code, err_detail));
                        }
                        None => return Err("FFmpeg terminated by signal".to_string()),
                    }
                }
                _ => {}
            }
        }
        Ok(())
    };

    // 10 分钟超时保护：防止 FFmpeg 进程挂起导致前端无限等待
    match tokio::time::timeout(std::time::Duration::from_secs(600), ffmpeg_future).await {
        Ok(Ok(())) => Ok(output),
        Ok(Err(e)) => Err(e),
        Err(_) => {
            let _ = child.kill();
            Err("FFmpeg 执行超时（超过10分钟），已强制终止".to_string())
        }
    }
}


/**
 * 标准化单个视频片段 (调整为 1080:1920, 25fps, libx264, aac 44100Hz stereo)
 */
pub async fn standardize_video(app: &AppHandle, input_path: &str, output_path: &str) -> Result<(), String> {
    // 验证路径安全
    let safe_input = validate_safe_path(input_path)?;
    let safe_output = sanitize_output_path(output_path)?;
    
    let args = vec![
        "-i".to_string(), safe_input,
        "-vf".to_string(), "fps=25,scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,format=yuv420p".to_string(),
        "-c:v".to_string(), "libx264".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-ar".to_string(), "44100".to_string(),
        "-ac".to_string(), "2".to_string(),
        "-preset".to_string(), "veryfast".to_string(),
        "-crf".to_string(), "23".to_string(),
        "-r".to_string(), "25".to_string(),
        "-y".to_string(),
        safe_output
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 拼接视频 - 快速模式 (要求编码/分辨率一致)
 */
pub async fn concat_videos_copy(app: &AppHandle, list_path: &str, output_path: &str) -> Result<(), String> {
    // 验证路径安全
    let safe_list = validate_safe_path(list_path)?;
    let safe_output = sanitize_output_path(output_path)?;
    
    let args = vec![
        "-f".to_string(), "concat".to_string(),
        "-safe".to_string(), "0".to_string(),
        "-i".to_string(), safe_list,
        "-c".to_string(), "copy".to_string(),  // 音视频流直接拷贝，保留音频
        "-y".to_string(),
        safe_output
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 拼接视频 - 兼容模式 (多步走：标准化 -> 拼接)
 */
pub async fn concat_videos_robust(app: &AppHandle, video_paths: Vec<String>, output_path: &str) -> Result<(), String> {
    // 使用输出路径的父目录作为临时目录（确保在安全目录内）
    let output_parent = std::path::Path::new(output_path)
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| std::env::temp_dir());
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis();
    
    let mut standardized_paths = Vec::new();

    // 1. 标准化每个片段（内部已验证路径）
    for (i, path) in video_paths.iter().enumerate() {
        let std_path = output_parent.join(format!("std_{}_{}.mp4", timestamp, i));
        match standardize_video(app, path, std_path.to_str().unwrap()).await {
            Ok(()) => standardized_paths.push(std_path),
            Err(e) => {
                // 清理本轮已创建的标准化文件
                for p in &standardized_paths {
                    let _ = std::fs::remove_file(p);
                }
                return Err(e);
            }
        }
    }

    // 2. 生成 concat 列表
    let list_path = output_parent.join(format!("concat_list_{}.txt", timestamp));
    let mut list_content = String::new();
    for path in &standardized_paths {
        // FFmpeg concat 列表中的路径需要用单引号包裹
        list_content.push_str(&format!("file '{}'\n", escape_ffmpeg_path(&path.to_string_lossy())));
    }
    std::fs::write(&list_path, list_content).map_err_string()?;

    // 3. 执行快速拼接（输出路径会被验证）
    let concat_res = concat_videos_copy(app, list_path.to_str().unwrap(), output_path).await;
    
    // 4. 清理临时文件
    for path in standardized_paths {
        let _ = std::fs::remove_file(path);
    }
    let _ = std::fs::remove_file(list_path);

    concat_res
}

/**
 * 音画合并 - 优化音质
 */
pub async fn add_audio_to_video(app: &AppHandle, video_path: &str, audio_path: &str, output_path: &str) -> Result<(), String> {
    // 验证路径安全
    let safe_video = validate_safe_path(video_path)?;
    let safe_audio = validate_safe_path(audio_path)?;
    let safe_output = sanitize_output_path(output_path)?;
    
    let args = vec![
        "-i".to_string(), safe_video,
        "-i".to_string(), safe_audio,
        "-c:v".to_string(), "copy".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-b:a".to_string(), "192k".to_string(), // 提高码率
        "-ar".to_string(), "44100".to_string(), // 统一采样率
        "-map".to_string(), "0:v:0".to_string(),
        "-map".to_string(), "1:a:0".to_string(),
        "-y".to_string(),
        safe_output
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 将封面图转换为一段短视频 (0.5s, 1080x1920, 25fps)
 * 带静音音频轨道，避免 concat 时丢失后续片段音频
 */
pub async fn create_cover_video(app: &AppHandle, input_path: &str, output_path: &str, duration: &str) -> Result<(), String> {
    // 验证路径安全
    let safe_input = validate_safe_path(input_path)?;
    let safe_output = sanitize_output_path(output_path)?;
    
    let args = vec![
        "-loop".to_string(), "1".to_string(),
        "-i".to_string(), safe_input,
        "-f".to_string(), "lavfi".to_string(),
        "-i".to_string(), "anullsrc=r=44100:cl=stereo".to_string(),
        "-c:v".to_string(), "libx264".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-t".to_string(), duration.to_string(),
        "-pix_fmt".to_string(), "yuv420p".to_string(),
        "-vf".to_string(), "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,setsar=1".to_string(),
        "-r".to_string(), "25".to_string(),
        "-shortest".to_string(),
        "-y".to_string(),
        safe_output
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 获取应用资源目录下的字体路径
 *
 * 开发模式下，如果资源目录找不到字体，回退到源码目录 src-tauri/fonts/
 */
fn get_fonts_dir(app: &AppHandle) -> Result<std::path::PathBuf, String> {
    use tauri::Manager;

    // 先尝试从资源目录获取（生产模式）
    if let Ok(resource_path) = app.path().resource_dir() {
        let fonts_path = resource_path.join("fonts");
        if fonts_path.exists() {
            return Ok(fonts_path);
        }
    }

    // 开发模式回退：从当前工作目录寻找源码中的 fonts 目录
    let cwd = std::env::current_dir()
        .map_err(|e| format!("Failed to get cwd: {}", e))?;

    // cwd 通常是 src-tauri 目录，所以直接找 fonts/
    let dev_fonts_path = cwd.join("fonts");
    if dev_fonts_path.exists() {
        return Ok(dev_fonts_path);
    }

    // 如果还是找不到，尝试往上一级找（支持项目根目录结构）
    if let Some(parent) = cwd.parent() {
        let dev_fonts_path_alt = parent.join("src-tauri/fonts");
        if dev_fonts_path_alt.exists() {
            return Ok(dev_fonts_path_alt);
        }
        
        // 再往上一级（如果 cwd 是 src-tauri/src）
        if let Some(grandparent) = parent.parent() {
            let dev_fonts_path_grand = grandparent.join("tauri-app/src-tauri/fonts");
            if dev_fonts_path_grand.exists() {
                return Ok(dev_fonts_path_grand);
            }
        }
    }

    Err("Could not find fonts directory in any location".to_string())
}


/**
 * 压制 ASS 字幕到视频（使用嵌入字体）
 *
 * 使用抖音美好体 Bold (DouyinSans Bold) 作为默认字体。
 * 支持可选的 PNG overlay（用于大标题/小标题，效果与前端 Canvas 预览一致）。
 */
/// 为 FFmpeg 字幕压制准备临时工作目录，将 ASS 文件和字体复制到该目录，
/// 返回 (临时目录路径, ASS 相对文件名, filter 字符串)
fn prepare_subtitle_work_dir(
    safe_ass: &str,
    app: &AppHandle,
) -> Result<(std::path::PathBuf, String, String), String> {
    let ass_path = std::path::Path::new(safe_ass);
    let ass_file_name = ass_path
        .file_name()
        .ok_or("无效的 ASS 文件路径")?
        .to_string_lossy()
        .to_string();

    // 创建临时目录（系统临时目录下）
    let temp_dir = std::env::temp_dir().join(format!(
        "meijiaka_subtitle_{}",
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis()
    ));
    std::fs::create_dir_all(&temp_dir)
        .map_err(|e| format!("创建临时目录失败: {}", e))?;

    // 复制 ASS 文件到临时目录
    let temp_ass = temp_dir.join(&ass_file_name);
    if let Err(e) = std::fs::copy(safe_ass, &temp_ass) {
        let _ = std::fs::remove_dir_all(&temp_dir);
        return Err(format!("复制 ASS 文件到临时目录失败: {}", e));
    }

    // 复制字体文件到临时目录
    if let Ok(fonts_dir) = get_fonts_dir(app) {
        if let Ok(entries) = std::fs::read_dir(&fonts_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if let Some(ext) = path.extension() {
                    let ext = ext.to_string_lossy().to_lowercase();
                    if ext == "ttf" || ext == "otf" || ext == "woff" || ext == "woff2" {
                        let _ = std::fs::copy(&path, temp_dir.join(path.file_name().unwrap_or_default()));
                    }
                }
            }
        }
    }

    // 使用相对路径的 filter 字符串（无 C: 盘符问题）
    let filter = format!("subtitles='{}'", escape_ffmpeg_filter_path(&ass_file_name));

    Ok((temp_dir, ass_file_name, filter))
}

pub async fn burn_ass_subtitle(
    app: &AppHandle,
    video_path: &str,
    ass_path: &str,
    output_path: &str,
    overlay_image: Option<&str>,
) -> Result<(), String> {
    // 输入路径验证：HTTP URL 直接传递，本地文件需要安全检查
    let safe_video = if video_path.starts_with("http://") || video_path.starts_with("https://") {
        video_path.to_string()
    } else {
        validate_safe_path(video_path)?
    };
    let safe_ass = validate_safe_path(ass_path)?;
    let safe_output = sanitize_output_path(output_path)?;

    // 准备临时工作目录（ASS + 字体），使用相对路径避开 Windows C: 盘符问题
    let (work_dir, _ass_name, subtitle_filter) = prepare_subtitle_work_dir(&safe_ass, app)?;

    // 如果有 overlay 图片，先 overlay 再 burn 字幕（两步避免 filter_complex 音频映射问题）
    if let Some(img_path) = overlay_image {
        let safe_img = validate_safe_path(img_path)?;
        let temp_output = format!("{}.tmp_overlay.mp4", safe_output);

        // Step 1: overlay PNG 到视频
        let overlay_filter = format!("[0:v][1:v]overlay=0:0:format=auto");
        let overlay_args = vec![
            "-i".to_string(), safe_video.clone(),
            "-i".to_string(), safe_img,
            "-filter_complex".to_string(), overlay_filter,
            "-c:v".to_string(), "libx264".to_string(),
            "-preset".to_string(), "veryfast".to_string(),
            "-crf".to_string(), "18".to_string(),
            "-pix_fmt".to_string(), "yuv420p".to_string(),
            "-c:a".to_string(), "copy".to_string(),
            "-y".to_string(),
            temp_output.clone(),
        ];

        let _ = app.emit("ffmpeg-phase-start", PhaseInfo { step: 1, total: 2 });
        if let Err(e) = run_ffmpeg(app, overlay_args).await {
            let _ = std::fs::remove_dir_all(&work_dir);
            let _ = std::fs::remove_file(&temp_output);
            return Err(format!("Overlay 图片失败: {}", e));
        }

        // Step 2: burn ASS 字幕
        let burn_args = vec![
            "-i".to_string(), temp_output.clone(),
            "-vf".to_string(), subtitle_filter.clone(),
            "-c:v".to_string(), "libx264".to_string(),
            "-preset".to_string(), "medium".to_string(),
            "-crf".to_string(), "23".to_string(),
            "-pix_fmt".to_string(), "yuv420p".to_string(),
            "-c:a".to_string(), "copy".to_string(),
            "-y".to_string(),
            safe_output.clone(),
        ];

        let _ = app.emit("ffmpeg-phase-start", PhaseInfo { step: 2, total: 2 });
        let result = run_ffmpeg_in_dir(app, burn_args, Some(&work_dir)).await;
        let _ = std::fs::remove_dir_all(&work_dir);
        let _ = std::fs::remove_file(&temp_output);
        return result.map(|_| ());
    }

    // 无 overlay，单步 burn 字幕
    let _ = app.emit("ffmpeg-phase-start", PhaseInfo { step: 1, total: 1 });

    let args = vec![
        "-i".to_string(), safe_video,
        "-vf".to_string(), subtitle_filter,
        "-c:v".to_string(), "libx264".to_string(),
        "-preset".to_string(), "medium".to_string(),
        "-crf".to_string(), "23".to_string(),
        "-pix_fmt".to_string(), "yuv420p".to_string(),
        "-c:a".to_string(), "copy".to_string(),
        "-y".to_string(),
        safe_output,
    ];

    let result = run_ffmpeg_in_dir(app, args, Some(&work_dir)).await;
    let _ = std::fs::remove_dir_all(&work_dir);
    result.map(|_| ())
}



/**
 * 混合背景音乐到视频（保留原视频音频）
 *
 * 使用 FFmpeg amix 滤镜将 BGM 与原视频音频混合。
 * 如果 BGM 短于视频，会自动循环；如果长于视频，会截断到视频长度。
 *
 * BGM 路径必须是本地文件（由前端下载到 bgm_cache 后传入）。
 */
pub async fn mix_bgm_to_video(
    app: &AppHandle,
    video_path: &str,
    bgm_path: &str,
    output_path: &str,
    video_volume: f64,
    bgm_volume: f64,
) -> Result<(), String> {
    let safe_video = validate_safe_path(video_path)?;
    // BGM 可以是用户通过系统文件选择器选取的任意本地文件，
    // 验证文件存在且不含路径遍历字符
    let safe_bgm = if !bgm_path.contains("..") && std::path::Path::new(bgm_path).exists() {
        bgm_path.to_string()
    } else {
        return Err(format!("BGM 文件不存在或路径非法: {}", bgm_path));
    };
    let safe_output = sanitize_output_path(output_path)?;

    // 构建 filter_complex:
    // [0:a]volume=1.0[a0];                — 原视频音频调整音量
    // [1:a]volume=0.25,aloop=loop=-1:size=2e+09[bgm]; — BGM 调整音量后无限循环
    // [a0][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout] — 混合，以第一个输入时长为准
    let filter_complex = format!(
        "[0:a]volume={:.2}[a0];[1:a]volume={:.2},aloop=loop=-1:size=2e+09[bgm];[a0][bgm]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[aout]",
        video_volume, bgm_volume
    );

    let args = vec![
        "-i".to_string(), safe_video,
        "-i".to_string(), safe_bgm,
        "-filter_complex".to_string(), filter_complex,
        "-map".to_string(), "0:v:0".to_string(),
        "-map".to_string(), "[aout]".to_string(),
        "-c:v".to_string(), "copy".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-b:a".to_string(), "192k".to_string(),
        "-ar".to_string(), "44100".to_string(),
        "-ac".to_string(), "2".to_string(),
        "-y".to_string(),
        safe_output,
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 音频替换 — 用配音音频替换视频中的原音
 *
 * 适用于配音替换场景：保留原视频画面，替换为 TTS/复刻音频。
 */
pub async fn replace_audio_track(
    app: &AppHandle,
    video_path: &str,
    audio_path: &str,
    output_path: &str,
) -> Result<(), String> {
    // 验证路径安全
    let safe_video = validate_safe_path(video_path)?;
    let safe_audio = validate_safe_path(audio_path)?;
    let safe_output = sanitize_output_path(output_path)?;
    
    let args = vec![
        "-i".to_string(), safe_video,
        "-i".to_string(), safe_audio,
        // 视频流：直接复制（不重新编码）
        "-c:v".to_string(), "copy".to_string(),
        // 音频流：转码为 AAC
        "-c:a".to_string(), "aac".to_string(),
        "-b:a".to_string(), "192k".to_string(),
        "-ar".to_string(), "44100".to_string(),
        "-ac".to_string(), "2".to_string(),
        // 只保留第一个视频流和第一个音频流
        "-map".to_string(), "0:v:0".to_string(),
        "-map".to_string(), "1:a:0".to_string(),
        "-y".to_string(),
        safe_output,
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 通过 ffprobe 获取视频元数据（分辨率、时长、帧率）
 *
 * 跨平台可靠，不依赖浏览器视频解码器。
 */
pub async fn get_video_metadata(app: &AppHandle, input_path: &str) -> Result<VideoMetadata, String> {
    // URL 直接传递；本地文件只检查是否存在（用户通过系统文件选择器选取，已获授权）
    let safe_input = if input_path.starts_with("http://") || input_path.starts_with("https://") {
        input_path.to_string()
    } else if std::path::Path::new(input_path).exists() {
        input_path.to_string()
    } else {
        return Err(format!("输入文件不存在: {}", input_path));
    };

    let args = vec![
        "-v".to_string(), "error".to_string(),
        "-select_streams".to_string(), "v:0".to_string(),
        "-show_entries".to_string(), "stream=width,height,duration,r_frame_rate:format=duration".to_string(),
        "-of".to_string(), "json".to_string(),
        safe_input,
    ];

    let (mut rx, child) = app.shell()
        .sidecar("ffprobe")
        .map_err(|e| format!("Failed to find ffprobe sidecar: {e}"))?
        .args(args)
        .spawn()
        .map_err(|e| format!("Failed to spawn ffprobe: {e}"))?;

    let mut stdout = String::new();
    let mut stderr = String::new();

    let probe_future = async {
        while let Some(event) = rx.recv().await {
            match event {
                CommandEvent::Stdout(line) => {
                    stdout.push_str(&String::from_utf8_lossy(&line));
                }
                CommandEvent::Stderr(line) => {
                    stderr.push_str(&String::from_utf8_lossy(&line));
                }
                CommandEvent::Terminated(status) => {
                    match status.code {
                        Some(0) => return Ok(()),
                        Some(code) => {
                            return Err(format!("ffprobe exited with status {}. stderr: {}", code, stderr.trim()));
                        }
                        None => return Err("ffprobe terminated by signal".to_string()),
                    }
                }
                _ => {}
            }
        }
        Ok(())
    };

    match tokio::time::timeout(std::time::Duration::from_secs(30), probe_future).await {
        Ok(Ok(())) => {}
        Ok(Err(e)) => return Err(e),
        Err(_) => {
            let _ = child.kill();
            return Err("ffprobe 执行超时（超过30秒），已强制终止".to_string());
        }
    }

    // 解析 JSON 输出
    let parsed: serde_json::Value = serde_json::from_str(&stdout)
        .map_err(|e| format!("ffprobe JSON 解析失败: {}. raw: {}", e, &stdout))?;

    let stream = parsed.get("streams")
        .and_then(|s| s.as_array())
        .and_then(|arr| arr.first())
        .ok_or_else(|| format!("ffprobe 未返回视频流信息: {}", &stdout))?;

    let width = stream.get("width")
        .and_then(|v| v.as_u64())
        .ok_or_else(|| "无法解析视频宽度".to_string())? as u32;

    let height = stream.get("height")
        .and_then(|v| v.as_u64())
        .ok_or_else(|| "无法解析视频高度".to_string())? as u32;

    // duration 可能是字符串或数字；某些格式（如 MPEG-TS）stream duration 为 N/A，需从 format 回退
    let stream_duration = stream.get("duration")
        .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())));

    let format_duration = parsed.get("format")
        .and_then(|f| f.get("duration"))
        .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())));

    let duration = stream_duration.or(format_duration).unwrap_or(0.0);

    // 帧率格式为 "25/1" 或 "30000/1001"
    let fps = stream.get("r_frame_rate")
        .and_then(|v| v.as_str())
        .and_then(|s| {
            let parts: Vec<&str> = s.split('/').collect();
            if parts.len() == 2 {
                let num: f64 = parts[0].parse().ok()?;
                let den: f64 = parts[1].parse().ok()?;
                if den != 0.0 { Some(num / den) } else { None }
            } else {
                s.parse().ok()
            }
        })
        .unwrap_or(0.0);

    Ok(VideoMetadata { width, height, duration, fps })
}


/**
 * 裁剪视频片段（支持本地文件和 HTTP URL）
 *
 * 从起始时间裁剪指定时长，同时标准化输出格式（1080x1920, 25fps, libx264, aac）。
 * 适用于从人物形象素材或空镜素材中提取指定时长的片段。
 */
pub async fn clip_video(
    app: &AppHandle,
    input: &str,
    start: f64,
    duration: f64,
    output_path: &str,
) -> Result<(), String> {
    let safe_output = sanitize_output_path(output_path)?;

    // 输入路径验证：URL 直接传递，本地文件只需验证存在性
    // （本地文件来自用户通过系统文件选择器主动选取，已获用户授权）
    let safe_input = if input.starts_with("http://") || input.starts_with("https://") {
        input.to_string()
    } else if std::path::Path::new(input).exists() {
        input.to_string()
    } else {
        return Err(format!("输入文件不存在: {}", input));
    };

    let start_str = format!("{:.3}", start);
    let duration_str = format!("{:.3}", duration);

    let args = vec![
        "-ss".to_string(), start_str,
        "-t".to_string(), duration_str,
        "-i".to_string(), safe_input,
        "-vf".to_string(), "fps=25,scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,format=yuv420p".to_string(),
        "-c:v".to_string(), "libx264".to_string(),
        "-preset".to_string(), "veryfast".to_string(),
        "-crf".to_string(), "23".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-ar".to_string(), "44100".to_string(),
        "-ac".to_string(), "2".to_string(),
        "-r".to_string(), "25".to_string(),
        "-pix_fmt".to_string(), "yuv420p".to_string(),
        "-avoid_negative_ts".to_string(), "make_zero".to_string(),
        "-y".to_string(),
        safe_output,
    ];

    run_ffmpeg(app, args).await.map(|_| ())
}

/**
 * 截取音频片段
 *
 * 从指定起始时间截取指定时长的音频，输出为 MP3 格式。
 */
pub async fn extract_audio_segment(
    app: &AppHandle,
    input_path: &str,
    start: f64,
    duration: f64,
    output_path: &str,
) -> Result<(), String> {
    let safe_input = validate_safe_path(input_path)?;
    let safe_output = sanitize_output_path(output_path)?;

    let start_str = format!("{:.3}", start);
    let duration_str = format!("{:.3}", duration);

    let args = vec![
        "-ss".to_string(), start_str,
        "-t".to_string(), duration_str,
        "-i".to_string(), safe_input,
        "-c:a".to_string(), "libmp3lame".to_string(),
        "-b:a".to_string(), "192k".to_string(),
        "-ar".to_string(), "44100".to_string(),
        "-ac".to_string(), "2".to_string(),
        "-vn".to_string(), // 无视频
        "-y".to_string(),
        safe_output,
    ];
    run_ffmpeg(app, args).await.map(|_| ())
}



/**
 * 解析 ffprobe 返回的帧率字符串（如 "30000/1001" 或 "30/1"）
 */
fn parse_frame_rate(s: &str) -> f64 {
    if s.is_empty() {
        return 0.0;
    }
    if let Some(idx) = s.find('/') {
        let num: f64 = s[..idx].parse().unwrap_or(0.0);
        let den: f64 = s[idx + 1..].parse().unwrap_or(1.0);
        if den > 0.0 {
            return num / den;
        }
    }
    s.parse().unwrap_or(0.0)
}

/**
 * 为预览转码视频（统一为浏览器兼容格式）
 *
 * 快速通过检测：只有满足全部兼容性条件的视频才直接返回原路径，
 * 任一不满足则强制转码为 H.264 Baseline + YUV420p 540p 代理。
 *
 * 检测项（基于主流浏览器/WebView 兼容性最佳实践）：
 * - codec: H.264
 * - pix_fmt: YUV420p
 * - profile: Baseline / Constrained Baseline / Main
 * - level: <= 4.1
 * - color_range: limited (tv) — macOS VideoToolbox 对 full range 支持不完整
 * - has_b_frames: 0 — macOS WKWebView 对 B-frames 支持不完整
 * - refs: <= 4 — 参考帧数过多可能导致解码失败
 * - r_frame_rate: <= 60fps — 高帧率可能异常
 * - width/height: <= 1920x1920
 *
 * 转码参数参考 Mux/Transloadit 主流推荐，针对预览场景优化：
 * - preset ultrafast: 编码速度优先（预览不追求极致压缩率）
 * - crf 28: 预览画质够用
 * - g 30 / keyint_min 30: 每秒一个关键帧，seek 更精确
 * - 保留音频（人物形象视频有配音）
 * - +faststart: moov atom 前置，快速开始播放
 *
 * 并发控制：tokio Semaphore(1)，保证同时只有一个 FFmpeg 进程在跑，
 * 避免多视频同时预览时 CPU/磁盘争抢。
 *
 * 转码结果按（文件路径 + 大小 + 修改时间）缓存，避免重复处理。
 */
pub async fn transcode_for_preview(app: &AppHandle, input_path: &str) -> Result<String, String> {
    // 网络视频：先下载到本地缓存，再统一走转码/预览流程
    let path_str = if input_path.starts_with("http://") || input_path.starts_with("https://") {
        let app_data_dir = crate::storage::paths::get_app_data_dir()
            .map_err(|e| format!("无法获取应用数据目录: {}", e))?;
        let cache_dir = app_data_dir.join("video_cache");
        std::fs::create_dir_all(&cache_dir)
            .map_err(|e| format!("无法创建缓存目录: {}", e))?;

        let url_hash = {
            use std::collections::hash_map::DefaultHasher;
            use std::hash::{Hash, Hasher};
            let mut hasher = DefaultHasher::new();
            input_path.hash(&mut hasher);
            format!("{:016x}", hasher.finish())
        };

        let download_path = cache_dir.join(format!("download_{}.mp4", url_hash));

        if !download_path.exists() {
            log::info!("[transcode_for_preview] 下载网络视频: {} -> {}", input_path, download_path.display());
            let client = reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(60))
                .build()
                .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
            let response = client.get(input_path)
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
                .send()
                .await
                .map_err(|e| format!("下载网络视频失败: {}", e))?
                .error_for_status()
                .map_err(|e| format!("下载网络视频返回错误: {}", e))?;
            let bytes = response.bytes()
                .await
                .map_err(|e| format!("读取网络视频数据失败: {}", e))?;
            // 用 spawn_blocking 避免同步写文件阻塞 tokio 运行时
            let download_path_clone = download_path.clone();
            let bytes_clone = bytes.clone();
            let write_result = tokio::task::spawn_blocking(move || {
                std::fs::write(&download_path_clone, &bytes_clone)
            })
            .await;
            if let Err(e) = write_result {
                let _ = std::fs::remove_file(&download_path);
                return Err(format!("写入任务失败: {}", e));
            }
            if let Err(e) = write_result.unwrap() {
                let _ = std::fs::remove_file(&download_path);
                return Err(format!("保存网络视频失败: {}", e));
            }
            if let Ok(file) = std::fs::File::open(&download_path) {
                let _ = file.sync_all();
            }
            log::info!("[transcode_for_preview] 下载完成: {} bytes", bytes.len());
        }

        download_path.to_string_lossy().to_string()
    } else {
        let input = std::path::Path::new(input_path);
        if !input.exists() {
            return Err(format!("输入文件不存在: {}", input_path));
        }
        input.canonicalize()
            .unwrap_or(input.to_path_buf())
            .to_string_lossy()
            .to_string()
    };

    // ============================================================
    // 1. 快速通过检测：用 ffprobe 全面检查视频兼容性
    // ============================================================
    let probe_args = vec![
        "-v".to_string(), "error".to_string(),
        "-select_streams".to_string(), "v:0".to_string(),
        "-show_entries".to_string(), "stream=codec_name,pix_fmt,width,height,profile,level,color_range,has_b_frames,refs,r_frame_rate".to_string(),
        "-of".to_string(), "json".to_string(),
        path_str.clone(),
    ];

    let probe_result = app.shell().sidecar("ffprobe")
        .and_then(|s| s.args(probe_args).spawn());
    if let Ok((mut rx, child)) = probe_result {
        let mut stdout = String::new();
        let probe_future = async {
            while let Some(event) = rx.recv().await {
                match event {
                    CommandEvent::Stdout(line) => stdout.push_str(&String::from_utf8_lossy(&line)),
                    CommandEvent::Terminated(status) if status.code == Some(0) => return Ok(()),
                    _ => {}
                }
            }
            Ok::<(), ()>(())
        };
        match tokio::time::timeout(std::time::Duration::from_secs(3), probe_future).await {
            Ok(Ok(())) => {
                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stdout) {
                    if let Some(stream) = parsed.get("streams").and_then(|s| s.as_array()).and_then(|a| a.first()) {
                        let codec = stream.get("codec_name").and_then(|v| v.as_str()).unwrap_or("");
                        let pix_fmt = stream.get("pix_fmt").and_then(|v| v.as_str()).unwrap_or("");
                        let profile = stream.get("profile").and_then(|v| v.as_str()).unwrap_or("");
                        let level = stream.get("level").and_then(|v| v.as_i64()).unwrap_or(0);
                        let color_range = stream.get("color_range").and_then(|v| v.as_str()).unwrap_or("tv");
                        let has_b_frames = stream.get("has_b_frames").and_then(|v| v.as_i64()).unwrap_or(0);
                        let refs = stream.get("refs").and_then(|v| v.as_i64()).unwrap_or(0);
                        let r_frame_rate = stream.get("r_frame_rate").and_then(|v| v.as_str()).unwrap_or("");
                        let width = stream.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
                        let height = stream.get("height").and_then(|v| v.as_u64()).unwrap_or(0);

                        let is_safe_profile = profile == "Baseline"
                            || profile == "Constrained Baseline"
                            || profile == "Main";
                        let is_limited_range = color_range == "tv" || color_range.is_empty();
                        let no_b_frames = has_b_frames == 0;
                        let refs_ok = refs <= 4;
                        let level_ok = level <= 41;
                        let fps = parse_frame_rate(r_frame_rate);
                        let fps_ok = fps > 0.0 && fps <= 60.0;
                        let resolution_ok = width <= 1920 && height <= 1920;

                        let can_skip = codec == "h264"
                            && pix_fmt == "yuv420p"
                            && is_safe_profile
                            && is_limited_range
                            && no_b_frames
                            && refs_ok
                            && level_ok
                            && fps_ok
                            && resolution_ok;

                        if can_skip {
                            return Ok(normalize_path(&std::path::Path::new(&path_str)));
                        }
                    }
                }
            }
            _ => {
                // 超时或异常：强制结束 ffprobe，避免僵尸进程
                let _ = child.kill();
            }
        }
    }

    // ============================================================
    // 2. 缓存检查
    // ============================================================
    let metadata = std::fs::metadata(&path_str)
        .map_err(|e| format!("无法读取文件元数据: {}", e))?;
    let mtime = metadata.modified()
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .unwrap_or_default()
        .as_secs();
    let file_size = metadata.len();

    let path_hash = {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        let mut hasher = DefaultHasher::new();
        path_str.hash(&mut hasher);
        format!("{:016x}", hasher.finish())
    };

    let app_data_dir = crate::storage::paths::get_app_data_dir()
        .map_err(|e| format!("无法获取应用数据目录: {}", e))?;
    let cache_dir = app_data_dir.join("video_cache");
    std::fs::create_dir_all(&cache_dir)
        .map_err(|e| format!("无法创建缓存目录: {}", e))?;
    // 缓存版本号：每次修改转码参数后递增，确保旧缓存自动失效
    const CACHE_VERSION: &str = "v3";
    let cache_path = cache_dir.join(format!("preview_{}_{}_{}_{}.mp4", path_hash, file_size, mtime, CACHE_VERSION));

    if cache_path.exists() {
        let cache_meta = std::fs::metadata(&cache_path)
            .map_err(|e| format!("无法读取缓存文件: {}", e))?;
        if cache_meta.len() > 1024 {
            return Ok(cache_path.to_string_lossy().to_string());
        }
    }

    // ============================================================
    // 3. FFmpeg 转码（带并发锁）
    // ============================================================
    let _permit = PREVIEW_SEMAPHORE.acquire().await
        .map_err(|e| format!("无法获取转码锁: {}", e))?;

    // 先写入临时文件，避免 FFmpeg 写入过程中被 WebKit 读取到不完整文件
    let tmp_path = cache_path.with_extension("mp4.tmp");

    let args = vec![
        "-i".to_string(), path_str.clone(),
        "-map".to_string(), "0:v:0".to_string(),
        "-map".to_string(), "0:a:0?".to_string(),
        "-c:v".to_string(), "libx264".to_string(),
        "-profile:v".to_string(), "baseline".to_string(),
        "-level".to_string(), "3.0".to_string(),
        "-pix_fmt".to_string(), "yuv420p".to_string(),
        "-preset".to_string(), "ultrafast".to_string(),
        "-crf".to_string(), "23".to_string(),
        "-vf".to_string(), "scale=540:-2:force_original_aspect_ratio=decrease".to_string(),
        "-c:a".to_string(), "aac".to_string(),
        "-b:a".to_string(), "128k".to_string(),
        "-movflags".to_string(), "+faststart".to_string(),
        "-f".to_string(), "mp4".to_string(),
        "-y".to_string(),
        tmp_path.to_string_lossy().to_string(),
    ];

    run_ffmpeg(app, args).await?;

    // 转码完成后验证临时文件大小
    let output_meta = std::fs::metadata(&tmp_path)
        .map_err(|e| format!("转码后无法读取临时文件: {}", e))?;
    if output_meta.len() < 1024 {
        let _ = std::fs::remove_file(&tmp_path);
        return Err(format!("FFmpeg 转码输出文件过小: {} bytes", output_meta.len()));
    }
    log::info!("[transcode_for_preview] 转码完成: {} ({} bytes)", tmp_path.display(), output_meta.len());

    // 原子重命名：确保 WebKit 只读到完整文件
    std::fs::rename(&tmp_path, &cache_path)
        .map_err(|e| format!("重命名缓存文件失败: {}", e))?;

    // 强制刷新文件系统缓存（文件数据 + 目录项），避免 WebKit asset:// 首次读取到不完整数据
    if let Ok(file) = std::fs::File::open(&cache_path) {
        let _ = file.sync_all();
    }
    if let Some(parent) = cache_path.parent() {
        if let Ok(dir) = std::fs::OpenOptions::new().read(true).open(parent) {
            let _ = dir.sync_all();
        }
    }

    Ok(cache_path.to_string_lossy().to_string())
}
