// filepath: src-tauri/src/binary_manager.rs
use std::fs;
use std::path::PathBuf;
use tauri::AppHandle;
use anyhow::{Result, anyhow};
#[cfg(target_family = "unix")]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use zip::ZipArchive;
use std::io::Cursor;

use crate::storage::{self};

const YT_DLP_REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
// Bellard's QuickJS binary releases URL
const QJS_REPO_URL: &str = "https://bellard.org/quickjs/binary_releases";
// FFmpeg builds
const FFMPEG_GITHUB_API: &str = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest";
const FFMPEG_EVERMEET_BASE: &str = "https://evermeet.cx/ffmpeg";

pub fn get_ytdlp_binary_name() -> &'static str {
    if cfg!(target_os = "windows") {
        "yt-dlp.exe"
    } else if cfg!(target_os = "macos") {
        "yt-dlp_macos"
    } else {
        "yt-dlp"
    }
}

// Target name on disk (for yt-dlp usage)
pub fn get_qjs_binary_name() -> &'static str {
    if cfg!(target_os = "windows") {
        "quickjs.exe"
    } else {
        "quickjs"
    }
}

// Source name inside the zip archive (standard QuickJS naming)
fn get_qjs_source_name_in_zip() -> &'static str {
    if cfg!(target_os = "windows") {
        "qjs.exe"
    } else {
        "qjs"
    }
}

// Get base directory for all binaries
pub fn get_bin_dir(app: &AppHandle) -> Result<PathBuf> {
    let app_data = storage::get_app_data_dir(app)?;
    let bin_dir = app_data.join("bin");
    if !bin_dir.exists() {
        fs::create_dir_all(&bin_dir)?;
    }
    Ok(bin_dir)
}

pub fn get_ytdlp_path(app: &AppHandle) -> Result<PathBuf> {
    Ok(get_bin_dir(app)?.join(get_ytdlp_binary_name()))
}

pub fn get_qjs_path(app: &AppHandle) -> Result<PathBuf> {
    Ok(get_bin_dir(app)?.join(get_qjs_binary_name()))
}

pub fn get_ffmpeg_binary_name() -> &'static str {
    if cfg!(target_os = "windows") {
        "ffmpeg.exe"
    } else {
        "ffmpeg"
    }
}

pub fn get_ffmpeg_path(app: &AppHandle) -> Result<PathBuf> {
    Ok(get_bin_dir(app)?.join(get_ffmpeg_binary_name()))
}

pub fn check_binaries(app: &AppHandle) -> bool {
    let ytdlp = get_ytdlp_path(app).map(|p| p.exists()).unwrap_or(false);
    let qjs = get_qjs_path(app).map(|p| p.exists()).unwrap_or(false);
    let ffmpeg = get_ffmpeg_path(app).map(|p| p.exists()).unwrap_or(false);
    ytdlp && qjs && ffmpeg
}

// --- yt-dlp Logic ---

pub async fn download_ytdlp(app: &AppHandle) -> Result<PathBuf> {
    let binary_name = get_ytdlp_binary_name();
    let url = format!("{}/{}", YT_DLP_REPO_URL, binary_name);
    
    let response = reqwest::get(&url).await?;
    if !response.status().is_success() {
        return Err(anyhow!("下载 yt-dlp 失败：状态 {}", response.status()));
    }

    let bytes = response.bytes().await?;
    let path = get_ytdlp_path(app)?;

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }

    fs::write(&path, bytes)?;

    #[cfg(target_family = "unix")]
    {
        let mut perms = fs::metadata(&path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&path, perms)?;
    }

    #[cfg(target_os = "macos")]
    {
        // Remove quarantine attribute to allow execution on macOS
        std::process::Command::new("xattr")
            .arg("-d")
            .arg("com.apple.quarantine")
            .arg(&path)
            .output()
            .ok();
    }

    Ok(path)
}

pub async fn update_ytdlp(app: &AppHandle) -> Result<String> {
    let path = get_ytdlp_path(app)?;
    if !path.exists() {
        download_ytdlp(app).await?;
        return Ok("yt-dlp 已全新下载".to_string());
    }

    // Use built-in update for yt-dlp
    let mut cmd = std::process::Command::new(&path);
    cmd.arg("-U");
    
    #[cfg(target_os = "windows")]
    cmd.creation_flags(0x08000000);

    let output = cmd.output()?;
        
    if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            return Err(anyhow!("yt-dlp 更新失败：{}", stderr));
    }
    
    // Update settings timestamp
    let mut settings = storage::load_settings(app)?;
    settings.last_updated = Some(chrono::Utc::now());
    storage::save_settings(app, &settings)?;

    Ok("yt-dlp 已更新".to_string())
}

