zv 0.9.0

Ziglang Version Manager and Project Starter
use std::collections::HashMap;

use crate::{
    ResolvedZigVersion, ZigVersion, ZvError,
    app::{App, Either},
    cli::r#use::resolve_zig_version,
};
use color_eyre::eyre::{Context, Result, eyre};
use yansi::Paint;

/// Main entry point for the install command
pub(crate) async fn install_versions(
    zig_versions: Vec<ZigVersion>,
    app: &mut App,
    force_ziglang: bool,
) -> Result<()> {
    if zig_versions.is_empty() {
        return Err(eyre!(
            "At least one version must be specified. e.g., 'zv install latest' or 'zv install 0.15.1,master'"
        ));
    }

    let is_single_version = zig_versions.len() == 1;
    let should_set_active = is_single_version && app.toolchain_manager.installations_empty();

    // Deduplicate semver variants before resolution
    // e.g., latest@0.14.0, stable@0.14.0, 0.14.0 all become just 0.14.0
    let zig_versions = crate::tools::deduplicate_semver_variants(zig_versions);

    // First, resolve all versions to detect duplicates and store their Either objects
    // This also fetches the actual version for master/stable/latest variants
    let mut resolved_map: HashMap<ResolvedZigVersion, Either> = HashMap::new();
    let mut resolution_errors: Vec<(ZigVersion, ZvError)> = Vec::new();

    for zig_version in zig_versions {
        match resolve_zig_version(app, &zig_version).await {
            Ok(resolved) => {
                // Get the Either that was set by resolve_zig_version
                let install_either = app.to_install.take().ok_or_else(|| {
                    resolution_errors.push((
                        zig_version,
                        ZvError::ZigVersionResolveError(eyre!(
                            "Internal Error: App failed to set to_install for zig version.",
                        )),
                    ));
                });
                if install_either.is_err() {
                    continue;
                }
                resolved_map
                    .entry(resolved)
                    .or_insert(install_either.unwrap());
            }
            Err(e) => {
                let error_msg = match e {
                    ZvError::ZigVersionResolveError(err) => ZvError::ZigVersionResolveError(eyre!(
                        "Failed to resolve version '{}': {}. Try running 'zv sync' to update the index or 'zv list' to see available versions.",
                        zig_version,
                        err
                    )),
                    _ => e,
                };
                eprintln!(
                    "❌ Failed to resolve {}: {}",
                    Paint::red(&zig_version.to_string()),
                    error_msg
                );
                resolution_errors.push((zig_version, error_msg));
            }
        }
    }

    // If all resolutions failed, return early
    if resolved_map.is_empty() {
        return Err(eyre!("Failed to resolve any versions"));
    }

    let resolved_versions: Vec<_> = resolved_map.keys().collect();
    if should_set_active {
        println!(
            "📦 Installing {} (will be set as active zig)...",
            Paint::blue(&resolved_versions[0].to_string())
        );
    } else if is_single_version {
        println!(
            "📦 Installing {}...",
            Paint::blue(&resolved_versions[0].to_string())
        );
    }
    println!(
        "📦 Installing {} version(s)...",
        Paint::blue(&resolved_map.keys().len().to_string())
    );

    let mut installed_versions = Vec::new();
    let mut failed_versions = Vec::new();

    // Process each unique resolved version
    for (resolved_version, install_either) in resolved_map {
        match install_resolved_version(
            &resolved_version,
            install_either,
            app,
            force_ziglang,
            should_set_active,
        )
        .await
        {
            Ok(()) => {
                installed_versions.push(resolved_version);
            }
            Err(e) => {
                eprintln!(
                    "❌ Failed to install {}: {}",
                    Paint::red(&resolved_version.to_string()),
                    e
                );
                failed_versions.push((resolved_version, e));
            }
        }
    }

    // Report results
    if !installed_versions.is_empty() {
        println!();
        for resolved in &installed_versions {
            if should_set_active {
                println!(
                    "✅ Installed and activated: {}",
                    Paint::green(&resolved.version().to_string())
                );
            } else {
                println!(
                    "✅ Installed: {}",
                    Paint::green(&resolved.version().to_string())
                );
            }
        }
    }

    if !failed_versions.is_empty() {
        println!();
        eprintln!("❌ Failed installations:");
        for (version, _) in &failed_versions {
            eprintln!("{}", Paint::red(&version.to_string()));
        }
    }

    // If all installations failed, return an error
    if installed_versions.is_empty() {
        return Err(eyre!("All version installations failed"));
    }

    Ok(())
}

/// Install a single Zig version that has already been resolved
async fn install_resolved_version(
    resolved_version: &ResolvedZigVersion,
    install_either: Either,
    app: &mut App,
    force_ziglang: bool,
    set_active: bool,
) -> Result<()> {
    // Check if already installed
    if let Some(p) = app.check_installed(resolved_version) {
        if set_active {
            app.set_active_version(resolved_version, Some(p)).await?;
        }
        // Version already installed, just return success
        return Ok(());
    }

    // Set the Either for installation
    app.to_install = Some(install_either.clone());

    // Install based on the Either variant
    match install_either {
        Either::Release(_) => {
            // Install a ZigRelease (resolved from index)
            app.install_release(force_ziglang).await.wrap_err_with(|| {
                format!(
                    "Failed to download and install Zig version {}",
                    resolved_version
                )
            })?;
        }
        Either::Version(_) => {
            // Install a direct ResolvedZigVersion (without index resolution)
            app.install_direct(force_ziglang)
                .await
                .wrap_err_with(|| format!("Failed to install Zig version {}", resolved_version))?;
        }
    }

    // Set as active if this is the special case (single version, no prior installations)
    if set_active {
        app.set_active_version(resolved_version, None).await?;
    }

    Ok(())
}