mod build;
mod cargo;
mod check;
mod cli;
mod commands;
mod config;
pub mod pack;
mod reporter;
mod target;
mod toolchain;

use clap::{Parser, Subcommand};
use std::{fmt, path::PathBuf};

use cli::{CliError, Result};
use commands::build::{BuildCommandOptions, BuildPlatform};
use commands::check::CheckOptions;
use commands::doctor::{ConfigSummary, DoctorOptions};
use commands::generate::{GenerateOptions, GenerateTarget, run_generate_with_output};
use commands::init::InitOptions;
use commands::pack::{
    PackAllOptions, PackAndroidOptions, PackAppleOptions, PackCSharpOptions, PackCommand,
    PackDartOptions, PackExecutionOptions, PackJavaOptions, PackKmpOptions, PackPythonOptions,
    PackWasmOptions, check_java_packaging_prereqs,
};
use commands::verify::VerifyOptions;
use commands::{run_build, run_check, run_doctor, run_init, run_pack, run_verify};
use config::{Config, Target};

#[derive(Parser)]
#[command(name = "boltffi")]
#[command(about = "BoltFFI - Rust FFI toolchain (Apple + Android + WASM)")]
#[command(
    after_help = "Examples:\n  boltffi init\n  boltffi check --apple\n  boltffi generate swift\n  boltffi generate kmp --experimental\n  boltffi build apple --release\n  boltffi build wasm --release\n  boltffi pack apple --layout bundled\n  boltffi pack wasm --release\n  boltffi pack csharp\n  boltffi --overlay boltffi.ci.toml pack android\n\nConfig:\n  boltffi reads ./boltffi.toml\n  Use --overlay PATH to load a merged overlay config on top of it\n  Settings live under [targets.apple.*], [targets.android.*], [targets.kotlin_multiplatform.*], [targets.wasm.*], [targets.java.*], [targets.dart.*], [targets.python.*], and [targets.csharp.*]\n"
)]
#[command(version)]
struct Cli {
    #[arg(short, long, action = clap::ArgAction::Count, global = true, help = "Increase verbosity (-v, -vv)")]
    verbose: u8,

    #[arg(short, long, global = true, help = "Suppress all output")]
    quiet: bool,

    #[arg(
        long = "cargo-arg",
        global = true,
        action = clap::ArgAction::Append,
        allow_hyphen_values = true,
        value_name = "ARG",
        help = "Pass an argument to cargo invocations (repeatable)"
    )]
    cargo_args: Vec<String>,

    #[arg(
        long,
        global = true,
        value_name = "PATH",
        help = "Optional overlay config file merged on top of the base config"
    )]
    overlay: Option<PathBuf>,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "Create boltffi.toml with sensible defaults")]
    Init {
        #[arg(long)]
        name: Option<String>,
    },

    #[command(
        about = "Check toolchain + required rust targets",
        long_about = "Check toolchain + required rust targets.\n\nIf no platform flags are provided, boltffi checks Apple, Android, and WASM.\n\nExamples:\n  boltffi check\n  boltffi check --apple\n  boltffi check --android\n  boltffi check --wasm\n  boltffi check --fix\n"
    )]
    Check {
        #[arg(long, help = "Install missing rust targets")]
        fix: bool,

        #[arg(long, help = "Check Apple (iOS/iOS-simulator) targets + Xcode tooling")]
        apple: bool,

        #[arg(long, help = "Check Android targets + NDK")]
        android: bool,

        #[arg(long, help = "Check WASM target")]
        wasm: bool,
    },

    #[command(
        about = "Print diagnostic environment info",
        long_about = "Print diagnostic environment info.\n\nExamples:\n  boltffi doctor\n  boltffi doctor --apple\n  boltffi doctor --android\n  boltffi doctor --wasm\n"
    )]
    Doctor {
        #[arg(long)]
        apple: bool,

        #[arg(long)]
        android: bool,

        #[arg(long)]
        wasm: bool,
    },

    #[command(
        about = "Generate bindings",
        long_about = "Generate bindings.\n\nExamples:\n  boltffi generate\n  boltffi generate swift\n  boltffi generate kotlin\n  boltffi generate kmp --experimental\n  boltffi generate header\n  boltffi generate python --experimental\n"
    )]
    Generate {
        #[arg(value_enum)]
        target: Option<GenerateTargetArg>,

        #[arg(
            short,
            long,
            help = "Override output directory (default comes from boltffi.toml)"
        )]
        output: Option<PathBuf>,

        #[arg(long, help = "Enable experimental targets/features")]
        experimental: bool,
    },

    #[command(
        about = "Build rust libraries for targets",
        long_about = "Build rust libraries for targets.\n\nExamples:\n  boltffi build\n  boltffi build apple\n  boltffi build android --release\n  boltffi build wasm --release\n"
    )]
    Build {
        #[arg(value_enum)]
        platform: Option<BuildPlatformArg>,

        #[arg(long)]
        release: bool,
    },

    #[command(
        about = "Package platform artifacts (xcframework/SPM/jniLibs/KMP/npm/NuGet)",
        long_about = "Package platform artifacts.\n\nExamples:\n  boltffi pack apple\n  boltffi pack apple --layout bundled\n  boltffi pack android --release\n  boltffi pack kmp --experimental\n  boltffi pack wasm --release\n  boltffi pack python --experimental\n  boltffi pack csharp\n"
    )]
    Pack {
        #[command(subcommand)]
        target: PackTargetArg,
    },

    #[command(about = "Run check/build/generate/pack in order")]
    Release {
        #[arg(value_enum)]
        platform: Option<BuildPlatformArg>,
    },

    #[command(about = "Verify a generated binding file")]
    Verify {
        path: PathBuf,

        #[arg(long)]
        json: bool,
    },
}

