zv 0.10.0

Ziglang Version Manager and Project Starter
use crate::App;
#[cfg(not(target_os = "linux"))]
use crate::shell::setup::{
    InteractiveSetup, SetupContext, apply_user_choices, execute_setup, handle_interactive_error,
    is_recoverable_interactive_error, post_setup_actions, pre_setup_checks,
};
#[cfg(not(target_os = "linux"))]
use color_eyre::eyre::Context as _;
use yansi::Paint;

#[cfg(not(target_os = "linux"))]
/// Print the XDG directory layout table and, if any directories are missing,
/// prompt the user to create them. Returns `false` if the user declined creation.
fn print_dir_table_and_ensure(app: &App) -> crate::Result<bool> {
    use crate::shell::path_utils::check_dir_in_path;
    use std::io::{self, Write};

    let paths = &app.paths;

    // Build table rows: (role, path, status)
    struct Row {
        role: &'static str,
        path: std::path::PathBuf,
    }

    let rows = vec![
        Row { role: "Data  ", path: paths.data_dir.clone() },
        Row { role: "Config", path: paths.config_dir.clone() },
        Row { role: "Cache ", path: paths.cache_dir.clone() },
    ];
    let pub_bin = paths.public_bin_dir.clone();

    // Compute column width for the path column
    let path_width = rows
        .iter()
        .map(|r| r.path.display().to_string().len())
        .chain(pub_bin.iter().map(|p| p.display().to_string().len()))
        .max()
        .unwrap_or(30)
        .max(30);

    let sep = "".repeat(8 + path_width + 14);
    println!();
    println!("{}", Paint::cyan("zv directory layout (XDG Base Directory Specification)").bold());
    println!("{sep}");
    println!("  {:<8}  {:<path_width$}  Status", "Role", "Directory");
    println!("{sep}");

    let mut dirs_to_create: Vec<std::path::PathBuf> = Vec::new();

    for row in &rows {
        let status = if row.path.is_dir() {
            Paint::green("✓ exists").to_string()
        } else {
            dirs_to_create.push(row.path.clone());
            Paint::yellow("[will create]").to_string()
        };
        println!(
            "  {:<8}  {:<path_width$}  {}",
            row.role,
            row.path.display(),
            status
        );
    }

    // Public bin row (XDG only)
    if let Some(ref pub_bin_path) = pub_bin {
        let in_path = check_dir_in_path(pub_bin_path);
        let status = if !pub_bin_path.is_dir() {
            dirs_to_create.push(pub_bin_path.clone());
            Paint::yellow("[will create]").to_string()
        } else if in_path {
            Paint::green("✓ in PATH").to_string()
        } else {
            Paint::yellow("exists, not in PATH").to_string()
        };
        println!(
            "  {:<8}  {:<path_width$}  {}",
            "Pub bin",
            pub_bin_path.display(),
            status
        );
    }

    println!("{sep}");
    println!();

    // Prompt for directory creation if needed
    if !dirs_to_create.is_empty() {
        println!("{}", Paint::yellow("Directories to create:"));
        for dir in &dirs_to_create {
            println!("{}", Paint::cyan(&dir.display().to_string()));
        }
        println!();

        if !crate::tools::supports_interactive_prompts() {
            // Non-interactive: create without asking
            for dir in &dirs_to_create {
                std::fs::create_dir_all(dir)?;
            }
        } else {
            print!("Create these directories? [Y/n] ");
            io::stdout().flush().ok();
            let mut input = String::new();
            io::stdin().read_line(&mut input).ok();
            let trimmed = input.trim().to_lowercase();
            if trimmed == "n" || trimmed == "no" {
                println!("{}", Paint::red("Aborted."));
                return Ok(false);
            }
            for dir in &dirs_to_create {
                std::fs::create_dir_all(dir)?;
                println!("  {} Created {}", Paint::green(""), dir.display());
            }
        }
        println!();
    }

    Ok(true)
}

/// Main setup_shell function that orchestrates the three-phase setup process
/// This is the public interface that maintains backward compatibility and supports interactive mode

