// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use tauri::{Manager, Emitter};
use tokio::process::Command as AsyncCommand;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use regex::Regex;
use serde::Serialize;
use ts_rs::TS;
use uuid::Uuid;
use tauri_plugin_deep_link::DeepLinkExt;

#[derive(Serialize, TS)]
#[ts(export, export_to = "../../src/bindings/")]
pub struct MetadataResponse {
    filename: String,
    size: String,
    #[ts(type = "number")]
    size_bytes: u64,
}

fn get_binary_name(base: &str) -> String {
    if cfg!(target_os = "windows") {
        format!("{}.exe", base)
    } else {
        base.to_string()
    }
}


#[tauri::command]
async fn fetch_metadata(url: String, user_agent: Option<String>, username: Option<String>, password: Option<String>) -> Result<MetadataResponse, String> {
    let mut builder = reqwest::Client::builder();
    if let Some(ua) = user_agent {
        if !ua.is_empty() {
            builder = builder.user_agent(ua);
        } else {
            builder = builder.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
        }
    } else {
        builder = builder.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
    }

    let client = builder.build().map_err(|e| e.to_string())?;

    if let Ok(parsed) = reqwest::Url::parse(&url) {
        if let Some(host) = parsed.host_str() {
            let port = parsed.port_or_known_default().unwrap_or(80);
            if let Ok(addrs) = std::net::ToSocketAddrs::to_socket_addrs(&(host, port)) {
                for addr in addrs {
                    let ip = addr.ip();
                    if ip.is_loopback() || ip.is_multicast() || ip.is_unspecified() {
                        return Err("SSRF blocked: Private/local IP not allowed".to_string());
                    }
                    match ip {
                        std::net::IpAddr::V4(ipv4) if ipv4.is_private() || ipv4.is_link_local() => {
                            return Err("SSRF blocked: Private/local IP not allowed".to_string());
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    let mut head_req = client.head(&url);
    if let Some(ref user) = username {
        if !user.is_empty() {
            head_req = head_req.basic_auth(user, password.as_deref());
        }
    }
    let mut res = head_req.send().await.map_err(|e| e.to_string())?;

    if !res.status().is_success() {
        let mut get_req = client.get(&url);
        if let Some(ref user) = username {
            if !user.is_empty() {
                get_req = get_req.basic_auth(user, password.as_deref());
            }
        }
        res = get_req.send().await.map_err(|e| e.to_string())?;
    }

    let mut filename = String::new();
    if let Some(cd) = res.headers().get(reqwest::header::CONTENT_DISPOSITION) {
        if let Ok(cd_str) = cd.to_str() {
            if let Some(idx) = cd_str.find("filename=") {
                let rest = &cd_str[idx + 9..];
                filename = rest.trim_matches('"').to_string();
            }
        }
    }

    if filename.is_empty() {
        if let Ok(parsed) = reqwest::Url::parse(&url) {
            if let Some(segments) = parsed.path_segments() {
                if let Some(last) = segments.last() {
                    filename = last.to_string();
                }
            }
        }
    }
    if filename.is_empty() {
        filename = "download".to_string();
    }

    let mut size_str = "Unknown".to_string();
    let mut size_bytes = 0;
    if let Some(len) = res.headers().get(reqwest::header::CONTENT_LENGTH) {
        if let Ok(len_str) = len.to_str() {
            if let Ok(bytes) = len_str.parse::<u64>() {
                size_bytes = bytes;
                if bytes < 1024 {
                    size_str = format!("{} B", bytes);
                } else if bytes < 1024 * 1024 {
                    size_str = format!("{:.1} KB", bytes as f64 / 1024.0);
                } else if bytes < 1024 * 1024 * 1024 {
                    size_str = format!("{:.1} MB", bytes as f64 / 1024.0 / 1024.0);
                } else {
                    size_str = format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0);
                }
            }
        }
    }

    Ok(MetadataResponse { filename, size: size_str, size_bytes })
}

#[tauri::command]
async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_browser: Option<String>, username: Option<String>, password: Option<String>) -> Result<String, String> {
    println!("fetch_media_metadata called for: {}", url);
    let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
    let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));

    let mut cmd = AsyncCommand::new(&ytdlp_path);
    cmd.arg("-J")
       .arg("--no-warnings")
       .arg("--no-playlist")
       .arg("--no-check-formats")
       .arg("--socket-timeout").arg("20")
       .arg("--retries").arg("3")
       .arg("--extractor-retries").arg("3")
       .arg("--compat-options").arg("no-youtube-unavailable-videos")
       .arg("--js-runtimes").arg("deno,node");

    if let Some(browser) = cookie_browser {
        if !browser.is_empty() {
            cmd.arg("--cookies-from-browser").arg(&browser);
        }
    }
    
    let mut config_file = tempfile::Builder::new().prefix("ytdlp-").suffix(".conf").tempfile().map_err(|e| e.to_string())?;
    let mut config_content = String::new();
    if let Some(user) = username {
        if !user.is_empty() {
            config_content.push_str(&format!("--username\n{}\n", user));
        }
    }
    if let Some(pass) = password {
        if !pass.is_empty() {
            config_content.push_str(&format!("--password\n{}\n", pass));
        }
    }
    use std::io::Write;
    config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
    let config_path = config_file.into_temp_path();
    if !config_content.is_empty() {
        cmd.arg("--config-locations").arg(&config_path);
    }

    cmd.arg(&url);

    // We use tokio AsyncCommand so it doesn't block the async thread
    let output = cmd.output()
        .await
        .map_err(|e| format!("Failed to execute yt-dlp: {}", e))?;

    if output.status.success() {
        let text = String::from_utf8_lossy(&output.stdout).to_string();
        Ok(text)
    } else {
        let err = String::from_utf8_lossy(&output.stderr);
        Err(format!("yt-dlp error: {}", err))
    }
}

#[tauri::command]
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
    println!("test_ytdlp called!");
    let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
    let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));
    println!("Resolved yt-dlp path: {:?}", ytdlp_path);

    let output = AsyncCommand::new(&ytdlp_path)
        .arg("--version")
        .output()
        .await
        .map_err(|e| {
            println!("Failed to execute: {}", e);
            format!("Failed to execute yt-dlp: {}", e)
        })?;

    println!("yt-dlp execution finished with status: {}", output.status);
    if output.status.success() {
        let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
        println!("yt-dlp output: {}", text);
        Ok(text)
    } else {
        let err = String::from_utf8_lossy(&output.stderr);
        println!("yt-dlp error output: {}", err);
        Err(format!("yt-dlp error: {}", err))
    }
}