#[derive(Clone, Copy, clap::ValueEnum)]
enum GenerateTargetArg {
    #[value(help = "Generate Swift bindings")]
    Swift,
    #[value(help = "Generate Kotlin bindings + JNI glue")]
    Kotlin,
    #[value(
        alias = "kotlin-multiplatform",
        help = "Generate experimental Kotlin Multiplatform sources"
    )]
    Kmp,
    #[value(help = "Generate Java bindings + JNI glue")]
    Java,
    #[value(help = "Generate C header")]
    Header,
    #[value(help = "Generate TypeScript bindings for WASM")]
    Typescript,
    #[value(help = "Generate experimental Dart bindings")]
    Dart,
    #[value(help = "Generate experimental Python bindings")]
    Python,
    #[value(help = "Generate C# bindings")]
    Csharp,
    #[value(help = "Generate all bindings")]
    All,
}

#[derive(Clone, Copy, clap::ValueEnum)]
enum BuildPlatformArg {
    #[value(help = "Build Apple targets (iOS + iOS-simulator, and macOS if enabled)")]
    Apple,
    #[value(help = "Build Android targets")]
    Android,
    #[value(help = "Build wasm target")]
    Wasm,
    #[value(help = "Build dart target")]
    Dart,
    #[value(help = "Build all configured targets")]
    All,
}

#[derive(Subcommand)]
enum PackTargetArg {
    #[command(about = "Package all enabled targets")]
    All {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long, help = "Include experimental targets/features")]
        experimental: bool,

        #[arg(
            long = "python",
            action = clap::ArgAction::Append,
            value_name = "PYTHON",
            help = "Python interpreter executable or path for Python packaging (repeatable)"
        )]
        python_interpreters: Vec<String>,
    },

    #[command(
        about = "Build + package Apple artifacts",
        long_about = "Build + package Apple artifacts.\n\nOutputs:\n  - xcframework: {targets.apple.xcframework.output}/{Name}.xcframework\n  - SwiftPM:      {targets.apple.spm.output}/Package.swift\n\nLayout:\n  bundled  -> one package with wrapper target\n  ffi-only -> standalone FFI package with Swift target\n  split    -> binary-only package (Swift bindings generated to targets.apple.swift.output)\n"
    )]
    Apple {
        #[arg(long)]
        release: bool,

        #[arg(long)]
        version: Option<String>,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long)]
        spm_only: bool,

        #[arg(long)]
        xcframework_only: bool,

        #[arg(long, value_enum)]
        layout: Option<PackLayoutArg>,
    },

    #[command(
        about = "Build + package Android artifacts",
        long_about = "Build + package Android artifacts.\n\nOutputs:\n  - Kotlin/JNI: {targets.android.kotlin.output}\n  - jniLibs:    {targets.android.pack.output}\n"
    )]
    Android {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long, help = "Enable experimental targets/features")]
        experimental: bool,
    },

    #[command(
        alias = "kotlin-multiplatform",
        about = "Build + package Kotlin Multiplatform native resources",
        long_about = "Build + package Kotlin Multiplatform native resources.\n\nOutputs:\n  - KMP project:           {targets.kotlin_multiplatform.output}\n  - Android jniLibs:       {targets.kotlin_multiplatform.output}/src/androidMain/jniLibs\n  - JVM native resources:  {targets.kotlin_multiplatform.output}/src/jvmMain/resources/native\n"
    )]
    Kmp {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long, help = "Enable experimental targets/features")]
        experimental: bool,
    },

    #[command(
        about = "Build + package WASM artifacts",
        long_about = "Build + package WASM artifacts.\n\nOutputs:\n  - wasm binary: {targets.wasm.npm.output}/{module_name}_bg.wasm\n  - JS/TS files: {targets.wasm.npm.output}\n  - npm metadata: package.json/README.md (when enabled)\n"
    )]
    Wasm {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,
    },

    #[command(
        about = "Build + package Java artifacts",
        long_about = "Build + package Java artifacts.\n\nOutputs:\n  - Java bindings: {targets.java.jvm.output}\n"
    )]
    Java {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,
    },

    #[command(
        about = "Build + package Python artifacts",
        long_about = "Build + package Python artifacts.\n\nOutputs:\n  - Python source package: {targets.python.output}\n  - Wheel output:          defaults to {targets.python.output}/wheelhouse\n\nConfig:\n  - [targets.python].module_name overrides the generated package module\n  - [targets.python.wheel].output overrides the wheel directory\n  - [targets.python.wheel].interpreters or repeated --python values select the Python build matrix\n"
    )]
    Python {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long, help = "Enable experimental targets/features")]
        experimental: bool,

        #[arg(
            long = "python",
            action = clap::ArgAction::Append,
            value_name = "PYTHON",
            help = "Python interpreter executable or path (repeatable)"
        )]
        python_interpreters: Vec<String>,
    },

    #[command(
        about = "Build + package Dart artifacts",
        long_about = "Build + package Dart artifacts.\n\nOutputs:\n  - Dart package: {targets.dart.output}\n"
    )]
    Dart {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,

        #[arg(long, help = "Enable experimental targets/features")]
        experimental: bool,
    },

    #[command(
        about = "Build + package C# artifacts",
        long_about = "Build + package C# artifacts.\n\nOutputs:\n  - C# package project: {targets.csharp.output}\n  - NuGet package:      {targets.csharp.package_output}\n  - Native assets:      runtimes/<rid>/native inside the .nupkg\n"
    )]
    Csharp {
        #[arg(long)]
        release: bool,

        #[arg(long, default_value = "true")]
        regenerate: bool,

        #[arg(long)]
        no_build: bool,
    },
}