pub async fn setup_shell(
    #[allow(unused_variables)] app: &mut App,
    #[allow(unused_variables)] using_env_var: bool,
    #[allow(unused_variables)] dry_run: bool,
    #[allow(unused_variables)] no_interactive: bool,
) -> crate::Result<()> {
    // On Linux, zv setup is a no-op — XDG dirs handle everything
    #[cfg(target_os = "linux")]
    {
        println!(
            "{} No setup needed. Your system uses XDG directories. Run {} to initialize.",
            Paint::green(""),
            Paint::blue("zv sync")
        );
        return Ok(());
    }

    // On macOS Tier 1 (XDG dirs exist), same as Linux
    #[cfg(target_os = "macos")]
    if app.paths.tier == 1 && !using_env_var {
        println!(
            "{} No setup needed. Your system uses XDG directories. Run {} to initialize.",
            Paint::green(""),
            Paint::blue("zv sync")
        );
        return Ok(());
    }

    #[cfg(not(target_os = "linux"))]
    {
    if !dry_run {
        let proceed = print_dir_table_and_ensure(app)?;
        if !proceed {
            return Ok(());
        }
    }

    // Check if shell environment is already set up
    if app.source_set {
        println!(
            "{}",
            Paint::white("✓ Shell environment PATH already includes path to zv")
        );

        // Even when shell environment is set up, we need to check if binary needs updating
        // or if shims need regeneration
        let context = SetupContext::new_with_interactive(
            app.shell.clone().unwrap_or_default(),
            app.clone(),
            using_env_var,
            dry_run,
            no_interactive,
        );
        post_setup_actions(&context).await?;
        return Ok(());
    }

    // App::init() for zv_main() ensures shell is always here
    // but in the rare case, fallback to default which calls Shell::detect()
    let shell = app.shell.clone().unwrap_or_default();

    // Create setup context with interactive mode control
    let context = SetupContext::new_with_interactive(
        shell,
        app.clone(),
        using_env_var,
        dry_run,
        no_interactive,
    );

    if dry_run {
        println!(
            "{} zv setup for {} shell...",
            Paint::yellow("Previewing"),
            Paint::cyan(&context.shell.to_string())
        );
    } else {
        println!(
            "Setting up zv for {} shell...",
            Paint::cyan(&context.shell.to_string())
        );
    }

    // Phase 1: Pre-setup checks
    let requirements = pre_setup_checks(&context)
        .await
        .with_context(|| "Pre-setup checks failed")?;

    // Phase 2: Interactive confirmation (default behavior) or fallback to existing behavior
    let final_requirements = if should_use_interactive(&context) {
        let interactive_setup = InteractiveSetup::new(context.clone(), requirements.clone());

        match interactive_setup.run_interactive_flow().await {
            Ok(user_choices) => {
                // Interactive flow succeeded, apply user choices
                apply_user_choices(requirements, user_choices)?
            }
            Err(e) => {
                // Try to downcast the error to ZvError for better handling
                if let Some(zv_error) = e.downcast_ref::<crate::ZvError>() {
                    // Interactive flow failed, check if we can recover
                    if is_recoverable_interactive_error(zv_error) {
                        // Provide clear error message and fallback
                        if let Some(message) = handle_interactive_error(zv_error) {
                            crate::tools::warn(message);
                            crate::tools::warn("Falling back to non-interactive mode");
                        }
                        requirements
                    } else {
                        // User explicitly cancelled or non-recoverable error
                        if let Some(suggestion) = handle_interactive_error(zv_error) {
                            crate::tools::error(suggestion);
                        }
                        return Err(e);
                    }
                } else {
                    // Non-ZvError, don't attempt recovery
                    return Err(e);
                }
            }
        }
    } else {
        // Fallback to existing behavior
        requirements
    };

    // Phase 3: Execute setup based on final requirements
    execute_setup(&context, &final_requirements)
        .await
        .with_context(|| "Setup execution failed")?;

    // Success message
    if dry_run {
        println!("{}", Paint::cyan("→ Dry Run Complete"));
        println!("Run {} to apply these changes", Paint::green("zv setup"));
    } else {
        println!("{}", Paint::green("→ Setup Complete"));
        println!(
            "Restart your shell or run the appropriate source command to apply changes immediately"
        );
    }
    Ok(())
    }
}

#[cfg(not(target_os = "linux"))]
/// Determine if interactive mode should be used based on context and environment
///
/// Interactive mode is automatically disabled when:
/// - `--no-interactive` flag is provided
/// - CI environment is detected (CI environment variable is set)
/// - TERM environment variable is set to "dumb"
/// - TTY is not available for interactive prompts
fn should_use_interactive(context: &SetupContext) -> bool {
    // Don't use interactive mode if explicitly disabled
    if context.no_interactive {
        return false;
    }

    // Don't use interactive mode in CI environments
    if std::env::var("CI").is_ok() {
        return false;
    }

    // Don't use interactive mode if TERM is dumb
    if let Ok(term) = std::env::var("TERM")
        && term == "dumb"
    {
        return false;
    }

    // Check if TTY is available for interactive prompts
    crate::tools::supports_interactive_prompts()
}