use std::sync::OnceLock;

use crate::apt::{does_package_exist, install_packages, purge_prefix};
use crate::args::*;
use crate::ini_diff::show_ini_diff;
use crate::io::print_items;
use crate::versioned_package::find_versioned_packages;
use clap::Parser;
use io::read_boolean;
use version::{FallbackVersion, get_installed_php_version, get_latest_php_version, get_version};

mod apt;
mod args;
mod ini_diff;
mod io;
mod version;
mod versioned_package;

static GLOBAL_ARGS: OnceLock<GlobalArgs> = OnceLock::new();

fn get_global_args() -> &'static GlobalArgs {
    GLOBAL_ARGS.get().expect("Global args not set.")
}

fn upgrade(args: UpgradeArgs) -> Result<(), Box<dyn std::error::Error>> {
    let from_version = match args.from {
        Some(version) => Some(version),
        None => get_version("upgrade from", FallbackVersion::Installed)?,
    }
    .ok_or("No upgrade from version given.")?;

    let to_version = match args.to {
        Some(version) => Some(version),
        None => get_version("upgrade to", FallbackVersion::Latest)?,
    }
    .ok_or("No upgrade to version given.")?;

    let modules = find_versioned_packages(&from_version)?;

    print!("Found packages for upgrading: ");
    print_items(&modules);

    let new_packages = modules.iter().map(|package| package.resolve(&to_version));

    let mut to_install = Vec::with_capacity(new_packages.len());

    for package in new_packages {
        let exists = does_package_exist(&package)?;

        if exists {
            to_install.push(package);
            continue;
        }

        if read_boolean(&format!(
            "Could not find package {}, do you want to continue without it?",
            package
        ))? {
            continue;
        }

        return Err(format!("Could not find package {}.", package).into());
    }

    install_packages(&to_install)?;
    println!("\nInstalled new packages, you may update your webserver configuration now.");

    if !get_global_args().non_interactive && !read_boolean("Would you like to see .ini diffs?")? {
        return Ok(());
    }

    show_ini_diff(&from_version, &to_version)?;

    Ok(())
}

fn purge(args: PurgeArgs) -> Result<(), Box<dyn std::error::Error>> {
    let version = match args.version {
        Some(version) => Some(version),
        None => get_version("purge", FallbackVersion::None)?,
    }
    .ok_or("No version given.")?;

    purge_prefix(&format!("php{}", version))
}

fn ini_diff(args: IniDiffArgs) -> Result<(), Box<dyn std::error::Error>> {
    let from_version = match args.from {
        Some(version) => Some(version),
        None => get_version("compare from", FallbackVersion::None)?,
    }
    .ok_or("No diff from version given.")?;

    let to_version = match args.to {
        Some(version) => Some(version),
        None => get_version("compare to", FallbackVersion::Installed)?,
    }
    .ok_or("No diff to version given.")?;

    show_ini_diff(&from_version, &to_version)?;

    Ok(())
}

fn list_packages(args: ListPackagesArgs) -> Result<(), Box<dyn std::error::Error>> {
    let version = match args.version {
        Some(version) => Some(version),
        None => get_version("list packages for", FallbackVersion::Installed)?,
    }
    .ok_or("No version given.")?;

    let modules = find_versioned_packages(&version)?;
    print_items(&modules);

    Ok(())
}

fn run_action<T>(action: &dyn Fn(T) -> Result<(), Box<dyn std::error::Error>>, args: T) -> () {
    if let Err(e) = action(args) {
        eprintln!("Error: {}", e);
    }
}

fn run_opt_string(func: &dyn Fn() -> Option<String>) {
    println!("{}", func().unwrap_or("unknown".to_owned()))
}

fn main() {
    let args = EntryArgs::parse();
    let cmd: ProgramCommands = args
        .command
        .unwrap_or_else(|| ProgramCommands::Upgrade(args.upgrade));

    let _ = GLOBAL_ARGS.set(args.global);

    match cmd {
        ProgramCommands::Upgrade(args) => run_action(&upgrade, args),
        ProgramCommands::Purge(args) => run_action(&purge, args),
        ProgramCommands::IniDiff(args) => run_action(&ini_diff, args),
        ProgramCommands::ListPackages(args) => run_action(&list_packages, args),
        ProgramCommands::LatestVer(_) => run_opt_string(&get_latest_php_version),
        ProgramCommands::InstalledVer(_) => run_opt_string(&get_installed_php_version),
    }
}