pub fn get_ytdlp_version(app: &AppHandle) -> Result<String> {
    let path = get_ytdlp_path(app)?;
    if !path.exists() {
        return Ok("未安装".to_string());
    }

    let mut cmd = std::process::Command::new(&path);
    cmd.arg("--version");

    #[cfg(target_os = "windows")]
    cmd.creation_flags(0x08000000);

    let output = cmd.output()?;
    
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        Ok("未知".to_string())
    }
}

// --- QuickJS Logic ---

#[derive(serde::Deserialize)]
struct LatestInfo {
    version: String,
}

pub async fn download_qjs(app: &AppHandle) -> Result<PathBuf> {
    // 1. Fetch LATEST.json to get version info (though filenames seem predictable-ish, version helps)
    // Actually, looking at the file list, Bellard uses date-based versions.
    // Format: quickjs-win-x86_64-YYYY-MM-DD.zip
    // We need to find the correct filename dynamically or parse LATEST.json if it gave filenames.
    // The LATEST.json content was {"version":"2024-01-13"} (example from prompt context).
    // So we can construct the filename: quickjs-{platform}-{version}.zip
    
    let latest_url = format!("{}/LATEST.json", QJS_REPO_URL);
    let latest_resp = reqwest::get(&latest_url).await?;
    if !latest_resp.status().is_success() {
        return Err(anyhow!("获取 QuickJS 版本信息失败"));
    }
    let latest_info: LatestInfo = latest_resp.json().await?;
    let version = latest_info.version;

    // Construct filename based on OS/Arch
    let filename = if cfg!(target_os = "windows") {
        format!("quickjs-win-x86_64-{}.zip", version)
    } else if cfg!(target_os = "macos") {
        // NOTE: Cosmo builds are universal/portable for Linux/Mac usually?
        // Based on prompt instruction: "macos download quickjs-cosmo marked file"
        // Bellard lists: quickjs-cosmo-YYYY-MM-DD.zip
        format!("quickjs-cosmo-{}.zip", version)
    } else {
        return Err(anyhow!("不支持当前操作系统的 QuickJS 自动下载"));
    };

    let download_url = format!("{}/{}", QJS_REPO_URL, filename);
    let response = reqwest::get(&download_url).await?;
    
    if !response.status().is_success() {
        return Err(anyhow!("下载 QuickJS 失败：状态 {}", response.status()));
    }

    let bytes = response.bytes().await?;
    let cursor = Cursor::new(bytes);
    let mut archive = ZipArchive::new(cursor)?;

    let bin_dir = get_bin_dir(app)?;
    
    // Extract logic: The zip from Bellard usually contains a folder or just binaries.
    // We need to find the `qjs` binary.
    // Windows zip usually has `qjs.exe`
    // Cosmo zip usually has `qjs`? Let's search for it.
    
    let source_name = get_qjs_source_name_in_zip();
    let target_name = get_qjs_binary_name(); // quickjs.exe or quickjs
    
    let mut found_exe = false;
    for i in 0..archive.len() {
        let mut file = archive.by_index(i)?;
        // Filenames in zip might be like "quickjs-win-x86_64-2024-01-13/qjs.exe"
        let name = file.name().to_string(); 
        
        // Skip directories
        if file.is_dir() {
            continue;
        }

        let filename_only = name.split('/').last().unwrap_or("");
        
        if filename_only == source_name {
            let mut out_file = fs::File::create(bin_dir.join(target_name))?;
            std::io::copy(&mut file, &mut out_file)?;
            found_exe = true;
        } else if filename_only.ends_with(".dll") {
            // Extract DLLs (needed for Windows MinGW builds, e.g. libwinpthread-1.dll)
            let mut out_file = fs::File::create(bin_dir.join(filename_only))?;
            std::io::copy(&mut file, &mut out_file)?;
        }
    }

    if !found_exe {
        return Err(anyhow!("在下载的压缩包中找不到 {}", source_name));
    }

    let final_path = get_qjs_path(app)?;

    #[cfg(target_family = "unix")]
    {
        let mut perms = fs::metadata(&final_path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&final_path, perms)?;
    }

    #[cfg(target_os = "macos")]
    {
        // Remove quarantine attribute to allow execution on macOS
        std::process::Command::new("xattr")
            .arg("-d")
            .arg("com.apple.quarantine")
            .arg(&final_path)
            .output()
            .ok();
    }

    Ok(final_path)
}