#[tauri::command]
async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
    println!("test_ffmpeg called!");
    let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
    let ffmpeg_path = resource_dir.join("binaries").join(get_binary_name("ffmpeg"));
    println!("Resolved ffmpeg path: {:?}", ffmpeg_path);

    let output = AsyncCommand::new(&ffmpeg_path)
        .arg("-version")
        .output()
        .await
        .map_err(|e| {
            println!("Failed to execute: {}", e);
            format!("Failed to execute ffmpeg: {}", e)
        })?;

    println!("ffmpeg execution finished with status: {}", output.status);
    if output.status.success() {
        let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
        let first_line = text.lines().next().unwrap_or("").to_string();
        let parts: Vec<&str> = first_line.split_whitespace().collect();
        let clean = parts.get(2).unwrap_or(&first_line.as_str()).split('-').next().unwrap_or("").to_string();
        println!("ffmpeg output: {}", clean);
        Ok(clean)
    } else {
        let err = String::from_utf8_lossy(&output.stderr);
        println!("ffmpeg error output: {}", err);
        Err(format!("ffmpeg error: {}", err))
    }
}

#[tauri::command]
async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
    println!("test_deno called!");
    let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
    let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
    println!("Resolved deno path: {:?}", deno_path);

    let output = AsyncCommand::new(&deno_path)
        .arg("--version")
        .output()
        .await
        .map_err(|e| {
            println!("Failed to execute: {}", e);
            format!("Failed to execute deno: {}", e)
        })?;

    println!("deno execution finished with status: {}", output.status);
    if output.status.success() {
        let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
        let first_line = text.lines().next().unwrap_or("").to_string();
        let parts: Vec<&str> = first_line.split_whitespace().collect();
        let clean = parts.get(1).unwrap_or(&first_line.as_str()).to_string();
        println!("deno output: {}", clean);
        Ok(clean)
    } else {
        let err = String::from_utf8_lossy(&output.stderr);
        println!("deno error output: {}", err);
        Err(format!("deno error: {}", err))
    }
}

#[tauri::command]
async fn open_file(app: tauri::AppHandle, path: String) -> Result<(), String> {
    println!("open_file called for path: {}", path);
    use tauri_plugin_opener::OpenerExt;
    app.opener().open_path(&path, None::<String>).map_err(|e| format!("Failed to open file: {}", e))
}

#[tauri::command]
async fn show_in_folder(app: tauri::AppHandle, path: String) -> Result<(), String> {
    println!("show_in_folder called for path: {}", path);
    use tauri_plugin_opener::OpenerExt;
    app.opener().reveal_item_in_dir(&path).map_err(|e| format!("Failed to reveal in folder: {}", e))
}

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};

struct Aria2DaemonGuard(std::sync::Mutex<Option<std::process::Child>>);

impl Drop for Aria2DaemonGuard {
    fn drop(&mut self) {
        if let Ok(mut lock) = self.0.lock() {
            if let Some(mut child) = lock.take() {
                let _ = child.kill();
            }
        }
    }
}

#[cfg(target_os = "macos")]
fn resign_aria2_debug_bundle(aria2c_path: &std::path::Path) -> Result<(), String> {
    let lib_dir = aria2c_path
        .parent()
        .ok_or_else(|| "aria2 resource directory is missing".to_string())?
        .join("aria2-libs");
    let mut libraries = std::fs::read_dir(&lib_dir)
        .map_err(|error| format!("Failed to read aria2 libraries: {error}"))?
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.extension().and_then(|extension| extension.to_str()) == Some("dylib"))
        .collect::<Vec<_>>();
    libraries.sort();

    for path in libraries.into_iter().chain(std::iter::once(aria2c_path.to_path_buf())) {
        let output = std::process::Command::new("/usr/bin/codesign")
            .args(["--force", "--sign", "-"])
            .arg(&path)
            .output()
            .map_err(|error| format!("Failed to run codesign for {}: {error}", path.display()))?;
        if !output.status.success() {
            return Err(format!(
                "Failed to sign {}: {}",
                path.display(),
                String::from_utf8_lossy(&output.stderr).trim()
            ));
        }
    }

    Ok(())
}

pub mod download;
#[allow(dead_code)]
mod ipc;
mod parity;
pub mod error;
pub use error::AppError;

