use crate::config_file::{
    load_config_db, load_mut_config_db, save_config_db, JuliaupConfigChannel,
};
use crate::global_paths::GlobalPaths;
#[cfg(not(windows))]
use crate::operations::create_symlink;
use crate::operations::{
    channel_to_name, commit_version_install, download_version_to_temp, install_non_db_version,
    update_version_db,
};
use crate::utils::{print_juliaup_style, JuliaupMessageType};
use crate::versions_file::load_versions_db;
use anyhow::{anyhow, Context, Result};
use regex::Regex;

pub fn run_command_add(channel: &str, paths: &GlobalPaths) -> Result<()> {
    // This regex is dynamically compiled, but its runtime is negligible compared to downloading Julia
    if Regex::new(r"^(?:pr\d+|nightly|\d+\.\d+-nightly)(?:~|$)")
        .unwrap()
        .is_match(channel)
    {
        return add_non_db(channel, paths);
    }

    update_version_db(&Some(channel.to_string()), paths)
        .with_context(|| "Failed to update versions db.")?;
    let version_db =
        load_versions_db(paths).with_context(|| "`add` command failed to load versions db.")?;

    let required_version = &version_db
        .available_channels
        .get(channel)
        .ok_or_else(|| {
            anyhow!(
                "'{}' is not a valid Julia version or channel name.",
                &channel
            )
        })?
        .version;

    // Check whether the channel is already installed before downloading. This
    // read only briefly takes a shared lock, which is released immediately.
    {
        let config_file = load_config_db(paths, None)
            .with_context(|| "`add` command failed to load configuration data.")?;

        if config_file.data.installed_channels.contains_key(channel) {
            eprintln!("'{}' is already installed.", &channel);
            return Ok(());
        }
    }

    // Download and extract the version without holding the configuration lock,
    // so concurrent juliaup processes (and the launcher) are not blocked.
    let downloaded = download_version_to_temp(required_version, &version_db, paths)?;

    // Re-acquire the exclusive lock to commit the installation.
    let mut config_file = load_mut_config_db(paths)
        .with_context(|| "`add` command failed to load configuration data.")?;

    if config_file.data.installed_channels.contains_key(channel) {
        eprintln!("'{}' is already installed.", &channel);
        return Ok(());
    }

    commit_version_install(downloaded, required_version, &mut config_file.data, paths)?;

    config_file.data.installed_channels.insert(
        channel.to_string(),
        JuliaupConfigChannel::SystemChannel {
            version: required_version.clone(),
        },
    );

    if config_file.data.default.is_none() {
        config_file.data.default = Some(channel.to_string());
    }

    #[cfg(not(windows))]
    let create_symlinks = config_file.data.settings.create_channel_symlinks;

    save_config_db(&mut config_file, paths).with_context(|| {
        format!(
            "Failed to save configuration file from `add` command after '{}' was installed at `{}`.",
            channel,
            paths.juliaupconfig.display()
        )
    })?;

    #[cfg(not(windows))]
    if create_symlinks {
        create_symlink(
            &JuliaupConfigChannel::SystemChannel {
                version: required_version.clone(),
            },
            &format!("julia-{}", channel),
            paths,
        )?;
    }

    print_juliaup_style(
        "Add",
        &format!("Installed Julia channel '{}'", channel),
        JuliaupMessageType::Success,
    );

    Ok(())
}

fn add_non_db(channel: &str, paths: &GlobalPaths) -> Result<()> {
    // Check whether the channel is already installed before downloading. This
    // read only briefly takes a shared lock, which is released immediately.
    {
        let config_file = load_config_db(paths, None)
            .with_context(|| "`add` command failed to load configuration data.")?;

        if config_file.data.installed_channels.contains_key(channel) {
            eprintln!("'{}' is already installed.", &channel);
            return Ok(());
        }
    }

    // Warn about security implications of PR builds
    if let Some(caps) = Regex::new(r"^pr(\d+)").unwrap().captures(channel) {
        let pr_number = &caps[1];
        eprintln!(
            "\nWARNING: Note that unmerged PRs may not have been reviewed for security issues etc."
        );
        eprintln!(
            "         Review code at https://github.com/JuliaLang/julia/pull/{}\n",
            pr_number
        );
    }

    // Download and extract the version without holding the configuration lock.
    let name = channel_to_name(channel)?;
    let (config_channel, _used_dmg) = install_non_db_version(channel, &name, paths)?;

    // Re-acquire the exclusive lock to commit the installation.
    let mut config_file = load_mut_config_db(paths)
        .with_context(|| "`add` command failed to load configuration data.")?;

    if config_file.data.installed_channels.contains_key(channel) {
        eprintln!("'{}' is already installed.", &channel);
        return Ok(());
    }

    config_file
        .data
        .installed_channels
        .insert(channel.to_string(), config_channel.clone());

    if config_file.data.default.is_none() {
        config_file.data.default = Some(channel.to_string());
    }

    save_config_db(&mut config_file, paths).with_context(|| {
        format!(
            "Failed to save configuration file from `add` command after '{channel}' was installed at `{}`.",
            paths.juliaupconfig.display()
        )
    })?;

    #[cfg(not(windows))]
    if config_file.data.settings.create_channel_symlinks {
        create_symlink(&config_channel, &format!("julia-{}", channel), paths)?;
    }

    print_juliaup_style(
        "Add",
        &format!("Installed Julia channel '{}'", channel),
        JuliaupMessageType::Success,
    );

    Ok(())
}
