use crate::colors::get_colors;
use crate::config::get_config;
use crate::load_data::load_data_json;
use crate::options::Options;
use crate::utils::{confirm, get_filename_from_url, run_command};
use std::fs;
use std::process::{exit, Command};

/// Downloads sources for each package into `dest_path`.
///
/// Copies the `SlackBuild` directory from the repository, removing any previous
/// destination directory first, then fetches each source URL with `wget`.
/// If the primary URL fails, a fallback download from the configured
/// `sbosrcarch_mirror` is attempted before giving up.
///
/// # Exits
///
/// Exits with a non-zero code if `cp`, GPG verification, or all download
/// attempts (primary and fallback) fail.
pub fn download(packages: &[String], options: &Options, dest_path: &str) {
    let cfg = get_config();
    let color = get_colors(cfg.colors);
    let repo = load_data_json();

    for pkg_name in packages {
        if let Some(pkg_data) = repo.get(pkg_name) {
            let repo_package = format!(
                "{}/{}/{}",
                cfg.repo_path, &pkg_data.category, &pkg_data.prgnam
            );

            let pkg_build_path = format!("{}{}", dest_path, pkg_data.prgnam);

            if std::path::Path::new(&pkg_build_path).exists() {
                fs::remove_dir_all(&pkg_build_path).unwrap_or_else(|e| {
                    eprintln!(
                        "{}Error:{} Failed to remove {}: {}",
                        color.red, color.reset, pkg_build_path, e
                    );
                    exit(1);
                });
            }

            let cp_args = vec!["-r".to_string(), repo_package, dest_path.to_string()];
            run_command("cp", &cp_args);

            if cfg.branch != "current" && (options.gpg || cfg.gpg_verify) {
                let tar_path = format!(
                    "{}/{}/{}.tar.gz",
                    cfg.repo_path, pkg_data.category, pkg_data.prgnam
                );

                let asc_src = format!("{tar_path}.asc");
                gpg_verify(&tar_path, &asc_src);
            }

            let urls = if !pkg_data.download_x86_64.is_empty() && cfg.arch == "x86_64" {
                &pkg_data.download_x86_64
            } else {
                &pkg_data.download
            };

            let expected_md5 = if !pkg_data.md5sum_x86_64.is_empty() && cfg.arch == "x86_64" {
                &pkg_data.md5sum_x86_64
            } else {
                &pkg_data.md5sum
            };

            let urls: Vec<&str> = urls.split_whitespace().collect();
            let md5s: Vec<&str> = expected_md5.split_whitespace().collect();

            for (url, expected) in urls.iter().zip(md5s) {
                let pkg_build_path = format!("{}{}", dest_path, pkg_data.prgnam);

                let mut args = cfg.wget_args.clone();

                args.push("-P".to_string());
                args.push(pkg_build_path.clone());
                args.push(url.to_string());

                let status = downloader("wget", &args);

                if status != 0 {
                    if let Some(name) = get_filename_from_url(url) {
                        let fallback_url = format!(
                            "{}{}/{}/{}",
                            cfg.sbosrcarch_mirror, pkg_data.category, pkg_data.prgnam, name
                        );

                        let mut fallback_args = cfg.wget_args.clone();

                        fallback_args.push("-P".to_string());
                        fallback_args.push(pkg_build_path.clone());
                        fallback_args.push(fallback_url.to_string());

                        let fallback_status = downloader("wget", &fallback_args);

                        if fallback_status != 0 {
                            eprintln!(
                                "{}Error:{} failed to download from fallback: {}",
                                color.red, color.reset, name
                            );
                            exit(1);
                        }
                    }
                }
                if !options.no_checksum && cfg.checksum {
                    checksum(expected, &pkg_build_path, url, options);
                }
            }
        }
    }
}

/// Runs `cmd` with `args` and returns its exit code.
///
/// Unlike [`run_command`], a non-zero exit code does not terminate the process,
/// allowing the caller to decide whether to retry or fall back.
///
/// # Exits
///
/// Exits with code 1 if `cmd` is not found or is terminated by a signal.
fn downloader(cmd: &str, args: &[String]) -> i32 {
    let cfg = get_config();
    let color = get_colors(cfg.colors);

    match Command::new(cmd).args(args).status() {
        Ok(status) => match status.code() {
            Some(code) => {
                if code != 0 {
                    eprintln!(
                        "{}Error:{} {} failed with code: {}",
                        color.red, color.reset, cmd, code
                    );
                }
                code
            }
            None => {
                let err_msg = format!("{} terminated by signal", cmd);
                eprintln!("{}Error:{} {}", color.red, color.reset, err_msg);
                exit(1);
            }
        },
        Err(e) => {
            let err_msg = format!("{} not found: {}", cmd, e);
            eprintln!("{}Error:{} {}", color.red, color.reset, err_msg);
            exit(1);
        }
    }
}

fn checksum(expected_md5: &str, pkg_build_path: &str, url: &str, options: &Options) {
    let cfg = get_config();
    let color = get_colors(cfg.colors);
    let filename = url.split('/').next_back().unwrap_or_default();
    let file_path = format!("{pkg_build_path}/{filename}");

    let data = fs::read(&file_path).unwrap_or_else(|e| {
        eprintln!(
            "{}Error:{} Failed to read {}: {}",
            color.red, color.reset, file_path, e
        );
        exit(1);
    });

    let hash = format!("{:x}", md5::compute(data));
    if hash == expected_md5 {
        println!(
            "{}{}Checksum OK: {}{}\n",
            color.bold, color.green, color.reset, filename
        );
    } else {
        let error_msg = format!(
            "{}Error:{} MD5 mismatch for {}",
            color.red, color.reset, filename
        );
        eprintln!(
            "{}{}{}",
            color.red,
            "=".repeat(error_msg.len()),
            color.reset
        );

        eprintln!("{error_msg}");
        eprintln!("Expected:{} {}{}", color.yellow, expected_md5, color.reset);
        eprintln!("Found:{} {}{}", color.green, hash, color.reset);
        eprintln!(
            "{}{}{}",
            color.red,
            "=".repeat(error_msg.len()),
            color.reset
        );

        confirm("\nContinue? [Y/n]: ", options.no_confirm);
    }
}

fn gpg_verify(file_path: &str, asc_src: &str) {
    let cfg = get_config();
    let color = get_colors(cfg.colors);

    if !std::path::Path::new(asc_src).exists() {
        return;
    }

    let status = Command::new("gpg")
        .args(["--verify", asc_src, file_path])
        .status()
        .unwrap_or_else(|e| {
            eprintln!(
                "{}Error:{} GPG verification failed: {}",
                color.red, color.reset, e
            );
            exit(1);
        });

    if !status.success() {
        eprintln!(
            "{}Error:{} BAD GPG signature for {}",
            color.red, color.reset, file_path
        );
        exit(1);
    }
}