#[derive(Clone, Copy, clap::ValueEnum)]
enum PackLayoutArg {
    #[value(help = "Single SwiftPM package with wrapper target + generated sources")]
    Bundled,
    #[value(help = "Binary-only SwiftPM package; generate Swift sources outside package")]
    Split,
    #[value(help = "Standalone FFI SwiftPM package (binary target + generated Swift target)")]
    FfiOnly,
}

fn main() {
    let cli = Cli::parse();
    let config_paths = ConfigPaths::from(&cli);

    let verbosity = if cli.quiet {
        reporter::Verbosity::Quiet
    } else if cli.verbose > 0 {
        reporter::Verbosity::Verbose
    } else {
        reporter::Verbosity::Normal
    };

    let reporter = reporter::Reporter::new(verbosity);
    let result = execute_command(cli.command, &reporter, cli.cargo_args, &config_paths);

    if let Err(err) = result {
        eprintln!("\n{} {}", console::style("error:").red().bold(), err);
        std::process::exit(1);
    }
}

fn execute_command(
    command: Commands,
    reporter: &reporter::Reporter,
    cargo_args: Vec<String>,
    config_paths: &ConfigPaths,
) -> Result<()> {
    match command {
        Commands::Init { name } => {
            let options = resolve_init_options(name, config_paths)?;
            run_init(options).map(|_| ())
        }

        Commands::Check {
            fix,
            apple,
            android,
            wasm,
        } => {
            let explicit_target_selected = apple || android || wasm;
            let config = load_config_if_present(config_paths)?;
            let check_wasm = if explicit_target_selected { wasm } else { true };
            let wasm_target_triple = if check_wasm {
                configured_wasm_target_triple_for_diagnostics(config.as_ref())
            } else {
                None
            };
            let options = CheckOptions {
                fix,
                apple: if explicit_target_selected {
                    apple
                } else {
                    true
                },
                apple_targets: configured_apple_targets_for_diagnostics(config.as_ref()),
                android: if explicit_target_selected {
                    android
                } else {
                    true
                },
                android_targets: configured_android_targets_for_diagnostics(config.as_ref()),
                wasm: check_wasm,
                wasm_target_triple,
            };
            run_check(options).map(|_| ())
        }

        Commands::Doctor {
            apple,
            android,
            wasm,
        } => {
            let explicit_target_selected = apple || android || wasm;
            let doctor_config =
                resolve_doctor_config(load_config_if_present(config_paths), config_paths);
            let diagnostic_config = match &doctor_config.summary {
                ConfigSummary::Loaded(config) => Some(config.as_ref()),
                ConfigSummary::Missing | ConfigSummary::Invalid(_) => None,
            };
            let apple_targets = configured_apple_targets_for_diagnostics(diagnostic_config);
            let android_targets = configured_android_targets_for_diagnostics(diagnostic_config);
            let wasm_target_triple =
                configured_wasm_target_triple_for_diagnostics(diagnostic_config);
            let options = DoctorOptions {
                apple: if explicit_target_selected {
                    apple
                } else {
                    true
                },
                apple_targets,
                android: if explicit_target_selected {
                    android
                } else {
                    true
                },
                android_targets,
                wasm: if explicit_target_selected { wasm } else { true },
                wasm_target_triple,
                config_summary: doctor_config.summary,
                config_path: PathBuf::from("boltffi.toml"),
                overlay_path: config_paths.overlay.clone(),
                config_warning: doctor_config.warning,
            };
            run_doctor(options)
        }

        Commands::Generate {
            target,
            output,
            experimental,
        } => {
            let config = load_config(config_paths)?;
            let options = GenerateOptions {
                target: target
                    .map(|t| match t {
                        GenerateTargetArg::Swift => GenerateTarget::Swift,
                        GenerateTargetArg::Kotlin => GenerateTarget::Kotlin,
                        GenerateTargetArg::Kmp => GenerateTarget::KotlinMultiplatform,
                        GenerateTargetArg::Java => GenerateTarget::Java,
                        GenerateTargetArg::Header => GenerateTarget::Header,
                        GenerateTargetArg::Typescript => GenerateTarget::Typescript,
                        GenerateTargetArg::Dart => GenerateTarget::Dart,
                        GenerateTargetArg::Python => GenerateTarget::Python,
                        GenerateTargetArg::Csharp => GenerateTarget::CSharp,
                        GenerateTargetArg::All => GenerateTarget::All,
                    })
                    .unwrap_or(GenerateTarget::All),
                output,
                experimental,
            };
            run_generate_with_output(&config, options)
        }

        Commands::Build { platform, release } => {
            let config = load_config(config_paths)?;
            let options = BuildCommandOptions {
                platform: platform
                    .map(|p| match p {
                        BuildPlatformArg::Apple => BuildPlatform::Apple,
                        BuildPlatformArg::Android => BuildPlatform::Android,
                        BuildPlatformArg::Wasm => BuildPlatform::Wasm,
                        BuildPlatformArg::Dart => BuildPlatform::Dart,
                        BuildPlatformArg::All => BuildPlatform::All,
                    })
                    .unwrap_or(BuildPlatform::All),
                release,
                cargo_args,
            };
            run_build(&config, options).map(|_| ())
        }

        Commands::Pack { target } => {
            let config = load_config(config_paths)?;
            let command = match target {
                PackTargetArg::All {
                    release,
                    regenerate,
                    no_build,
                    experimental,
                    python_interpreters,
                } => PackCommand::All(PackAllOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                    experimental,
                    python_interpreters,
                }),
                PackTargetArg::Apple {
                    release,
                    version,
                    regenerate,
                    no_build,
                    spm_only,
                    xcframework_only,
                    layout,
                } => PackCommand::Apple(PackAppleOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                    version,
                    spm_only,
                    xcframework_only,
                    layout: layout.map(|l| match l {
                        PackLayoutArg::Bundled => crate::config::SpmLayout::Bundled,
                        PackLayoutArg::Split => crate::config::SpmLayout::Split,
                        PackLayoutArg::FfiOnly => crate::config::SpmLayout::FfiOnly,
                    }),
                }),
                PackTargetArg::Android {
                    release,
                    regenerate,
                    no_build,
                    experimental: _,
                } => PackCommand::Android(PackAndroidOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                }),
                PackTargetArg::Kmp {
                    release,
                    regenerate,
                    no_build,
                    experimental,
                } => PackCommand::Kmp(PackKmpOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                    experimental,
                }),
                PackTargetArg::Wasm {
                    release,
                    regenerate,
                    no_build,
                } => PackCommand::Wasm(PackWasmOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                }),
                PackTargetArg::Java {
                    release,
                    regenerate,
                    no_build,
                } => PackCommand::Java(PackJavaOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                    experimental: false,
                }),
                PackTargetArg::Python {
                    release,
                    regenerate,
                    no_build,
                    experimental,
                    python_interpreters,
                } => PackCommand::Python(PackPythonOptions {
                    execution: pack_execution_options(
                        release,
                        regenerate,
                        no_build,
                        cargo_args.clone(),
                    ),
                    experimental,
                    python_interpreters,
                }),
                PackTargetArg::Dart {
                    release,
                    regenerate,
                    no_build,
                    experimental,
                } => PackCommand::Dart(PackDartOptions {
                    execution: pack_execution_options(release, regenerate, no_build, cargo_args),
                    experimental,
                }),
                PackTargetArg::Csharp {
                    release,
                    regenerate,
                    no_build,
                } => PackCommand::CSharp(PackCSharpOptions {
                    execution: pack_execution_options(release, regenerate, no_build, cargo_args),
                }),
            };
            run_pack(&config, command, reporter)
        }

        Commands::Release { platform } => {
            let config = load_config(config_paths)?;
            run_release(&config, platform, reporter, cargo_args)
        }

        Commands::Verify { path, json } => {
            let options = VerifyOptions { path, json };
            run_verify(options).map(|verified| {
                if !verified {
                    std::process::exit(1);
                }
            })
        }
    }
}

