use std::ffi::OsStr;
use std::fs::{File, create_dir_all, read};
use std::path;
use std::process::exit;

use backends::Arch;
use backends::Backend;
use backends::Cargo;
use backends::Flatpak;
use backends::Rustup;
use backends::Uv;
use backends::{Backends, Error};

use clap::Args;
use clap::Parser;
use clap::Subcommand;
use env_logger::Env;
use parser::Engine;

mod backends;
mod commands;
mod config;
mod error;
mod parser;

/// A nushell based declarative package management utility
#[derive(Parser)]
#[command(version, about)]
struct Arguments {
    /// Path to config-file
    #[arg(short, long)]
    config_dir: Option<path::PathBuf>,
    #[command(subcommand)]
    subcommand: SubCommand,
}

#[derive(Subcommand)]
enum SubCommand {
    Clean(CleanCommand),
    Sync(SyncCommand),
    Update(UpdateCommand),
    Unmanaged(UnmanagedCommand),
    Validate,
    CleanCache(CleanCacheCommand),
}

#[derive(Args)]
#[command(visible_alias("c"))]
/// remove unmanaged packages
struct CleanCommand {
    #[arg(short = 'n', long)]
    /// do not execute commands
    dry_run: bool,
    #[arg(short = 'y', long)]
    /// do not ask for any confirmation
    no_confirm: bool,
}

#[derive(Args)]
#[command(visible_alias("s"))]
/// install packages from groups
struct SyncCommand {
    #[arg(short = 'n', long)]
    /// do not execute commands
    dry_run: bool,
    #[arg(short = 'y', long)]
    /// do not ask for any confirmation
    no_confirm: bool,
}

#[derive(Args)]
#[command(visible_alias("u"))]
/// show explicitly installed packages not managed
struct UnmanagedCommand;

#[derive(Args)]
#[command(visible_alias("b"))]
/// show the backends found and validate their configs
struct ValidateCommand;

#[derive(Args)]
#[command(visible_alias("e"))]
/// clean the caches of all the backends
struct CleanCacheCommand {
    #[arg(short = 'n', long)]
    /// do not execute commands
    dry_run: bool,
    #[arg(short = 'y', long)]
    /// do not ask for any confirmation
    no_confirm: bool,
}

#[derive(Args)]
#[command(visible_alias("U"))]
/// clean the caches of all the backends
struct UpdateCommand {
    #[arg(short = 'n', long)]
    /// do not execute commands
    dry_run: bool,
    #[arg(short = 'y', long)]
    /// do not ask for any confirmation
    no_confirm: bool,
}

fn main() {
    env_logger::Builder::from_env(Env::default().default_filter_or("WARN"))
        .format_timestamp(None)
        .init();

    let args = Arguments::parse();

    if let Err(e) = inner_main(args) {
        log::error!("\n{e}");
        exit(1);
    }
}

fn inner_main(args: Arguments) -> backends::Result<()> {
    let config_file = args
        .config_dir
        .map(Ok)
        .unwrap_or_else(|| {
            log::info!("config path not supplied through arguments. Reading from default path");
            config::get_config_path()
        })
        .map_err(|e| nest_errors!("Failed to read config file", e))?;

    if !config_file.exists() {
        create_dir_all(config_file.parent().unwrap_or(path::Path::new("/"))).map_err(|e| {
            nest_errors!("Failed to create parent directories. While unlikely, it may be possible that their was no parent of the config file.", e)
        })?;

        File::create(&config_file)
            .map_err(|e| nest_errors!("Failed to create file", e))?
            .sync_all()
            .map_err(|e| nest_errors!("Failed to sync file", e))?;

        config::write_default_config(&config_file)
            .map_err(|e| nest_errors!("Error occured while writing the default config.", e))?;
    }

    let config_dir = config_file.parent().unwrap();

    let config_contents = read(&config_file)
        .map_err(|e| nest_errors!("Error occured when reading the config spec", e))?;
    let mut config_engine = Engine::new(config_dir);
    let config = config_engine
        .fetch(&config_contents)
        .map_err(|e| nest_errors!("Error encountered while parsing config spec", e))?;

    let package_nu = [config_dir.as_os_str(), OsStr::new("package.nu")].join(OsStr::new("/"));

    let contents = read(package_nu)
        .map_err(|e| nest_errors!("Error occured when reading the package spec", e))?;

    let mut engine = Engine::new(config_dir);

    let packages = engine
        .fetch(&contents)
        .map_err(|e| nest_errors!("Error encountered while parsing package spec", e))?;

    let backends = parse_all_backends!(packages, config);

    let results = backends.into_iter().flat_map(|backend_opt| {
        backend_opt.map(|backend| process_subcommand(backend, &args.subcommand, &mut engine))
    });

    #[allow(clippy::manual_try_fold)]
    results.fold(Ok(()), |acc, curr| match (acc, curr) {
        (acc, Ok(_)) => acc,
        (Ok(_), curr) => curr,
        (Err(orig), Err(e)) => Err(orig.chain(e)),
    })
}

fn process_subcommand(
    backend: backends::Result<Backends>,
    subcommand: &SubCommand,
    engine: &mut Engine,
) -> backends::Result<()> {
    match subcommand {
        SubCommand::Clean(opts) => backend?.remove(opts),
        SubCommand::Sync(opts) => backend?.install(engine, opts),
        SubCommand::Update(opts) => backend?.update(opts),
        SubCommand::Unmanaged(_opts) => todo!(),
        SubCommand::Validate => backend
            .inspect_err(|e| {
                log::warn!("Validation failed");
                log::warn!("Errors\n{}", e.to_spanned(engine));
            })
            .map(|_| ())
            .or(Ok(())),
        SubCommand::CleanCache(opts) => backend?.clean_cache(opts),
    }
}