// Retained only for compatibility with the optional aria2 diagnostic monitor.
// Active downloads are owned by DownloadCoordinator.
pub enum TaskHandle {
    Aria2(String),
    #[doc(hidden)]
    Inactive,
}

pub struct AppState {
    pub download_coordinator: download::DownloadCoordinator,
    pub extension_pairing_token: extension_server::SharedExtensionToken,
    pub extension_frontend_ready: extension_server::SharedFrontendReady,
    pub aria2_port: u16,
    pub aria2_secret: String,
    pub media_semaphore: Arc<tokio::sync::Semaphore>,
    pub sleep_preventer: Arc<Mutex<Option<keepawake::KeepAwake>>>,
}

#[derive(Clone, Serialize, TS)]
#[ts(export, export_to = "../../src/bindings/")]
pub struct DownloadProgressEvent {
    id: String,
    fraction: f64,
    speed: String,
    eta: String,
}

fn collect_download_uris(url: &str, mirrors: Option<&str>) -> Vec<String> {
    let mut uris = Vec::new();
    for uri in std::iter::once(url).chain(mirrors.into_iter().flat_map(str::lines)) {
        let uri = uri.trim();
        if !uri.is_empty() && !uris.iter().any(|existing| existing == uri) {
            uris.push(uri.to_string());
        }
    }
    uris
}

const MAX_DEEP_LINK_PAYLOAD_LEN: usize = 65_536;
const MAX_DEEP_LINK_URLS: usize = 200;

fn parse_firelink_urls(deep_links: impl IntoIterator<Item = url::Url>) -> Vec<String> {
    let mut captured = Vec::new();

    for deep_link in deep_links {
        if deep_link.scheme() != "firelink" || deep_link.host_str() != Some("add") {
            continue;
        }

        let Some(raw_urls) = deep_link
            .query_pairs()
            .find_map(|(key, value)| (key == "url").then(|| value.into_owned()))
        else {
            continue;
        };
        if raw_urls.is_empty() || raw_urls.chars().count() >= MAX_DEEP_LINK_PAYLOAD_LEN {
            continue;
        }

        for raw_url in raw_urls.lines() {
            let raw_url = raw_url.trim();
            let Ok(url) = url::Url::parse(raw_url) else {
                continue;
            };
            if !matches!(url.scheme(), "http" | "https" | "ftp" | "sftp") {
                continue;
            }
            let url = url.to_string();
            if !captured.iter().any(|existing| existing == &url) {
                captured.push(url);
                if captured.len() == MAX_DEEP_LINK_URLS {
                    return captured;
                }
            }
        }
    }

    captured
}

fn restore_main_window(app_handle: &tauri::AppHandle) {
    if let Some(window) = app_handle.get_webview_window("main") {
        let _ = window.unminimize();
        let _ = window.show();
        let _ = window.set_focus();
    }
}

fn dispatch_deep_links(app_handle: tauri::AppHandle, deep_links: Vec<url::Url>) {
    let urls = parse_firelink_urls(deep_links);
    if urls.is_empty() {
        return;
    }

    restore_main_window(&app_handle);
    let coordinator = app_handle.state::<AppState>().download_coordinator.clone();
    tauri::async_runtime::spawn(async move {
        if let Err(error) = coordinator
            .send(download::DownloadCmd::CaptureUrls(urls))
            .await
        {
            eprintln!("Failed to dispatch deep link to download coordinator: {error}");
        }
    });
}

async fn rpc_call(port: u16, secret: &str, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
    let url = format!("http://127.0.0.1:{}/jsonrpc", port);
    let mut payload = serde_json::Map::new();
    payload.insert("jsonrpc".to_string(), serde_json::json!("2.0"));
    payload.insert("id".to_string(), serde_json::json!("1"));
    payload.insert("method".to_string(), serde_json::json!(method));
    
    let mut p = vec![serde_json::json!(format!("token:{}", secret))];
    if let serde_json::Value::Array(arr) = params {
        p.extend(arr);
    }
    payload.insert("params".to_string(), serde_json::json!(p));

    let client = reqwest::Client::new();
    let res = client.post(&url)
        .json(&payload)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    let json: serde_json::Value = res.json().await.map_err(|e| e.to_string())?;
    if let Some(error) = json.get("error") {
        return Err(error.to_string());
    }
    json.get("result")
        .cloned()
        .ok_or_else(|| "aria2 returned no result".to_string())
}

#[tauri::command]
async fn test_aria2c(state: tauri::State<'_, AppState>) -> Result<String, String> {
    let result = rpc_call(
        state.aria2_port,
        &state.aria2_secret,
        "aria2.getVersion",
        serde_json::json!([]),
    )
    .await
    .map_err(|error| format!("aria2 daemon unavailable: {error}"))?;

    result
        .get("version")
        .and_then(serde_json::Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| "aria2 returned an invalid version response".to_string())
}