fn pack_execution_options(
    release: bool,
    regenerate: bool,
    no_build: bool,
    cargo_args: Vec<String>,
) -> PackExecutionOptions {
    PackExecutionOptions {
        release,
        regenerate,
        no_build,
        cargo_args,
    }
}

#[derive(Debug, Clone)]
struct ConfigPaths {
    overlay: Option<PathBuf>,
}

impl From<&Cli> for ConfigPaths {
    fn from(cli: &Cli) -> Self {
        Self {
            overlay: cli.overlay.clone(),
        }
    }
}

impl fmt::Display for ConfigPaths {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.overlay {
            Some(overlay) => write!(f, "boltffi.toml with overlay {}", overlay.display()),
            None => f.write_str("boltffi.toml"),
        }
    }
}

fn load_config(config_paths: &ConfigPaths) -> Result<Config> {
    let config_path = PathBuf::from("boltffi.toml");

    if !config_path.exists() {
        return Err(CliError::ConfigNotFound(config_path));
    }

    match config_paths.overlay.as_deref() {
        Some(overlay_path) => Config::load_with_overlay(&config_path, Some(overlay_path)),
        None => Config::load(&config_path),
    }
    .map_err(Into::into)
}

fn load_config_if_present(config_paths: &ConfigPaths) -> Result<Option<Config>> {
    let config_path = PathBuf::from("boltffi.toml");

    if !config_path.exists() {
        if config_paths.overlay.is_some() {
            return Err(CliError::ConfigNotFound(config_path));
        }
        return Ok(None);
    }

    match config_paths.overlay.as_deref() {
        Some(overlay_path) => Config::load_with_overlay(&config_path, Some(overlay_path)),
        None => Config::load(&config_path),
    }
    .map(Some)
    .map_err(Into::into)
}

