use crate::blacklist::get_blacklist;
use crate::colors::get_colors;
use crate::config::get_config;
use crate::load_data::load_data_json;
use crate::options::Options;
use crate::process::process;
use crate::utils::{get_installed_packages, get_user_input, matches_blacklist, split_package};
use std::process::exit;
use version_compare::{compare, Cmp};

struct UpgradablePackage {
    id: i32,
    prgnam: String,
    inst_version: String,
    repo_version: String,
    inst_build: String,
    repo_build: String,
    package: String,
    message: String,
}

/// Compares installed `SBo` packages against the repository index and lists
/// packages that have a newer version available.
///
/// Displays each upgradable package with a status indicator (`UPDATE`, `AHEAD`,
/// `BUILD`, or `CHECK`).
///
/// If `--check` is set, the function returns after displaying the list.
/// Otherwise, unless `--no-confirm` is set, prompts the user to
/// choose packages to exclude from the upgrade by number before proceeding.
pub fn upgrade(options: &Options, action: &str) {
    let installed = get_installed_packages();
    let repo = load_data_json();
    let mut upgradable = Vec::new();

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

    let blacklist = get_blacklist();

    let mut count = 1;
    for inst_pkg in &installed {
        let Some(parts) = split_package(inst_pkg) else {
            continue;
        };

        let Some(pkg_data) = repo.get(&parts.name) else {
            continue;
        };

        if matches_blacklist(&parts.name, &blacklist) {
            continue;
        }

        let Some(message) = version_cmp(
            &parts.version,
            &parts.build,
            &pkg_data.version,
            &pkg_data.build,
            options,
        ) else {
            continue;
        };

        upgradable.push(UpgradablePackage {
            id: count,
            prgnam: parts.name.clone(),
            inst_version: parts.version,
            repo_version: pkg_data.version.clone(),
            inst_build: parts.build,
            repo_build: pkg_data.build.clone(),
            package: pkg_data.package.clone(),
            message: message.clone(),
        });
        count += 1;
    }

    if upgradable.is_empty() {
        println!("All SBo packages are up to date.");
        return;
    }
    println!(
        "Installed SBo packages ({}{}{}):",
        color.yellow,
        installed.len(),
        color.reset
    );

    println!(
        "\n{}Packages available for upgrade ({}):{}\n",
        color.cyan,
        upgradable.len(),
        color.reset
    );

    let max_pkg_len = count_max_pkg_len(&upgradable);
    let max_inst_len = count_max_inst_len(&upgradable);
    print_header(max_pkg_len, max_inst_len);

    let separator_width = 4 + 1 + 8 + 1 + (max_pkg_len + 2) + 1 + (max_inst_len + 2) + 1 + 15;
    println!("{}", "-".repeat(separator_width));

    for pkg in &upgradable {
        let id_str = format!("[{}]", pkg.id);

        let inst_full = format!("{}-{}", pkg.inst_version.trim(), pkg.inst_build.trim());
        let repo_full = format!("{}-{}", pkg.repo_version.trim(), pkg.repo_build.trim());

        let status_colored = match pkg.message.as_str() {
            "UPDATE" => format!("{}{:<8}{}", color.green, pkg.message, color.reset),
            "AHEAD" => format!("{}{:<8}{}", color.yellow, pkg.message, color.reset),
            "BUILD" => format!("{}{:<8}{}", color.cyan, pkg.message, color.reset),
            _ => format!("{:<8}", pkg.message),
        };

        println!(
            "{:<4} {} {:<pkg_w$} {:<inst_w$} {}",
            id_str,
            status_colored,
            pkg.prgnam,
            inst_full,
            repo_full,
            pkg_w = max_pkg_len + 2,
            inst_w = max_inst_len + 2,
        );
    }

    println!();

    if options.check {
        return;
    }

    let choices = if options.no_confirm {
        String::new()
    } else {
        match get_user_input("Choose packages to exclude from upgrade: ") {
            Ok(choice) => choice,
            Err(e) => {
                eprintln!("{}Error:{} with input: {}", color.red, color.reset, e);
                return;
            }
        }
    };

    let packages = packages_for_upgrade(upgradable, &choices);
    process(&packages, options, action);
}

/// Prints the column header row for the upgradable packages table.
fn print_header(max_pkg_len: usize, max_inst_len: usize) {
    println!(
        "{:<4} {:<8} {:<pkg_w$} {:<inst_w$} {:<15}",
        "#",
        "Status",
        "Package",
        "Installed",
        "Repository",
        pkg_w = max_pkg_len + 2,
        inst_w = max_inst_len + 2,
    );
}