#[tauri::command]
async fn start_download(
    app_handle: tauri::AppHandle,
    state: tauri::State<'_, AppState>,
    id: String,
    url: String,
    destination: String,
    filename: String,
    connections: Option<i32>,
    speed_limit: Option<String>,
    username: Option<String>,
    password: Option<String>,
    headers: Option<String>,
    checksum: Option<String>,
    cookies: Option<String>,
    mirrors: Option<String>,
    user_agent: Option<String>,
    max_tries: Option<i32>,
    proxy: Option<String>,
) -> Result<(), AppError> {
    println!("start_download called for id: {}", id);
    let download_id = Uuid::parse_str(&id).map_err(|error| AppError::Internal(error.to_string()))?;

    let mut resolved_dest = std::path::PathBuf::from(&destination);
    if destination.starts_with("~/") {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home.join(&destination[2..]);
        }
    } else if destination == "~" {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home;
        }
    }
    let _ = connections;
    let _ = checksum;
    state
        .download_coordinator
        .send(download::DownloadCmd::Start(download::DownloadPayload {
            id: download_id,
            urls: collect_download_uris(&url, mirrors.as_deref()),
            output_path: resolved_dest.join(filename),
            speed_limit,
            username,
            password,
            headers,
            cookies,
            user_agent,
            max_tries: max_tries.unwrap_or(1).max(1) as u32,
            proxy,
        }))
        .await
        .map_err(AppError::Internal)?;

    Ok(())
}

#[tauri::command]
async fn start_media_download(
    app_handle: tauri::AppHandle,
    state: tauri::State<'_, AppState>,
    id: String,
    url: String,
    destination: String,
    filename: String,
    format_selector: Option<String>,
    cookie_source: Option<String>,
    speed_limit: Option<String>,
    username: Option<String>,
    password: Option<String>,
    headers: Option<String>,
    proxy: Option<String>,
    user_agent: Option<String>,
    max_tries: Option<i32>,
) -> Result<(), String> {
    let media_semaphore = state.media_semaphore.clone();
    let coordinator = state.download_coordinator.clone();
    let mut cancel_rx = coordinator.register_media(id.clone()).await?;
    tauri::async_runtime::spawn(async move {
        let permit = tokio::select! {
            permit = media_semaphore.acquire() => permit,
            _ = cancel_rx.changed() => {
                coordinator.finish_media(id).await;
                return;
            }
        };

        let _ = start_media_download_internal(
            app_handle,
            &id, url,
            destination,
            filename,
            format_selector,
            cookie_source,
            speed_limit,
            username,
            password,
            headers,
            proxy,
            user_agent,
            max_tries,
            &mut cancel_rx,
        ).await;

        drop(permit);
        coordinator.finish_media(id).await;
    });

    Ok(())
}