fn configured_apple_targets_for_diagnostics(
    config: Option<&Config>,
) -> Vec<crate::target::RustTarget> {
    config
        .filter(|config| config.is_apple_enabled())
        .map(|config| config.apple_targets())
        .unwrap_or_else(|| crate::target::RustTarget::ALL_IOS.to_vec())
}

fn configured_android_targets_for_diagnostics(
    config: Option<&Config>,
) -> Vec<crate::target::RustTarget> {
    config
        .filter(|config| config.is_android_enabled())
        .map(|config| config.android_targets())
        .unwrap_or_else(|| crate::target::RustTarget::ALL_ANDROID.to_vec())
}

fn configured_wasm_target_triple_for_diagnostics(config: Option<&Config>) -> Option<String> {
    config.map(|config| config.wasm_triple().to_string())
}

struct DoctorConfig {
    summary: ConfigSummary,
    warning: Option<String>,
}

fn resolve_init_options(name: Option<String>, config_paths: &ConfigPaths) -> Result<InitOptions> {
    if config_paths.overlay.is_some() {
        return Err(CliError::CommandFailed {
            command: "--overlay is not supported with init".to_string(),
            status: None,
        });
    }

    Ok(InitOptions {
        name,
        path: std::env::current_dir().unwrap_or_default(),
    })
}

fn resolve_doctor_config(
    config_result: Result<Option<Config>>,
    config_paths: &ConfigPaths,
) -> DoctorConfig {
    match config_result {
        Ok(Some(config)) => DoctorConfig {
            summary: ConfigSummary::Loaded(Box::new(config)),
            warning: None,
        },
        Ok(None) => DoctorConfig {
            summary: ConfigSummary::Missing,
            warning: None,
        },
        Err(CliError::ConfigNotFound(_)) => DoctorConfig {
            summary: ConfigSummary::Missing,
            warning: Some(format!(
                "failed to load config {} ({}); using default Apple/Android/WASM target checks",
                config_paths,
                CliError::ConfigNotFound(PathBuf::from("boltffi.toml"))
            )),
        },
        Err(error) => DoctorConfig {
            summary: ConfigSummary::Invalid(error.to_string()),
            warning: Some(format!(
                "failed to load config {} ({}); using default Apple/Android/WASM target checks",
                config_paths, error
            )),
        },
    }
}

fn run_release(
    config: &Config,
    platform: Option<BuildPlatformArg>,
    reporter: &reporter::Reporter,
    cargo_args: Vec<String>,
) -> Result<()> {
    reporter.section("🚀", "Running full release pipeline");

    let check_options = CheckOptions {
        fix: false,
        apple: config.is_apple_enabled(),
        apple_targets: config.apple_targets(),
        android: config.is_android_enabled(),
        android_targets: config.android_targets(),
        wasm: config.is_wasm_enabled(),
        wasm_target_triple: Some(config.wasm_triple().to_string()),
    };

    println!("[1/4] Checking environment...");
    let env_ok = run_check(check_options)?;

    if !env_ok {
        println!("Environment check failed. Run 'boltffi check --fix' to install missing targets.");
        return Ok(());
    }

    if release_requires_java_environment_validation(config, platform)
        && let Err(error) = check_java_packaging_prereqs(config, true, &cargo_args)
    {
        println!("JVM packaging preflight failed: {error}");
        return Err(error);
    }
    println!();

    println!("[2/4] Building...");
    let build_options = BuildCommandOptions {
        platform: platform
            .map(|p| match p {
                BuildPlatformArg::Apple => BuildPlatform::Apple,
                BuildPlatformArg::Android => BuildPlatform::Android,
                BuildPlatformArg::Wasm => BuildPlatform::Wasm,
                BuildPlatformArg::Dart => BuildPlatform::Dart,
                BuildPlatformArg::All => BuildPlatform::All,
            })
            .unwrap_or(BuildPlatform::All),
        release: true,
        cargo_args: cargo_args.clone(),
    };
    run_build(config, build_options)?;
    println!();

    println!("[3/4] Generating bindings...");
    run_generate_with_output(
        config,
        GenerateOptions {
            target: GenerateTarget::All,
            output: None,
            experimental: false,
        },
    )?;
    println!();

    println!("[4/4] Packaging...");

    for command in release_pack_commands(config, platform, &cargo_args) {
        run_pack(config, command, reporter)?;
    }

    println!();
    println!("Release complete!");

    Ok(())
}

