//! This is the main entry point for the `rattler-build` binary.

// Use custom allocators for improved performance when the `performance` feature is enabled.
// This must be at the crate root to set the global allocator.
#[cfg(feature = "performance")]
use rattler_build_allocator as _;

mod debug;

use std::{
    fs::File,
    io::{self, IsTerminal},
    path::PathBuf,
};

use clap::{CommandFactory, Parser};
use miette::IntoDiagnostic;
use rattler_build::config::{Config, load_default_config};
use rattler_build::{
    build_recipes, bump_recipe,
    console_utils::init_logging,
    debug_recipe, extract_package, get_recipe_path, migrate_recipe,
    opt::{
        App, BuildData, BumpRecipeOpts, DebugData, DebugSubCommands, MigrateRecipeOpts,
        PackageCommands, PublishData, RebuildData, ShellCompletion, SubCommands, TestData,
    },
    publish_packages, rebuild, run_test, show_package_info,
    tool_configuration::APP_USER_AGENT,
};
use rattler_upload::upload_from_args;
use tempfile::{TempDir, tempdir};

/// Run the bump-recipe command
async fn run_bump_recipe(opts: BumpRecipeOpts) -> miette::Result<()> {
    // Resolve recipe path
    let recipe_path = get_recipe_path(&opts.recipe)?;

    // Create a simple HTTP client
    let client = reqwest::Client::builder()
        .user_agent(APP_USER_AGENT)
        .referer(false)
        .build()
        .into_diagnostic()?;

    if opts.check_only {
        // Only check for updates
        match bump_recipe::check_for_updates(&recipe_path, &client, opts.include_prerelease).await {
            Ok(Some(new_version)) => {
                tracing::info!("New version available: {}", new_version);
            }
            Ok(None) => {
                tracing::info!("No new version available");
            }
            Err(e) => {
                return Err(miette::miette!("Failed to check for updates: {}", e));
            }
        }
    } else {
        // Bump the recipe
        match bump_recipe::bump_recipe(
            &recipe_path,
            opts.version.as_deref(),
            &client,
            opts.include_prerelease,
            opts.dry_run,
            opts.keep_build_number,
        )
        .await
        {
            Ok(result) => {
                tracing::debug!("Provider: {:?}", result.provider);
                tracing::debug!(
                    "SHA256 changes: {:?} -> {:?}",
                    result.old_sha256,
                    result.new_sha256
                );
            }
            Err(bump_recipe::BumpRecipeError::NoNewVersion(v)) => {
                tracing::info!("Recipe is already at the latest version ({})", v);
            }
            Err(e) => {
                return Err(miette::miette!("Failed to bump recipe: {}", e));
            }
        }
    }

    Ok(())
}

/// Run the migrate-recipe command
fn run_migrate_recipe(opts: MigrateRecipeOpts) -> miette::Result<()> {
    let recipe_path = get_recipe_path(&opts.recipe)?;

    match migrate_recipe::migrate_recipe(&recipe_path, opts.dry_run) {
        Ok(_) => {
            if !opts.dry_run {
                tracing::info!("Recipe migrated successfully: {}", recipe_path.display());
            }
        }
        Err(migrate_recipe::MigrateRecipeError::NoCacheKey) => {
            tracing::info!(
                "Recipe does not use the deprecated 'cache:' format — no migration needed"
            );
        }
        Err(e) => {
            return Err(miette::miette!("Failed to migrate recipe: {}", e));
        }
    }

    Ok(())
}