pub(crate) async fn start_media_download_internal(
    app_handle: tauri::AppHandle,
    id: &str,
    url: String,
    destination: String,
    filename: String,
    format_selector: Option<String>,
    cookie_source: Option<String>,
    speed_limit: Option<String>,
    username: Option<String>,
    password: Option<String>,
    headers: Option<String>,
    proxy: Option<String>,
    user_agent: Option<String>,
    max_tries: Option<i32>,
    cancel_rx: &mut tokio::sync::watch::Receiver<bool>,
) -> Result<(), String> {
    println!("start_media_download called for id: {}", id);
    let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
    let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));
    let ffmpeg_path = resource_dir.join("binaries").join(get_binary_name("ffmpeg"));

    let mut resolved_dest = std::path::PathBuf::from(&destination);
    if destination.starts_with("~/") {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home.join(&destination[2..]);
        }
    } else if destination == "~" {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home;
        }
    }

    if !resolved_dest.exists() {
        let _ = tokio::fs::create_dir_all(&resolved_dest).await;
    }

    let out_path = resolved_dest.join(&filename);

    let total_tracks: f64 = if let Some(ref format) = format_selector {
        if format.contains('+') { 2.0 } else { 1.0 }
    } else {
        1.0
    };

    let mut cmd = AsyncCommand::new(&ytdlp_path);
    cmd.arg("--newline")
       .arg("--ffmpeg-location")
       .arg(&ffmpeg_path)
       .arg("--no-check-formats")
       .arg("--socket-timeout").arg("20")
       .arg("--retries").arg("3")
       .arg("--extractor-retries").arg("3")
       .arg("--downloader").arg("aria2c")
       .arg("--downloader-args").arg("aria2c:-x 16 -s 16 -k 1M")
       .arg("--concurrent-fragments").arg("4")
       .arg("-o").arg(out_path.to_string_lossy().to_string());

    if let Some(limit) = speed_limit {
        if !limit.is_empty() {
            cmd.arg("--limit-rate").arg(limit);
        }
    }

    if let Some(p) = proxy {
        if !p.is_empty() {
            cmd.arg("--proxy").arg(p);
        }
    }

    if let Some(mut cs) = cookie_source {
        if !cs.is_empty() && cs != "none" {
            if cs == "safari" { cs = "safari:".to_string() }
            cmd.arg("--cookies-from-browser").arg(cs);
        }
    }

    if let Some(ua) = user_agent {
        if !ua.is_empty() {
            cmd.arg("--user-agent").arg(ua);
        }
    }

    if let Some(tries) = max_tries {
        cmd.arg("--retries").arg(tries.to_string());
    }

    let mut config_file = tempfile::Builder::new().prefix("ytdlp-").suffix(".conf").tempfile().map_err(|e| e.to_string())?;
    let mut config_content = String::new();
    if let Some(user) = username {
        if !user.is_empty() {
            config_content.push_str(&format!("--username\n{}\n", user));
        }
    }
    if let Some(pass) = password {
        if !pass.is_empty() {
            config_content.push_str(&format!("--password\n{}\n", pass));
        }
    }
    if let Some(headers) = headers {
        for header in headers.lines().map(str::trim).filter(|header| !header.is_empty()) {
            config_content.push_str(&format!("--add-header\n{}\n", header));
        }
    }
    use std::io::Write;
    config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
    let config_path = config_file.into_temp_path();
    if !config_content.is_empty() {
        cmd.arg("--config-locations").arg(&config_path);
    }

    if let Some(format) = format_selector {
        cmd.arg("-f").arg(format);
        // If the filename implies an audio format, use it as audio output
        if filename.ends_with(".mp3") {
            cmd.arg("-x").arg("--audio-format").arg("mp3");
        } else if filename.ends_with(".m4a") {
            cmd.arg("-x").arg("--audio-format").arg("m4a");
        } else if filename.ends_with(".opus") {
            cmd.arg("-x").arg("--audio-format").arg("opus");
        } else {
            // Otherwise attempt to merge into mp4 or mkv based on filename
            if filename.ends_with(".mp4") {
                cmd.arg("--merge-output-format").arg("mp4");
            } else if filename.ends_with(".webm") {
                cmd.arg("--merge-output-format").arg("webm");
            } else {
                cmd.arg("--merge-output-format").arg("mkv");
            }
        }
    }

    cmd.arg(&url);
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::piped()); // Also pipe stderr for better error reporting

    let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn yt-dlp: {}", e))?;
    let stdout = child.stdout.take().unwrap();

    // yt-dlp parsing regex
    let pct_re = Regex::new(r"\[download\]\s+(\d+(?:\.\d+)?)%").unwrap();
    let spd_re = Regex::new(r"at\s+([^\s]+)").unwrap();
    let eta_re = Regex::new(r"ETA\s+([^\s]+)").unwrap();

    let _keep_alive = config_path;
    let mut reader = BufReader::new(stdout).lines();
    let mut current_track: f64 = 0.0;
    let mut last_fraction: f64 = 0.0;
    let mut last_progress_at = std::time::Instant::now()
        .checked_sub(std::time::Duration::from_millis(150))
        .unwrap_or_else(std::time::Instant::now);

    loop {
        tokio::select! {
            _ = cancel_rx.changed() => {
                let _ = child.kill().await;
                return Ok(());
            }
            line_result = reader.next_line() => {
                match line_result {
                    Ok(Some(line)) => {
                        if line.contains("[download]") && line.contains("%") {
                            let fraction = pct_re.captures(&line)
                                .and_then(|cap| cap.get(1))
                                .and_then(|m| m.as_str().parse::<f64>().ok())
                                .unwrap_or(0.0) / 100.0;

                            if fraction < last_fraction && (last_fraction - fraction) > 0.5 {
                                current_track += 1.0;
                            }
                            last_fraction = fraction;

                            let overall_fraction = ((current_track + fraction) / total_tracks).min(1.0);

                            let speed = spd_re.captures(&line)
                                .and_then(|cap| cap.get(1))
                                .map(|m| m.as_str().to_string())
                                .unwrap_or_else(|| "-".to_string());

                            let eta = eta_re.captures(&line)
                                .and_then(|cap| cap.get(1))
                                .map(|m| m.as_str().to_string())
                                .unwrap_or_else(|| "-".to_string());

                            let now = std::time::Instant::now();
                            if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(150) {
                                let _ = app_handle.emit("download-progress", DownloadProgressEvent {
                                    id: id.to_string(),
                                    fraction: overall_fraction,
                                    speed,
                                    eta,
                                });
                                last_progress_at = now;
                            }
                        }
                    }
                    _ => break,
                }
            }
            status = child.wait() => {
                println!("child exit status: {:?}", status);
                if let Ok(exit_status) = status {
                    if exit_status.success() {
                        let _ = app_handle.emit("download-complete", id.to_string());
                    } else {
                        let _ = app_handle.emit("download-failed", id.to_string());
                    }
                }
                break;
            }
        }
    }

    Ok(())
}

#[tauri::command]
async fn pause_download(state: tauri::State<'_, AppState>, id: String) -> Result<(), String> {
    println!("pause_download called for id: {}", id);
    if let Ok(download_id) = Uuid::parse_str(&id) {
        state
            .download_coordinator
            .send(download::DownloadCmd::Pause(download_id))
            .await?;
    }
    state.download_coordinator.pause_media(id).await
}

#[tauri::command]
async fn remove_download(state: tauri::State<'_, AppState>, id: String, filepath: Option<String>) -> Result<(), String> {
    println!("remove_download called for id: {}", id);

    if let Ok(download_id) = Uuid::parse_str(&id) {
        state
            .download_coordinator
            .send(download::DownloadCmd::Cancel(download_id))
            .await?;
    }
    state.download_coordinator.pause_media(id).await?;
    
    if let Some(path) = filepath {
        if !path.is_empty() {
            let p = std::path::Path::new(&path);
            if p.exists() {
                let _ = tokio::fs::remove_file(p).await;
            }
            let aria2_path = format!("{}.aria2", path);
            let p_aria2 = std::path::Path::new(&aria2_path);
            if p_aria2.exists() {
                let _ = tokio::fs::remove_file(p_aria2).await;
            }
        }
    }
    
    Ok(())
}

#[tauri::command]
fn update_dock_badge(_app_handle: tauri::AppHandle, count: i32) {
    #[cfg(target_os = "macos")]
    {
        use cocoa::appkit::NSApp;
        use cocoa::base::{nil, id};
        use cocoa::foundation::NSString;
        use objc::{msg_send, sel, sel_impl};
        
        unsafe {
            let app = NSApp();
            let dock_tile: id = msg_send![app, dockTile];
            let label = if count > 0 { count.to_string() } else { "".to_string() };
            let ns_label = NSString::alloc(nil).init_str(&label);
            let _: () = msg_send![dock_tile, setBadgeLabel: ns_label];
        }
    }
}