fn release_pack_commands(
    config: &Config,
    platform: Option<BuildPlatformArg>,
    cargo_args: &[String],
) -> Vec<PackCommand> {
    let mut commands = Vec::new();

    match platform {
        Some(BuildPlatformArg::Apple) => {
            if config.is_apple_enabled() {
                commands.push(PackCommand::Apple(PackAppleOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                    version: None,
                    spm_only: false,
                    xcframework_only: false,
                    layout: None,
                }));
            }
        }
        Some(BuildPlatformArg::Android) => {
            if config.is_android_enabled() {
                commands.push(PackCommand::Android(PackAndroidOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                }));
            }
        }
        Some(BuildPlatformArg::Wasm) => {
            if config.is_wasm_enabled() {
                commands.push(PackCommand::Wasm(PackWasmOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                }));
            }
        }
        Some(BuildPlatformArg::Dart) => {
            if config.is_dart_enabled() {
                commands.push(PackCommand::Dart(PackDartOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                    experimental: true,
                }));
            }
        }
        Some(BuildPlatformArg::All) | None => {
            if config.is_apple_enabled() {
                commands.push(PackCommand::Apple(PackAppleOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                    version: None,
                    spm_only: false,
                    xcframework_only: false,
                    layout: None,
                }));
            }
            if config.is_android_enabled() {
                commands.push(PackCommand::Android(PackAndroidOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                }));
            }
            if config.should_process(Target::KotlinMultiplatform, false) {
                commands.push(PackCommand::Kmp(PackKmpOptions {
                    execution: pack_execution_options(true, true, false, cargo_args.to_vec()),
                    experimental: false,
                }));
            }
            if config.is_wasm_enabled() {
                commands.push(PackCommand::Wasm(PackWasmOptions {
                    execution: pack_execution_options(true, false, true, cargo_args.to_vec()),
                }));
            }
            if config.should_process(Target::Python, false) {
                commands.push(PackCommand::Python(PackPythonOptions {
                    execution: pack_execution_options(true, false, false, cargo_args.to_vec()),
                    experimental: false,
                    python_interpreters: Vec::new(),
                }));
            }
            if config.should_process(Target::Java, false) {
                commands.push(PackCommand::Java(PackJavaOptions {
                    execution: pack_execution_options(true, true, false, cargo_args.to_vec()),
                    experimental: false,
                }));
            }

            if config.should_process(Target::Dart, false) {
                commands.push(PackCommand::Dart(PackDartOptions {
                    execution: pack_execution_options(true, false, false, cargo_args.to_vec()),
                    experimental: false,
                }));
            }

            if config.is_csharp_enabled() {
                commands.push(PackCommand::CSharp(PackCSharpOptions {
                    execution: pack_execution_options(true, true, false, cargo_args.to_vec()),
                }));
            }
        }
    }

    commands
}

fn release_requires_java_environment_validation(
    config: &Config,
    platform: Option<BuildPlatformArg>,
) -> bool {
    matches!(platform, Some(BuildPlatformArg::All) | None)
        && config.should_process(Target::Java, false)
}

#[cfg(test)]
mod tests {
    use super::{
        BuildPlatformArg, Cli, Commands, ConfigPaths, GenerateTargetArg, PackTargetArg,
        configured_android_targets_for_diagnostics, configured_apple_targets_for_diagnostics,
        configured_wasm_target_triple_for_diagnostics, load_config_if_present,
        release_pack_commands, release_requires_java_environment_validation, resolve_doctor_config,
        resolve_init_options,
    };
    use crate::commands::doctor::ConfigSummary;
    use crate::commands::pack::PackCommand;
    use crate::target::RustTarget;
    use crate::{cli::CliError, config::Config};
    use clap::Parser;
    use std::path::PathBuf;

    fn parse_config(input: &str) -> Config {
        let parsed: Config = toml::from_str(input).expect("toml parse failed");
        parsed.validate().expect("config validation failed");
        parsed
    }