pub async fn update_qjs(app: &AppHandle) -> Result<String> {
    // QuickJS doesn't have self-update, so we just re-download
    download_qjs(app).await?;
    Ok("QuickJS 已更新/安装".to_string())
}

// --- FFmpeg Logic ---

pub async fn download_ffmpeg(app: &AppHandle) -> Result<PathBuf> {
    let bin_dir = get_bin_dir(app)?;
    if cfg!(target_os = "windows") {
        // Query GitHub releases API to find a suitable win64 zip asset
        let client = reqwest::Client::new();
        let resp = client.get(FFMPEG_GITHUB_API)
            .header(reqwest::header::USER_AGENT, "stream-capture")
            .send()
            .await?;
        if !resp.status().is_success() {
            return Err(anyhow!("无法获取 FFmpeg releases 信息"));
        }
        let json: serde_json::Value = resp.json().await?;
        let mut download_url: Option<String> = None;
        if let Some(assets) = json.get("assets").and_then(|a| a.as_array()) {
            for asset in assets {
                if let (Some(name), Some(url)) = (asset.get("name").and_then(|n| n.as_str()), asset.get("browser_download_url").and_then(|u| u.as_str())) {
                    let lname = name.to_lowercase();
                    // Prefer GPL static build, avoid shared to get a single exe
                    if lname.contains("win64") && lname.contains("gpl") && !lname.contains("shared") && lname.ends_with(".zip") {
                        download_url = Some(url.to_string());
                        break;
                    }
                }
            }
            if download_url.is_none() {
                // fallback: choose first zip asset that is NOT shared if possible
                for asset in assets {
                    if let (Some(url), Some(name)) = (asset.get("browser_download_url").and_then(|u| u.as_str()), asset.get("name").and_then(|n| n.as_str())) {
                        let lname = name.to_lowercase();
                        if lname.ends_with(".zip") && !lname.contains("shared") {
                            download_url = Some(url.to_string());
                            break;
                        }
                    }
                }
            }
        }

        let url = download_url.ok_or(anyhow!("未找到适合 Windows 的 FFmpeg 发行包"))?;
        let resp = client.get(&url).header(reqwest::header::USER_AGENT, "stream-capture").send().await?;
        if !resp.status().is_success() {
            return Err(anyhow!("下载 FFmpeg 失败"));
        }
        let bytes = resp.bytes().await?;
        let mut archive = ZipArchive::new(Cursor::new(bytes))?;

        let mut found = false;
        for i in 0..archive.len() {
            let mut file = archive.by_index(i)?;
            if file.is_dir() { continue; }
            let name = file.name().to_string();
            let filename_only = name.split('/').last().unwrap_or("");
            // Only extract the executable, ignore DLLs
            if filename_only.eq_ignore_ascii_case("ffmpeg.exe") {
                let mut out_file = fs::File::create(bin_dir.join(filename_only))?;
                std::io::copy(&mut file, &mut out_file)?;
                found = true;
            }
        }
        if !found {
            return Err(anyhow!("在 FFmpeg 压缩包中未找到 ffmpeg 可执行文件"));
        }

        let final_path = get_ffmpeg_path(app)?;
        Ok(final_path)
    } else if cfg!(target_os = "macos") {
        // Fetch listing page and find latest ffmpeg-*.zip
        let resp = reqwest::get(FFMPEG_EVERMEET_BASE).await?;
        if !resp.status().is_success() {
            return Err(anyhow!("无法获取 evermeet.ffmpeg 列表"));
        }
        let body = resp.text().await?;
        let re = regex::Regex::new(r#"href="(ffmpeg-[0-9][^"]*\.zip)"#)?;
        let mut candidates: Vec<&str> = Vec::new();
        for cap in re.captures_iter(&body) {
            if let Some(m) = cap.get(1) {
                candidates.push(m.as_str());
            }
        }
        let filename = candidates.last().ok_or(anyhow!("未在 evermeet 找到 ffmpeg zip 文件"))?;
        let url = format!("{}/{}", FFMPEG_EVERMEET_BASE, filename);
        let resp = reqwest::get(&url).await?;
        if !resp.status().is_success() {
            return Err(anyhow!("下载 FFmpeg macOS 版本失败"));
        }
        let bytes = resp.bytes().await?;
        let mut archive = ZipArchive::new(Cursor::new(bytes))?;

        let mut found = false;
        for i in 0..archive.len() {
            let mut file = archive.by_index(i)?;
            if file.is_dir() { continue; }
            let name = file.name().to_string();
            let filename_only = name.split('/').last().unwrap_or("");
            if filename_only == "ffmpeg" {
                let mut out_file = fs::File::create(bin_dir.join("ffmpeg"))?;
                std::io::copy(&mut file, &mut out_file)?;
                found = true;
            }
        }
        if !found {
            return Err(anyhow!("在 FFmpeg 压缩包中未找到 ffmpeg 可执行文件"));
        }

        let final_path = get_ffmpeg_path(app)?;
        #[cfg(target_family = "unix")]
        {
            let mut perms = fs::metadata(&final_path)?.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&final_path, perms)?;
        }
        #[cfg(target_os = "macos")]
        {
            std::process::Command::new("xattr").arg("-d").arg("com.apple.quarantine").arg(&final_path).output().ok();
        }
        Ok(final_path)
    } else {
        Err(anyhow!("当前操作系统不支持自动下载 FFmpeg"))
    }
}