#[tauri::command]
fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) {
    let mut current_preventer = state.sleep_preventer.lock().unwrap();
    if prevent {
        if current_preventer.is_none() {
            if let Ok(keepawake) = keepawake::Builder::default().display(true).reason("Downloading files").create() {
                *current_preventer = Some(keepawake);
            }
        }
    } else {
        *current_preventer = None;
    }
}

pub(crate) fn execute_system_action(action: crate::ipc::PostQueueAction) -> Result<(), String> {
    match action {
        crate::ipc::PostQueueAction::Shutdown => {
            system_shutdown::shutdown().map_err(|e| e.to_string())
        }
        crate::ipc::PostQueueAction::Restart => {
            system_shutdown::reboot().map_err(|e| e.to_string())
        }
        crate::ipc::PostQueueAction::Sleep => {
            system_shutdown::sleep().map_err(|e| e.to_string())
        }
        crate::ipc::PostQueueAction::None => Err("Invalid action".to_string()),
    }
}

#[tauri::command]
fn perform_system_action(action: crate::ipc::PostQueueAction) -> Result<(), String> {
    execute_system_action(action)
}

#[tauri::command]
async fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) -> Result<(), String> {
    let _ = rpc_call(
        state.aria2_port,
        &state.aria2_secret,
        "aria2.changeGlobalOption",
        serde_json::json!([{"max-concurrent-downloads": limit.to_string()}])
    ).await;
    Ok(())
}

#[tauri::command]
async fn set_global_speed_limit(state: tauri::State<'_, AppState>, limit: Option<String>) -> Result<(), String> {
    let limit_str = limit.unwrap_or_else(|| "0".to_string());
    let _ = rpc_call(
        state.aria2_port,
        &state.aria2_secret,
        "aria2.changeGlobalOption",
        serde_json::json!([{"max-overall-download-limit": limit_str}])
    ).await;
    Ok(())
}

#[tauri::command]
fn request_automation_permission() -> Result<(), String> {
    #[cfg(target_os = "macos")]
    {
        use cocoa::foundation::NSString;
        use cocoa::base::{nil, id};
        use objc::{msg_send, sel, sel_impl, class};

        unsafe {
            let script_str = NSString::alloc(nil).init_str("tell application \"Finder\" to get name");
            let ns_apple_script: id = msg_send![class!(NSAppleScript), alloc];
            let ns_apple_script: id = msg_send![ns_apple_script, initWithSource: script_str];
            let mut error_dict: id = nil;
            let result: id = msg_send![ns_apple_script, executeAndReturnError: &mut error_dict];
            if result == nil {
                return Err("Automation permission was not granted".to_string());
            }
        }
        return Ok(());
    }

    #[cfg(not(target_os = "macos"))]
    Ok(())
}

#[tauri::command]
fn open_automation_settings(app_handle: tauri::AppHandle) -> Result<(), String> {
    #[cfg(target_os = "macos")]
    {
        use tauri_plugin_opener::OpenerExt;
        app_handle.opener().open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Automation", None::<String>)
            .map_err(|e| format!("Failed to open Automation settings: {}", e))?;
        return Ok(());
    }

    #[cfg(not(target_os = "macos"))]
    Err("Automation settings are only available on macOS".to_string())
}

#[tauri::command]
fn get_free_space(app_handle: tauri::AppHandle, path: String) -> Result<String, String> {
    use sysinfo::Disks;
    use tauri::Manager;
    let disks = Disks::new_with_refreshed_list();

    let mut resolved_dest = std::path::PathBuf::from(&path);
    if path.starts_with("~/") {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home.join(&path[2..]);
        }
    } else if path == "~" {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home;
        }
    }

    // Find the disk that the path is mounted on
    let mut best_match: Option<&sysinfo::Disk> = None;
    let mut max_match_len = 0;

    for disk in disks.list() {
        let mount_point = disk.mount_point();
        if resolved_dest.starts_with(mount_point) {
            let match_len = mount_point.as_os_str().len();
            if match_len > max_match_len {
                max_match_len = match_len;
                best_match = Some(disk);
            }
        }
    }

    if let Some(disk) = best_match {
        let bytes = disk.available_space();
        let size_str = if bytes < 1024 * 1024 {
            format!("{:.1} KB", bytes as f64 / 1024.0)
        } else if bytes < 1024 * 1024 * 1024 {
            format!("{:.1} MB", bytes as f64 / 1024.0 / 1024.0)
        } else {
            format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0)
        };
        Ok(size_str)
    } else {
        Ok("Unknown".to_string())
    }
}

#[tauri::command]
fn set_keychain_password(id: String, password: String) -> Result<(), String> {
    let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
    entry.set_password(&password).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
fn get_keychain_password(id: String) -> Result<String, String> {
    let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
    entry.get_password().map_err(|e| e.to_string())
}

#[tauri::command]
fn delete_keychain_password(id: String) -> Result<(), String> {
    let entry = keyring::Entry::new("com.firelink.app", &id).map_err(|e| e.to_string())?;
    let _ = entry.delete_credential(); // Ignore error if it doesn't exist
    Ok(())
}

#[tauri::command]
fn check_file_exists(app_handle: tauri::AppHandle, path: String) -> bool {
    use tauri::Manager;
    let mut resolved_dest = std::path::PathBuf::from(&path);
    if path.starts_with("~/") {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home.join(&path[2..]);
        }
    } else if path == "~" {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home;
        }
    }
    resolved_dest.exists()
}