    #[test]
    fn diagnostics_ignore_disabled_apple_target_configuration() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.apple]
enabled = false
include_macos = true
ios_architectures = ["arm64"]
simulator_architectures = ["arm64"]
macos_architectures = ["arm64"]
"#,
        );

        assert_eq!(
            configured_apple_targets_for_diagnostics(Some(&config)),
            RustTarget::ALL_IOS.to_vec()
        );
    }

    #[test]
    fn diagnostics_ignore_disabled_android_target_configuration() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.android]
enabled = false
architectures = ["arm64"]
"#,
        );

        assert_eq!(
            configured_android_targets_for_diagnostics(Some(&config)),
            RustTarget::ALL_ANDROID.to_vec()
        );
    }

    #[test]
    fn diagnostics_preserve_wasm_triple_when_target_is_disabled() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.wasm]
enabled = false
triple = "wasm32-wasip1"
"#,
        );

        assert_eq!(
            configured_wasm_target_triple_for_diagnostics(Some(&config)),
            Some("wasm32-wasip1".to_string())
        );
    }

    #[test]
    fn doctor_falls_back_to_defaults_when_config_load_fails() {
        let config_paths = ConfigPaths {
            overlay: Some(PathBuf::from("boltffi.ci.toml")),
        };
        let resolved = resolve_doctor_config(
            Err(crate::config::ConfigError::Validation("bad config".to_string()).into()),
            &config_paths,
        );

        assert!(matches!(resolved.summary, ConfigSummary::Invalid(_)));
        assert!(resolved.warning.as_deref().is_some_and(|warning| {
            warning.contains("using default Apple/Android/WASM target checks")
        }));
    }

    #[test]
    fn missing_base_config_fails_when_overlay_is_requested() {
        let config_paths = ConfigPaths {
            overlay: Some(PathBuf::from("boltffi.ci.toml")),
        };

        let error =
            load_config_if_present(&config_paths).expect_err("overlay without base should fail");

        assert!(
            matches!(error, CliError::ConfigNotFound(path) if path == std::path::Path::new("boltffi.toml"))
        );
    }

    #[test]
    fn doctor_warns_when_overlay_is_requested_without_base_config() {
        let config_paths = ConfigPaths {
            overlay: Some(PathBuf::from("boltffi.ci.toml")),
        };
        let resolved = resolve_doctor_config(
            Err(CliError::ConfigNotFound(PathBuf::from("boltffi.toml"))),
            &config_paths,
        );

        assert!(matches!(resolved.summary, ConfigSummary::Missing));
        assert!(resolved.warning.as_deref().is_some_and(|warning| {
            warning.contains("boltffi.toml with overlay boltffi.ci.toml")
                && warning.contains("config file not found")
        }));
    }

    #[test]
    fn init_uses_current_directory() {
        let config_paths = ConfigPaths { overlay: None };

        let options = resolve_init_options(Some("mylib".to_string()), &config_paths)
            .expect("init options should resolve");

        assert_eq!(options.path, std::env::current_dir().unwrap_or_default());
    }

    #[test]
    fn init_rejects_overlay_config() {
        let config_paths = ConfigPaths {
            overlay: Some(PathBuf::from("boltffi.ci.toml")),
        };

        let error = match resolve_init_options(None, &config_paths) {
            Ok(_) => panic!("overlay should fail"),
            Err(error) => error,
        };
        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command == "--overlay is not supported with init"
        ));
    }

    #[test]
    fn release_all_includes_java_packaging_without_no_build() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.apple]
enabled = true

[targets.android]
enabled = true

[targets.wasm]
enabled = true

[targets.java.jvm]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert_eq!(commands.len(), 4);
        assert!(matches!(
            &commands[0],
            PackCommand::Apple(options) if options.execution.no_build
        ));
        assert!(matches!(
            &commands[1],
            PackCommand::Android(options) if options.execution.no_build
        ));
        assert!(matches!(
            &commands[2],
            PackCommand::Wasm(options) if options.execution.no_build
        ));
        assert!(matches!(
            &commands[3],
            PackCommand::Java(options)
                if !options.execution.no_build
                    && options.execution.release
                    && options.execution.regenerate
                    && !options.experimental
        ));
    }

    #[test]
    fn release_platform_filter_does_not_add_java_for_non_all_platforms() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.apple]
enabled = true

