mod build;
mod layout;
mod plan;
mod wheel;

use crate::cli::{CliError, Result};
use crate::commands::generate::run_generate_python_with_output_from_source_dir;
use crate::commands::pack::PackPythonOptions;
use crate::config::{Config, Target};
use crate::reporter::Reporter;

use self::build::PythonSharedLibraryBuilder;
use self::wheel::PythonWheelBuilder;

pub use self::layout::PythonPackageLayout;
pub use self::plan::{PythonCargoContext, PythonInterpreterSelection, PythonPackagingPlan};

pub(crate) fn pack_python(
    config: &Config,
    options: PackPythonOptions,
    reporter: &Reporter,
) -> Result<()> {
    if !config.is_python_enabled() {
        return Err(CliError::CommandFailed {
            command: "targets.python.enabled = false".to_string(),
            status: None,
        });
    }

    if !config.should_process(Target::Python, options.experimental) {
        return Err(CliError::CommandFailed {
            command:
                "python is experimental, use --experimental flag or add \"python\" to [experimental]"
                    .to_string(),
            status: None,
        });
    }

    reporter.section("🐍", "Packing Python");

    let step = reporter.step("Preparing Python packaging");
    let plan = PythonPackagingPlan::from_config(
        config,
        options.execution.release,
        &options.execution.cargo_args,
        &options.python_interpreters,
    )?;
    let wheel_builder = PythonWheelBuilder::new(&plan)?;
    step.finish_success();

    if options.execution.regenerate {
        let step = reporter.step("Generating Python sources");
        run_generate_python_with_output_from_source_dir(
            config,
            Some(plan.layout.root_directory.clone()),
            plan.generation_source_directory()?,
            plan.generation_crate_name(),
        )?;
        step.finish_success();
    }

    let shared_library_builder = PythonSharedLibraryBuilder::new(&plan);
    let shared_library = if options.execution.no_build {
        let step = reporter.step("Reusing host Rust shared library");
        let shared_library = shared_library_builder.existing()?;
        step.finish_success_with(&format!("{}", shared_library.source_path.display()));
        shared_library
    } else {
        let step = reporter.step("Building host Rust shared library");
        let shared_library = shared_library_builder.build(&step)?;
        step.finish_success_with(&format!("{}", shared_library.source_path.display()));
        shared_library
    };

    let step = reporter.step("Building Python wheel");
    let built_wheel_matrix = wheel_builder.build(&shared_library, &step)?;
    let wheel_summary = built_wheel_matrix
        .wheels
        .iter()
        .map(|built_wheel| {
            format!(
                "{} => {}",
                built_wheel.interpreter,
                built_wheel.wheel_path.display()
            )
        })
        .collect::<Vec<_>>()
        .join(", ");
    step.finish_success_with(&wheel_summary);

    reporter.finish();
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::pack_python;
    use crate::cli::CliError;
    use crate::commands::pack::{PackExecutionOptions, PackPythonOptions};
    use crate::config::{CargoConfig, Config, PackageConfig, PythonConfig, TargetsConfig};
    use crate::reporter::{Reporter, Verbosity};

    fn reporter() -> Reporter {
        Reporter::new(Verbosity::Quiet)
    }

    fn config(enabled: bool, experimental: bool) -> Config {
        Config {
            experimental: experimental
                .then(|| "python".to_string())
                .into_iter()
                .collect(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "demo".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig {
                python: PythonConfig {
                    enabled,
                    ..Default::default()
                },
                ..TargetsConfig::default()
            },
        }
    }

    #[test]
    fn rejects_pack_python_when_target_is_disabled() {
        let error = pack_python(
            &config(false, false),
            PackPythonOptions {
                execution: PackExecutionOptions {
                    release: false,
                    regenerate: false,
                    no_build: true,
                    cargo_args: Vec::new(),
                },
                experimental: false,
                python_interpreters: Vec::new(),
            },
            &reporter(),
        )
        .expect_err("expected disabled python target to fail");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command == "targets.python.enabled = false"
        ));
    }

    #[test]
    fn rejects_pack_python_without_experimental_gate() {
        let error = pack_python(
            &config(true, false),
            PackPythonOptions {
                execution: PackExecutionOptions {
                    release: false,
                    regenerate: false,
                    no_build: true,
                    cargo_args: Vec::new(),
                },
                experimental: false,
                python_interpreters: Vec::new(),
            },
            &reporter(),
        )
        .expect_err("expected experimental gate failure");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("python is experimental")
        ));
    }
}