#[tauri::command]
fn delete_file(app_handle: tauri::AppHandle, path: String) -> Result<(), String> {
    use tauri::Manager;
    let mut resolved_dest = std::path::PathBuf::from(&path);
    if path.starts_with("~/") {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home.join(&path[2..]);
        }
    } else if path == "~" {
        if let Ok(home) = app_handle.path().home_dir() {
            resolved_dest = home;
        }
    }
    if resolved_dest.exists() {
        std::fs::remove_file(resolved_dest).map_err(|e| e.to_string())
    } else {
        Ok(())
    }
}

#[tauri::command]
fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), String> {
    use tauri::tray::TrayIconBuilder;
    use tauri::menu::{Menu, MenuItem};

    if show {
        if app_handle.tray_by_id("main").is_none() {
            let quit_i = MenuItem::with_id(&app_handle, "quit", "Quit", true, None::<&str>).map_err(|e| e.to_string())?;
            let show_i = MenuItem::with_id(&app_handle, "show", "Show Firelink", true, None::<&str>).map_err(|e| e.to_string())?;
            let menu = Menu::with_items(&app_handle, &[&show_i, &quit_i]).map_err(|e| e.to_string())?;

            let tray_icon = tauri::image::Image::from_bytes(include_bytes!("../icons/trayTemplate.png")).unwrap();
            let _tray = TrayIconBuilder::with_id("main")
                .icon(tray_icon)
                .icon_as_template(true)
                .menu(&menu)
                .show_menu_on_left_click(false)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "quit" => {
                        app.exit(0);
                    }
                    "show" => {
                        restore_main_window(app);
                    }
                    _ => {}
                })
                .on_tray_icon_event(|tray, event| {
                    use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};

                    if matches!(
                        event,
                        TrayIconEvent::Click {
                            button: MouseButton::Left,
                            button_state: MouseButtonState::Up,
                            ..
                        }
                    ) {
                        restore_main_window(tray.app_handle());
                    }
                })
                .build(&app_handle)
                .map_err(|e| e.to_string())?;
        }
    } else {
        if let Some(_tray) = app_handle.tray_by_id("main") {
            let _ = app_handle.remove_tray_by_id("main");
        }
    }
    Ok(())
}

#[tauri::command]
fn set_extension_pairing_token(
    state: tauri::State<'_, AppState>,
    token: String,
) -> Result<(), String> {
    if token.is_empty() || token.len() > 512 {
        return Err("Invalid extension pairing token".to_string());
    }

    let mut pairing_token = state
        .extension_pairing_token
        .write()
        .map_err(|_| "Extension pairing token lock is unavailable".to_string())?;
    *pairing_token = token;
    Ok(())
}

#[tauri::command]
fn set_extension_frontend_ready(
    state: tauri::State<'_, AppState>,
    ready: bool,
) {
    state
        .extension_frontend_ready
        .store(ready, Ordering::Release);
    let coordinator = state.download_coordinator.clone();
    tauri::async_runtime::spawn(async move {
        let _ = coordinator
            .send(download::DownloadCmd::FrontendReady(ready))
            .await;
    });
}

#[cfg(test)]
mod tests {
    use super::{collect_download_uris, parse_firelink_urls};

    #[test]
    fn collects_primary_url_and_unique_mirrors_in_order() {
        let uris = collect_download_uris(
            "https://primary.example/file.zip",
            Some(
                "\nhttps://mirror-one.example/file.zip\n\
                 https://primary.example/file.zip\n\
                 https://mirror-two.example/file.zip\n",
            ),
        );

        assert_eq!(
            uris,
            vec![
                "https://primary.example/file.zip",
                "https://mirror-one.example/file.zip",
                "https://mirror-two.example/file.zip",
            ]
        );
    }

    #[test]
    fn parses_valid_firelink_download_urls() {
        let deep_link = url::Url::parse(
            "firelink://add?url=https%3A%2F%2Fexample.com%2Fone.zip%0Aftp%3A%2F%2Fexample.com%2Ftwo.zip",
        )
        .unwrap();

        assert_eq!(
            parse_firelink_urls([deep_link]),
            vec![
                "https://example.com/one.zip",
                "ftp://example.com/two.zip",
            ]
        );
    }