fn main() -> miette::Result<()> {
    let in_ci = matches!(std::env::var("CI").as_deref(), Ok("1" | "true"));
    let no_wrap = matches!(
        std::env::var("RATTLER_BUILD_NO_WRAP").as_deref(),
        Ok("1" | "true")
    );
    miette::set_hook(Box::new(move |_| {
        Box::new(
            miette::MietteHandlerOpts::new()
                .wrap_lines(!in_ci && !no_wrap)
                .build(),
        )
    }))?;

    // Stack size varies significantly across platforms:
    // - Windows: only 1MB by default
    // - macOS/Linux: ~8MB by default
    //
    // This discrepancy causes stack overflows primarily on Windows, especially in debug builds
    // To address this, we spawn another main thread (main2) with a consistent
    // larger stack size across all platforms.
    //
    // 4MB is sufficient for most operations while remaining memory-efficient.
    // If needed, developers should/can override with RUST_MIN_STACK environment variable.
    // Further, we preserve error messages from main thread in case something goes wrong.
    const STACK_SIZE: usize = 4 * 1024 * 1024;

    let thread_handle = std::thread::Builder::new()
        .stack_size(STACK_SIZE)
        .spawn(|| {
            // Create and run the tokio runtime
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap()
                .block_on(async { async_main().await })
        })
        .map_err(|e| miette::miette!("Failed to spawn thread: {}", e))?;

    thread_handle
        .join()
        .map_err(|_| miette::miette!("Thread panicked"))?
}