[targets.java.jvm]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::Apple), &[]);

        assert_eq!(commands.len(), 1);
        assert!(matches!(
            &commands[0],
            PackCommand::Apple(options) if options.execution.no_build
        ));
    }

    #[test]
    fn release_all_includes_java_when_enabled() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.java.jvm]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert!(
            commands
                .iter()
                .any(|command| matches!(command, PackCommand::Java(_)))
        );
        assert!(release_requires_java_environment_validation(
            &config,
            Some(BuildPlatformArg::All)
        ));
    }

    #[test]
    fn release_all_includes_kmp_packaging_when_enabled_and_experimental() {
        let config = parse_config(
            r#"
experimental = ["kotlin_multiplatform"]

[package]
name = "mylib"

[targets.kotlin_multiplatform]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert!(commands.iter().any(|command| matches!(
            command,
            PackCommand::Kmp(options)
                if options.execution.release
                    && options.execution.regenerate
                    && !options.execution.no_build
                    && !options.experimental
        )));
    }

    #[test]
    fn release_all_does_not_include_kmp_without_experimental_opt_in() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.kotlin_multiplatform]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert!(
            !commands
                .iter()
                .any(|command| matches!(command, PackCommand::Kmp(_)))
        );
    }

    #[test]
    fn release_all_includes_python_packaging_when_enabled_and_experimental() {
        let config = parse_config(
            r#"
experimental = ["python"]

[package]
name = "mylib"

[targets.python]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert!(commands.iter().any(|command| matches!(
            command,
            PackCommand::Python(options)
                if options.execution.release
                    && !options.execution.regenerate
                    && !options.execution.no_build
                    && !options.experimental
        )));
    }

    #[test]
    fn release_all_includes_csharp_packaging_when_enabled() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.csharp]
enabled = true
"#,
        );

        let commands = release_pack_commands(&config, Some(BuildPlatformArg::All), &[]);

        assert!(commands.iter().any(|command| matches!(
            command,
            PackCommand::CSharp(options)
                if options.execution.release
                    && options.execution.regenerate
                    && !options.execution.no_build
        )));
    }

    #[test]
    fn cli_parses_generate_python_target() {
        let cli = Cli::try_parse_from(["boltffi", "generate", "python", "--experimental"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Generate {
                target: Some(GenerateTargetArg::Python),
                experimental: true,
                ..
            }
        ));
    }

    #[test]
    fn cli_parses_generate_csharp_target_without_experimental_flag() {
        let cli = Cli::try_parse_from(["boltffi", "generate", "csharp"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Generate {
                target: Some(GenerateTargetArg::Csharp),
                experimental: false,
                ..
            }
        ));
    }

    #[test]
    fn cli_parses_generate_kmp_target() {
        let cli = Cli::try_parse_from(["boltffi", "generate", "kmp", "--experimental"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Generate {
                target: Some(GenerateTargetArg::Kmp),
                experimental: true,
                ..
            }
        ));
    }

    #[test]
    fn cli_parses_pack_python_target() {
        let cli = Cli::try_parse_from(["boltffi", "pack", "python", "--experimental"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Pack {
                target: PackTargetArg::Python {
                    experimental: true,
                    ..
                }
            }
        ));
    }

    #[test]
    fn cli_parses_pack_csharp_target() {
        let cli =
            Cli::try_parse_from(["boltffi", "pack", "csharp"]).expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Pack {
                target: PackTargetArg::Csharp { .. }
            }
        ));
    }

    #[test]
    fn cli_parses_pack_android_experimental_flag() {
        let cli = Cli::try_parse_from(["boltffi", "pack", "android", "--experimental"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Pack {
                target: PackTargetArg::Android {
                    experimental: true,
                    ..
                }
            }
        ));
    }

    #[test]
    fn cli_parses_pack_kmp_experimental_flag() {
        let cli = Cli::try_parse_from(["boltffi", "pack", "kmp", "--experimental"])
            .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Pack {
                target: PackTargetArg::Kmp {
                    experimental: true,
                    ..
                }
            }
        ));
    }

    #[test]
    fn cli_parses_pack_python_interpreter_matrix() {
        let cli = Cli::try_parse_from([
            "boltffi",
            "pack",
            "python",
            "--experimental",
            "--python",
            "python3.12",
            "--python",
            "python3.13",
        ])
        .expect("cli parse should succeed");

        assert!(matches!(
            cli.command,
            Commands::Pack {
                target: PackTargetArg::Python {
                    python_interpreters,
                    ..
                }
            } if python_interpreters == vec!["python3.12".to_string(), "python3.13".to_string()]
        ));
    }

    #[test]
    fn release_all_requires_java_environment_validation_when_enabled() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.java.jvm]
enabled = true
"#,
        );

        assert!(release_requires_java_environment_validation(
            &config,
            Some(BuildPlatformArg::All)
        ));
        assert!(release_requires_java_environment_validation(&config, None));
    }

    #[test]
    fn release_platform_filter_skips_java_environment_validation_for_non_all_platforms() {
        let config = parse_config(
            r#"
[package]
name = "mylib"

[targets.java.jvm]
enabled = true
"#,
        );

        assert!(!release_requires_java_environment_validation(
            &config,
            Some(BuildPlatformArg::Apple)
        ));
        assert!(!release_requires_java_environment_validation(
            &config,
            Some(BuildPlatformArg::Android)
        ));
        assert!(!release_requires_java_environment_validation(
            &config,
            Some(BuildPlatformArg::Wasm)
        ));
    }
}