    #[test]
    fn rejects_unexpected_deep_links_and_nested_schemes() {
        let links = [
            url::Url::parse("firelink://open?url=https%3A%2F%2Fexample.com").unwrap(),
            url::Url::parse("firelink://add?url=file%3A%2F%2F%2Ftmp%2Fsecret").unwrap(),
            url::Url::parse("other://add?url=https%3A%2F%2Fexample.com").unwrap(),
        ];

        assert!(parse_firelink_urls(links).is_empty());
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let extension_pairing_token = Arc::new(RwLock::new(String::new()));
    let server_pairing_token = extension_pairing_token.clone();
    let extension_frontend_ready = Arc::new(AtomicBool::new(false));
    let server_frontend_ready = extension_frontend_ready.clone();

    let aria2_port = std::net::TcpListener::bind("127.0.0.1:0")
        .and_then(|listener| listener.local_addr())
        .map(|addr| addr.port())
        .unwrap_or(6800);
    let aria2_secret = format!("{:x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
    tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
            restore_main_window(app);
        }))
        .plugin(tauri_plugin_deep_link::init())
        .manage(Aria2DaemonGuard(std::sync::Mutex::new(None)))
        .setup(move |app| {
            app.manage(AppState {
                download_coordinator: download::DownloadCoordinator::spawn(app.handle().clone()),
                extension_pairing_token,
                extension_frontend_ready,
                aria2_port,
                aria2_secret: aria2_secret.clone(),
                media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
                sleep_preventer: Arc::new(Mutex::new(None)),
            });
            let deep_link_app = app.handle().clone();
            app.deep_link().on_open_url(move |event| {
                dispatch_deep_links(deep_link_app.clone(), event.urls());
            });
            match app.deep_link().get_current() {
                Ok(Some(urls)) => dispatch_deep_links(app.handle().clone(), urls),
                Ok(None) => {}
                Err(error) => eprintln!("Failed to read startup deep link: {error}"),
            }
            let db_conn = crate::db::init_db(app.handle()).expect("Failed to init db");
            app.manage(crate::db::DbState { conn: tokio::sync::Mutex::new(db_conn) });

            crate::scheduler::spawn_scheduler(app.handle().clone());

            let resource_dir = app.path().resource_dir().unwrap();
            let aria2c_path = resource_dir.join("binaries").join(get_binary_name("aria2c"));

            #[cfg(all(target_os = "macos", debug_assertions))]
            if let Err(error) = resign_aria2_debug_bundle(&aria2c_path) {
                eprintln!("{error}");
            }
            
            let aria2_process = std::process::Command::new(&aria2c_path)
                .arg("--enable-rpc=true")
                .arg(format!("--rpc-listen-port={}", aria2_port))
                .arg(format!("--rpc-secret={}", aria2_secret))
                .arg("--rpc-listen-all=false")
                .arg("--continue=true")
                .arg("--allow-overwrite=false")
                .arg("--summary-interval=1")
                .arg("--console-log-level=warn")
                .arg("--download-result=hide")
                .arg("--check-certificate=false")
                .spawn();

            match aria2_process {
                Ok(process) => {
                    println!("Spawned global aria2c daemon on port {}", aria2_port);
                    let guard = app.state::<Aria2DaemonGuard>();
                    *guard.0.lock().unwrap() = Some(process);
                }
                Err(e) => eprintln!("Failed to spawn aria2c daemon: {}", e),
            }


            let ext_app_handle = app.handle().clone();
            tauri::async_runtime::spawn(async move {
                if let Err(error) = extension_server::start_server(
                    ext_app_handle,
                    server_pairing_token.clone(),
                    server_frontend_ready.clone(),
                ).await {
                    eprintln!("Browser extension server unavailable: {error}");
                }
            });
            Ok(())
        })
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_notification::init())
        .on_window_event(|window, event| {
            if window.label() == "main" {
                if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                    if window.app_handle().tray_by_id("main").is_some() {
                        api.prevent_close();
                        let _ = window.hide();
                    }
                }
            }
        })
        .invoke_handler(tauri::generate_handler![
            test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
            start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata,
            update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
            request_automation_permission, open_automation_settings,
            set_keychain_password, get_keychain_password, delete_keychain_password,
            check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
            set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download,
            parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media,
            db_save_settings, db_load_settings, db_get_all_downloads, db_save_download, db_delete_download, db_get_all_queues, db_save_queue, db_delete_queue
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
mod extension_server;
mod db;
mod scheduler;

#[tauri::command]
async fn db_save_settings(state: tauri::State<'_, crate::db::DbState>, data: String) -> Result<(), String> {
    let conn = state.conn.lock().await;
    crate::db::save_settings(&conn, &data).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_load_settings(state: tauri::State<'_, crate::db::DbState>) -> Result<Option<String>, String> {
    let conn = state.conn.lock().await;
    crate::db::get_settings(&conn).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_get_all_downloads(state: tauri::State<'_, crate::db::DbState>) -> Result<Vec<String>, String> {
    let conn = state.conn.lock().await;
    crate::db::get_all_downloads(&conn).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_save_download(state: tauri::State<'_, crate::db::DbState>, id: String, status: crate::ipc::DownloadStatus, queue_id: String, data: String) -> Result<(), String> {
    let conn = state.conn.lock().await;
    crate::db::insert_download(&conn, &id, status.as_str(), &queue_id, &data)
        .map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_delete_download(state: tauri::State<'_, crate::db::DbState>, id: String) -> Result<(), String> {
    let conn = state.conn.lock().await;
    crate::db::delete_download(&conn, &id).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_get_all_queues(state: tauri::State<'_, crate::db::DbState>) -> Result<Vec<String>, String> {
    let conn = state.conn.lock().await;
    crate::db::get_all_queues(&conn).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_save_queue(state: tauri::State<'_, crate::db::DbState>, id: String, data: String) -> Result<(), String> {
    let conn = state.conn.lock().await;
    crate::db::insert_queue(&conn, &id, &data).map_err(|e| e.to_string())
}

#[tauri::command]
async fn db_delete_queue(state: tauri::State<'_, crate::db::DbState>, id: String) -> Result<(), String> {
    let conn = state.conn.lock().await;
    crate::db::delete_queue(&conn, &id).map_err(|e| e.to_string())
}
