use crate::build::{build, clear_build_log};
use crate::colors::get_colors;
use crate::config::get_config;
use crate::download::download;
use crate::install::install;
use crate::load_data::load_data_json;
use crate::options::Options;
use crate::parse::PackageData;
use crate::requires::resolve_requires;
use crate::utils::{
    built_package_path, confirm, get_installed_packages, is_installed, is_installed_with_version,
    package_exists, prepare_packages, run_command,
};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::exit;
use std::slice;

pub struct BuildPackages {
    pub prgnam: String,
    pub version: String,
}

/// Resolves dependencies, downloads sources, builds and optionally installs each package.
///
/// When `action` is `"build"`, only builds. For all other actions (`"install"`,
/// `"reinstall"`, `"upgrade"`), each package is installed immediately after it is built.
/// Skips already-installed dependencies when `options.skip_installed` is set,
/// and skips dependency resolution entirely when `options.no_deps` is set.
///
/// # Exits
///
/// Exits with a non-zero code if any download, build or install step fails.
pub fn process(packages: &[String], options: &Options, action: &str) {
    let packages = package_exists(packages);
    let packages = prepare_packages(&packages);
    let installed = get_installed_packages();

    let cfg = get_config();
    let color = get_colors(cfg.colors);

    let repo = load_data_json();
    let mut all_deps: Vec<String> = Vec::new();
    let mut build_packages = Vec::new();

    let out_path = Path::new(&cfg.var_log_path).join(&cfg.deps_log_file);
    let mut deps_log: HashMap<String, Vec<String>> = if out_path.exists() {
        let data = fs::read_to_string(&out_path).unwrap_or_default();
        serde_json::from_str(&data).unwrap_or_default()
    } else {
        HashMap::new()
    };

    if !options.no_deps {
        for pkg in &packages {
            let requires = resolve_requires(pkg, &repo);
            for dep in &requires {
                if !all_deps.contains(dep) {
                    if options.skip_installed && is_installed(dep, &installed) {
                        continue;
                    }
                    all_deps.push(dep.to_string());
                }
            }
        }
    }

    let requested: Vec<String> = packages
        .iter()
        .filter(|p| !all_deps.contains(*p))
        .cloned()
        .collect();

    let total = all_deps.len() + requested.len();
    println!(
        "{}Package(s) ({}) to {}:{}",
        color.bold, total, action, color.reset
    );

    if !all_deps.is_empty() {
        println!(
            "{}Dependencies ({}):{}",
            color.cyan,
            all_deps.len(),
            color.reset
        );
        view_packages(&all_deps, &repo, &installed);
    }

    for pkg in &packages {
        if !all_deps.contains(pkg) {
            all_deps.push(pkg.clone());
        }
    }
    println!(
        "\n{}Requested ({}):{}",
        color.cyan,
        requested.len(),
        color.reset
    );

    view_packages(&requested, &repo, &installed);

    for pkg in &all_deps {
        if let Some(pkg_data) = repo.get(pkg) {
            build_packages.push(BuildPackages {
                prgnam: pkg.clone(),
                version: pkg_data.version.clone(),
            });
        }
    }
    println!("\nTotal download: {} packages", all_deps.len());
    confirm("Continue? [Y/n]: ", options.no_confirm);
    println!();

    clear_build_log();

    let total = build_packages.len();
    for (i, pkg) in build_packages.iter().enumerate() {
        println!(
            "\n{}[{}/{}] Building {}-{}{}\n",
            color.yellow,
            i + 1,
            total,
            pkg.prgnam,
            pkg.version,
            color.reset
        );
        download(slice::from_ref(&pkg.prgnam), options, &cfg.build_path);

        let output_dir = build(&pkg.prgnam);
        let pkg_path = built_package_path(&output_dir);

        if action != "build" {
            install(&pkg.prgnam, action, &pkg_path);

            // Let's tracking packages and dependencies in a json file
            let requires = resolve_requires(&pkg.prgnam, &repo);
            deps_log.insert(pkg.prgnam.to_string(), requires.clone());

            let json = serde_json::to_string_pretty(&deps_log).unwrap_or_else(|e| {
                eprintln!(
                    "{}Error:{} Failed to serialize: {}",
                    color.red, color.reset, e
                );
                exit(1);
            });

            let out_path = Path::new(&cfg.var_log_path).join(&cfg.deps_log_file);
            fs::write(&out_path, json).unwrap_or_else(|e| {
                eprintln!(
                    "{}Error:{} Failed to write {}: {}",
                    color.red, color.reset, cfg.deps_log_file, e
                );
                exit(1);
            });
        }

        // Copy package to /tmp
        let cp_args = vec![pkg_path.clone(), "/tmp".to_string()];
        run_command("cp", &cp_args);

        view_move(pkg_path);

        // Remove tempdir directory
        let rm_args = vec!["-rf".to_string(), output_dir.display().to_string()];
        run_command("rm", &rm_args);
    }
}

/// Prints the move package to the /tmp folder.
fn view_move(pkg_path: String) {
    let filename_osstr = Path::new(&pkg_path).file_name().unwrap();
    let filename = filename_osstr.to_string_lossy();

    let msg = format!("Moved: {} to the /tmp folder", filename);
    println!();
    println!("+{}", "=".repeat(msg.len() + 1));
    println!("| {msg}");
    println!("+{}", "=".repeat(msg.len() + 1));
    println!();
}

/// Prints a labeled list of packages with their versions.
fn view_packages(
    packages: &[String],
    repo: &HashMap<String, PackageData>,
    installed: &Vec<String>,
) {
    let cfg = get_config();
    let color = get_colors(cfg.colors);

    let max_name_len = packages.iter().map(String::len).max().unwrap_or(0);

    let max_ver_len = packages
        .iter()
        .filter_map(|p| repo.get(p))
        .map(|d| d.version.len())
        .max()
        .unwrap_or(0);

    for pkg in packages {
        if let Some(d) = repo.get(pkg) {
            let installed_label = if is_installed_with_version(pkg, &d.version, installed) {
                format!(" {}[installed]{}", color.green, color.reset)
            } else {
                String::new()
            };
            println!(
                "  {:<max_name_len$}  {:<max_ver_len$}  {}",
                pkg, d.version, installed_label,
            );
        }
    }
}