pub async fn update_ffmpeg(app: &AppHandle) -> Result<String> {
    let path = get_ffmpeg_path(app)?;
    if !path.exists() {
        download_ffmpeg(app).await?;
        return Ok("FFmpeg 已安装".to_string());
    }
    // Re-download to update
    download_ffmpeg(app).await?;

    // Update settings timestamp
    let mut settings = storage::load_settings(app)?;
    settings.last_updated = Some(chrono::Utc::now());
    storage::save_settings(app, &settings)?;

    Ok("FFmpeg 已更新".to_string())
}

pub fn get_ffmpeg_version(app: &AppHandle) -> Result<String> {
    let path = get_ffmpeg_path(app)?;
    if !path.exists() {
        return Ok("未安装".to_string());
    }
    let mut cmd = std::process::Command::new(&path);
    cmd.arg("-version");
    #[cfg(target_os = "windows")]
    cmd.creation_flags(0x08000000);

    let output = cmd.output()?;
    if output.status.success() {
        // Prefer stdout, fallback to stderr if stdout empty
        let out = if !output.stdout.is_empty() {
            String::from_utf8_lossy(&output.stdout).to_string()
        } else {
            String::from_utf8_lossy(&output.stderr).to_string()
        };

        if let Some(first_line) = out.lines().next() {
            let v = first_line.trim().to_string();
            if !v.is_empty() {
                return Ok(v);
            }
        }
    }

    // If we couldn't obtain a usable version string, treat as not installed
    Ok("未安装".to_string())
}

pub fn get_qjs_version(app: &AppHandle) -> Result<String> {
    let path = get_qjs_path(app)?;
    if !path.exists() {
        return Ok("未安装".to_string());
    }
    Ok("已安装".to_string())
}

pub async fn ensure_binaries(app: &AppHandle) -> Result<()> {
    let ytdlp = get_ytdlp_path(app)?;
    if !ytdlp.exists() {
        download_ytdlp(app).await?;
    } else {
        #[cfg(target_os = "macos")]
        {
             std::process::Command::new("xattr")
            .arg("-d")
            .arg("com.apple.quarantine")
            .arg(&ytdlp)
            .output()
            .ok();
        }
    }
    
    let qjs = get_qjs_path(app)?;
    if !qjs.exists() {
        download_qjs(app).await?;
    } else {
        #[cfg(target_os = "macos")]
        {
             std::process::Command::new("xattr")
            .arg("-d")
            .arg("com.apple.quarantine")
            .arg(&qjs)
            .output()
            .ok();
        }
    }

    let ffmpeg = get_ffmpeg_path(app)?;
    if !ffmpeg.exists() {
        download_ffmpeg(app).await?;
    } else {
        #[cfg(target_os = "macos")]
        {
             std::process::Command::new("xattr")
            .arg("-d")
            .arg("com.apple.quarantine")
            .arg(&ffmpeg)
            .output()
            .ok();
        }
    }
    
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_binary_names() {
        if cfg!(target_os = "windows") {
            assert_eq!(get_ytdlp_binary_name(), "yt-dlp.exe");
            assert_eq!(get_qjs_binary_name(), "quickjs.exe");
            assert_eq!(get_qjs_source_name_in_zip(), "qjs.exe");
        } else if cfg!(target_os = "macos") {
            assert_eq!(get_ytdlp_binary_name(), "yt-dlp_macos");
            assert_eq!(get_qjs_binary_name(), "quickjs");
            assert_eq!(get_qjs_source_name_in_zip(), "qjs");
        }
    }
}