/// Load the configuration according to the command-line options and print a
/// startup line with the version and the configuration files that were used.
///
/// Only subcommands that consume the configuration call this, so commands
/// like `completion` produce no output besides their actual result.
fn load_config(no_config: bool, config_file: Option<PathBuf>) -> miette::Result<Option<Config>> {
    let config = if no_config {
        // `--no-config` disables all loading and discovery: only built-in
        // defaults and command-line arguments apply (the behavior before
        // default config discovery was added).
        None
    } else if let Some(config_path) = config_file {
        // An explicitly passed configuration file disables the automatic
        // discovery and is used as-is.
        Some(Config::load_from_files(&[config_path]).into_diagnostic()?)
    } else {
        // Otherwise, load and merge the configuration from the default
        // locations (system-wide and global pixi configuration as well as
        // rattler-build's own configuration files).
        load_default_config().into_diagnostic()?
    };

    // Print a startup line so users can always see which version is running
    // and exactly which configuration files were loaded (if any), making
    // config resolution easy to trace. Shown at the default log level.
    tracing::info!("rattler-build {}", env!("CARGO_PKG_VERSION"));
    match config.as_ref() {
        Some(config) if !config.loaded_from.is_empty() => tracing::info!(
            "Loaded configuration from: {}",
            config
                .loaded_from
                .iter()
                .map(|p| p.display().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        ),
        _ => tracing::info!("No configuration file loaded"),
    }

    Ok(config)
}

async fn async_main() -> miette::Result<()> {
    let app = App::parse();
    let log_handler = init_logging(
        &app.log_style,
        &app.verbose,
        &app.color,
        app.wrap_log_lines,
        None::<fn() -> std::io::Stderr>,
    )
    .into_diagnostic()?;

    match app.subcommand {
        Some(SubCommands::Completion(ShellCompletion { shell })) => {
            let mut cmd = App::command();
            fn print_completions<G: clap_complete::Generator>(
                generator: G,
                cmd: &mut clap::Command,
            ) {
                clap_complete::generate(
                    generator,
                    cmd,
                    cmd.get_name().to_string(),
                    &mut std::io::stdout(),
                );
            }

            print_completions(shell, &mut cmd);
            Ok(())
        }
        Some(SubCommands::Build(build_only)) => {
            let config = load_config(app.no_config, app.config_file)?;
            let recipes = build_only.build.recipes.clone();
            let recipe_dir = build_only.build.recipe_dir.clone();
            let mut build_data = BuildData::from_opts_and_config(build_only.build, config);
            build_data.build_num_override = build_only.build_num;
            build_data.build_string_prefix = build_only.build_string_prefix;

            // Get all recipe paths and keep tempdir alive until end of the function
            let (recipe_paths, _temp_dir) = recipe_paths(recipes, recipe_dir.as_ref())?;
            if recipe_paths.is_empty() {
                if recipe_dir.is_some() {
                    tracing::warn!("No recipes found in recipe directory: {:?}", recipe_dir);
                    return Ok(());
                } else {
                    miette::bail!("Couldn't find recipe.")
                }
            }

            build_recipes(recipe_paths, build_data, &Some(log_handler)).await
        }

        Some(SubCommands::Publish(publish_args)) => {
            let config = load_config(app.no_config, app.config_file)?;
            let publish_data = PublishData::from_opts_and_config(publish_args, config);
            publish_packages(publish_data, &Some(log_handler)).await
        }

        Some(SubCommands::Test(test_args)) => {
            let config = load_config(app.no_config, app.config_file)?;
            run_test(
                TestData::from_opts_and_config(test_args, config),
                Some(log_handler),
            )
            .await
        }
        Some(SubCommands::Rebuild(rebuild_args)) => {
            let config = load_config(app.no_config, app.config_file)?;
            rebuild(
                RebuildData::from_opts_and_config(rebuild_args, config),
                log_handler,
            )
            .await
        }
        Some(SubCommands::Upload(upload_args)) => upload_from_args(upload_args).await,
        #[cfg(feature = "recipe-generation")]
        Some(SubCommands::GenerateRecipe(args)) => {
            rattler_build::recipe_generator::generate_recipe(args).await
        }
        Some(SubCommands::Auth(args)) => rattler::cli::auth::execute(args).await.into_diagnostic(),
        Some(SubCommands::Debug(args)) => match args.subcommand {
            DebugSubCommands::Setup(opts) => {
                let config = load_config(app.no_config, app.config_file)?;
                let debug_data = DebugData::from_setup_opts_and_config(opts, config);
                debug_recipe(debug_data, &Some(log_handler)).await
            }
            DebugSubCommands::Shell(opts) => debug::debug_shell(opts).into_diagnostic(),
            DebugSubCommands::HostAdd(opts) => {
                let config = load_config(app.no_config, app.config_file)?;
                debug::debug_env_add("host", opts, config, &Some(log_handler)).await
            }
            DebugSubCommands::BuildAdd(opts) => {
                let config = load_config(app.no_config, app.config_file)?;
                debug::debug_env_add("build", opts, config, &Some(log_handler)).await
            }
            DebugSubCommands::Workdir(opts) => debug::debug_workdir(opts).into_diagnostic(),
            DebugSubCommands::Run(opts) => {
                let exit_code = debug::debug_run(opts).into_diagnostic()?;
                if exit_code != 0 {
                    std::process::exit(exit_code);
                }
                Ok(())
            }
            DebugSubCommands::CreatePatch(opts) => debug::debug_create_patch(opts),
        },
        Some(SubCommands::Package(cmd)) => match cmd {
            PackageCommands::Inspect(opts) => show_package_info(opts),
            PackageCommands::Extract(opts) => extract_package(opts).await,
        },
        Some(SubCommands::BumpRecipe(opts)) => run_bump_recipe(opts).await,
        Some(SubCommands::MigrateRecipe(opts)) => run_migrate_recipe(opts),
        None => {
            _ = App::command().print_long_help();
            Ok(())
        }
    }
}

fn recipe_paths(
    recipes: Vec<PathBuf>,
    recipe_dir: Option<&PathBuf>,
) -> Result<(Vec<PathBuf>, Option<TempDir>), miette::Error> {
    let mut recipe_paths = Vec::new();
    let mut temp_dir_opt = None;
    if !std::io::stdin().is_terminal()
        && recipes.len() == 1
        && get_recipe_path(&recipes[0]).is_err()
    {
        let temp_dir = tempdir().into_diagnostic()?;

        let recipe_path = temp_dir.path().join("recipe.yaml");
        io::copy(
            &mut io::stdin(),
            &mut File::create(&recipe_path).into_diagnostic()?,
        )
        .into_diagnostic()?;
        recipe_paths.push(get_recipe_path(&recipe_path)?);
        temp_dir_opt = Some(temp_dir);
    } else {
        for recipe_path in &recipes {
            recipe_paths.push(get_recipe_path(recipe_path)?);
        }
        if let Some(recipe_dir) = &recipe_dir {
            for entry in ignore::Walk::new(recipe_dir) {
                let entry = entry.into_diagnostic()?;
                if entry.path().is_dir()
                    && let Ok(recipe_path) = get_recipe_path(entry.path())
                {
                    recipe_paths.push(recipe_path);
                }
            }
            // Sort to ensure deterministic ordering across platforms/filesystems
            recipe_paths.sort();
        }
    }

    Ok((recipe_paths, temp_dir_opt))
}