/// Returns the column width to use for the package-name field when listing
/// upgradable packages.
///
/// Uses the longest `prgnam` in `upgradable`, clamped to the range `[18, 40]`
/// so very short or very long names do not break the layout. Falls back to
/// `18` when the list is empty.
fn count_max_pkg_len(upgradable: &[UpgradablePackage]) -> usize {
    let max_pkg_len = upgradable
        .iter()
        .map(|p| p.prgnam.len())
        .max()
        .unwrap_or(18)
        .clamp(18, 40);
    max_pkg_len
}

/// Returns the column width to use for the installed-version field when listing
/// upgradable packages.
///
/// Uses the longest `{inst_version}-{inst_build}` string in `upgradable`,
/// clamped to the range `[20, 40]` so very short or very long versions do not
/// break the layout. Falls back to `20` when the list is empty.
fn count_max_inst_len(upgradable: &[UpgradablePackage]) -> usize {
    let max_inst_len = upgradable
        .iter()
        .map(|p| format!("{}-{}", p.inst_version.trim(), p.inst_build.trim()).len())
        .max()
        .unwrap_or(20)
        .clamp(20, 40);
    max_inst_len
}

/// Filters the upgradable package list by removing entries the user chose to exclude,
/// prints the excluded packages, and returns the remaining list packages for upgrade.
///
/// # Arguments
///
/// * `upgradable` - Mutable reference to the list of upgradable packages
/// * `choices` - Comma-separated string of package numbers to exclude
fn packages_for_upgrade(mut upgradable: Vec<UpgradablePackage>, choices: &str) -> Vec<String> {
    if !choices.is_empty() {
        println!("\nExclude packages:\n");

        for package_info in &upgradable {
            let id = package_info.id;
            let repo_package = &package_info.package;
            if choices
                .split([',', ' '])
                .any(|s| s.trim() == id.to_string())
            {
                println!("{id} {repo_package}");
            }
        }
    }

    println!();

    let excluded_ids: Vec<i32> = choices
        .split([',', ' '])
        .filter_map(|s| s.trim().parse().ok())
        .collect();

    upgradable.retain(|p| !excluded_ids.contains(&p.id));

    if upgradable.is_empty() {
        println!("\nNo packages found for upgrade.");
        exit(0);
    }

    let package_names: Vec<String> = upgradable.iter().map(|p| p.prgnam.clone()).collect();
    package_names
}

/// Compares the installed package version with the repository version.
///
/// This function performs a semantic comparison and returns a concise status
/// keyword (max 8 characters) to ensure consistent alignment in the terminal output.
///
/// # Arguments
/// * `inst_ver` - Installed version string (e.g., "1.91.1").
/// * `inst_build` - Installed build number (e.g., "1").
/// * `repo_ver` - Repository version string (e.g., "1.91.1").
/// * `repo_build` - Repository build number (e.g., "2").
///
/// # Returns
/// * `Some(String)` - One of the following status keywords:
///     * `"UPDATE"`  - Repository has a newer semantic version.
///     * `"AHEAD"`   - Installed version is newer than the repository (skipped if `--skip-ahead`).
///     * `"BUILD"`   - Semantic versions are equal, but the full strings differ
///       (indicates a `SlackBuild` revision or tag change).
///     * `"CHECK"`   - An indeterminate state or parsing error occurred.
/// * `None` - If the versions and build strings are identical.
///
/// # Example
/// If `inst` is "1.0.0" and `repo` is "1.0.0" but the full strings are "1.0.0-1"
/// and "1.0.0-2", it returns `Some("BUILD")`.
fn version_cmp(
    inst_ver: &str,
    inst_build: &str,
    repo_ver: &str,
    repo_build: &str,
    options: &Options,
) -> Option<String> {
    let comparison = compare(inst_ver, repo_ver).unwrap_or(Cmp::Eq);

    match comparison {
        Cmp::Gt => {
            if options.skip_ahead {
                None
            } else {
                Some("AHEAD".to_string())
            }
        }
        Cmp::Lt => Some("UPDATE".to_string()),
        Cmp::Eq => {
            if inst_build == repo_build {
                None
            } else {
                Some("BUILD".to_string())
            }
        }
        _ => Some("CHECK".to_string()),
    }
}
