//! Contains builders for the CLI commands
//! We are using a builder pattern here to make it easier to write tests.
//! And are kinda abusing the `IntoFuture` trait to make it easier to execute as
//! close as we can get to the command line args
//!
//! # Using IntoFuture
//!
//! When `.await` is called on an object that is not a `Future` the compiler
//! will first check if the type implements `IntoFuture`. If it does it will
//! call the `IntoFuture::into_future()` method and await the resulting
//! `Future`. We can abuse this behavior in builder patterns because the
//! `into_future` method can also be used as a `finish` function. This allows
//! you to reduce the required code.
//!
//! ```rust
//! impl IntoFuture for InitBuilder {
//!     type Output = miette::Result<()>;
//!     type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'static>>;
//!
//!     fn into_future(self) -> Self::IntoFuture {
//!         Box::pin(init::execute(self.args))
//!     }
//! }
//! ```

use pixi_cli::{
    add, build,
    cli_config::{
        DependencyConfig, GitRev, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig,
    },
    global, init, install, lock, remove, search, task, update, workspace,
};
use pixi_core::DependencyType;
use std::{
    future::{Future, IntoFuture},
    io,
    path::{Path, PathBuf},
    pin::Pin,
    str::FromStr,
};
use typed_path::Utf8NativePathBuf;

use futures::FutureExt;
use pixi_manifest::{EnvironmentName, FeatureName, SpecType, task::Dependency};
use rattler_conda_types::{NamedChannelOrUrl, Platform, RepoDataRecord};
use url::Url;

/// Strings from an iterator
pub fn string_from_iter(iter: impl IntoIterator<Item = impl AsRef<str>>) -> Vec<String> {
    iter.into_iter().map(|s| s.as_ref().to_string()).collect()
}

/// Contains the arguments to pass to [`init::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct InitBuilder {
    pub args: init::Args,
    pub no_fast_prefix: bool,
}

impl InitBuilder {
    /// Disable using `https://prefix.dev` as the default channel.
    pub fn no_fast_prefix_overwrite(self, no_fast_prefix: bool) -> Self {
        Self {
            no_fast_prefix,
            ..self
        }
    }

    pub fn with_channel(mut self, channel: impl ToString) -> Self {
        self.args
            .channels
            .get_or_insert_with(Default::default)
            .push(NamedChannelOrUrl::from_str(channel.to_string().as_str()).unwrap());
        self
    }

    pub fn with_local_channel(mut self, channel: impl AsRef<Path>) -> Self {
        self.args
            .channels
            .get_or_insert_with(Default::default)
            .push(NamedChannelOrUrl::Url(
                Url::from_directory_path(channel).unwrap(),
            ));
        // Disable the pypi mapping
        self.args.conda_pypi_map = Some(Vec::new());
        self
    }

    pub fn without_channels(mut self) -> Self {
        self.args.channels = Some(vec![]);
        self
    }

    /// Instruct init which manifest format to use
    pub fn with_format(mut self, format: init::ManifestFormat) -> Self {
        self.args.format = Some(format);
        self
    }

    pub fn with_platforms(mut self, platforms: Vec<Platform>) -> Self {
        self.args.platforms = platforms.into_iter().map(|p| p.to_string()).collect();
        self
    }
}

impl IntoFuture for InitBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        init::execute(init::Args {
            channels: if !self.no_fast_prefix {
                self.args.channels.or_else(|| {
                    Some(vec![
                        NamedChannelOrUrl::from_str("https://prefix.dev/conda-forge").unwrap(),
                    ])
                })
            } else {
                self.args.channels
            },
            ..self.args
        })
        .boxed_local()
    }
}
/// A trait used by builders to access NoInstallConfig
pub trait HasNoInstallConfig: Sized {
    fn no_install_config(&mut self) -> &mut NoInstallConfig;
    /// Set whether to also install the environment. By default, the environment
    /// is NOT installed to reduce test times.
    fn with_install(mut self, install: bool) -> Self {
        self.no_install_config().no_install = !install;
        self
    }
}

/// A trait used by AddBuilder and RemoveBuilder to set their inner
/// DependencyConfig
pub trait HasLockFileUpdateConfig: Sized {
    fn lock_file_update_config(&mut self) -> &mut LockFileUpdateConfig;

    /// Set the frozen flag to skip lock-file updates
    fn with_frozen(mut self, frozen: bool) -> Self {
        self.lock_file_update_config().lock_file_usage.frozen = frozen;
        self
    }
}

/// A trait used by AddBuilder and RemoveBuilder to set their inner
/// DependencyConfig
pub trait HasDependencyConfig: Sized {
    fn dependency_config(&mut self) -> &mut DependencyConfig;

    fn dependency_config_with_specs(specs: Vec<&str>) -> DependencyConfig {
        DependencyConfig {
            specs: specs.iter().map(|s| s.to_string()).collect(),
            host: false,
            build: false,
            pypi: false,
            platforms: Default::default(),
            feature: Default::default(),
            git: Default::default(),
            rev: Default::default(),
            subdir: Default::default(),
        }
    }

    fn with_spec(mut self, spec: &str) -> Self {
        self.dependency_config().specs.push(spec.to_string());
        self
    }

    /// Set as a host
    fn set_type(mut self, t: DependencyType) -> Self {
        match t {
            DependencyType::CondaDependency(spec_type) => match spec_type {
                SpecType::Host => {
                    self.dependency_config().host = true;
                    self.dependency_config().build = false;
                }
                SpecType::Build => {
                    self.dependency_config().host = false;
                    self.dependency_config().build = true;
                }
                SpecType::Run => {
                    self.dependency_config().host = false;
                    self.dependency_config().build = false;
                }
            },
            DependencyType::PypiDependency => {
                self.dependency_config().host = false;
                self.dependency_config().build = false;
                self.dependency_config().pypi = true;
            }
        }
        self
    }

    fn set_platforms(mut self, platforms: &[Platform]) -> Self {
        self.dependency_config().platforms.extend(platforms.iter());
        self
    }
}

/// Contains the arguments to pass to [`add::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct AddBuilder {
    pub args: add::Args,
}

impl AddBuilder {
    pub fn set_editable(mut self, editable: bool) -> Self {
        self.args.editable = editable;
        self
    }

    pub fn set_pypi(mut self, pypi: bool) -> Self {
        self.args.dependency_config.pypi = pypi;
        self
    }

    pub fn with_feature(mut self, feature: impl ToString) -> Self {
        self.args.dependency_config.feature = FeatureName::from(feature.to_string());
        self
    }

    pub fn with_platform(mut self, platform: Platform) -> Self {
        self.args.dependency_config.platforms.push(platform);
        self
    }

    pub fn with_git_url(mut self, url: Url) -> Self {
        self.args.dependency_config.git = Some(url);
        self
    }

    pub fn with_git_rev(mut self, rev: GitRev) -> Self {
        self.args.dependency_config.rev = Some(rev);
        self
    }

    pub fn with_git_subdir(mut self, subdir: String) -> Self {
        self.args.dependency_config.subdir = Some(subdir);
        self
    }

    /// Deprecated: Use .with_frozen(true).with_install(false) instead
    pub fn with_no_lockfile_update(mut self, no_lockfile_update: bool) -> Self {
        if no_lockfile_update {
            // Since no_lockfile_update is deprecated, we simulate the behavior by setting frozen=true and no_install=true
            self.args.lock_file_update_config.lock_file_usage.frozen = true;
            self.args.no_install_config.no_install = true;
        }
        self
    }

    pub fn with_no_install(mut self, no_install: bool) -> Self {
        self.args.no_install_config.no_install = no_install;
        self
    }
}

impl HasDependencyConfig for AddBuilder {
    fn dependency_config(&mut self) -> &mut DependencyConfig {
        &mut self.args.dependency_config
    }
}

impl HasNoInstallConfig for AddBuilder {
    fn no_install_config(&mut self) -> &mut NoInstallConfig {
        &mut self.args.no_install_config
    }
}

impl HasLockFileUpdateConfig for AddBuilder {
    fn lock_file_update_config(&mut self) -> &mut LockFileUpdateConfig {
        &mut self.args.lock_file_update_config
    }
}

impl IntoFuture for AddBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        add::execute(self.args).boxed_local()
    }
}

/// Contains the arguments to pass to [`search::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct SearchBuilder {
    pub args: search::Args,
}

impl IntoFuture for SearchBuilder {
    type Output = miette::Result<Vec<RepoDataRecord>>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move {
            let mut out = io::stdout();
            search::execute_impl(self.args, &mut out).await
        })
    }
}

/// Contains the arguments to pass to [`remove::execute()`]. Call `.await` to
/// call the CLI execute method and await the result at the same time.
pub struct RemoveBuilder {
    pub args: remove::Args,
}

impl HasDependencyConfig for RemoveBuilder {
    fn dependency_config(&mut self) -> &mut DependencyConfig {
        &mut self.args.dependency_config
    }
}

impl HasNoInstallConfig for RemoveBuilder {
    fn no_install_config(&mut self) -> &mut NoInstallConfig {
        &mut self.args.no_install_config
    }
}

impl IntoFuture for RemoveBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        remove::execute(self.args).boxed_local()
    }
}
pub struct TaskAddBuilder {
    pub manifest_path: Option<PathBuf>,
    pub args: task::AddArgs,
}

impl TaskAddBuilder {
    /// Execute these commands
    pub fn with_commands(mut self, commands: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
        self.args.commands = string_from_iter(commands);
        self
    }

    /// Depends on these commands
    pub fn with_depends_on(mut self, depends: Vec<Dependency>) -> Self {
        self.args.depends_on = Some(depends);
        self
    }

    /// With this working directory
    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
        self.args.cwd = Some(cwd);
        self
    }

    /// With this environment variable
    pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
        self.args.env = env;
        self
    }

    /// Execute the CLI command
    pub async fn execute(self) -> miette::Result<()> {
        task::execute(task::Args {
            operation: task::Operation::Add(self.args),
            workspace_config: WorkspaceConfig {
                manifest_path: self.manifest_path,
                ..Default::default()
            },
        })
        .await
    }
}

pub struct TaskAliasBuilder {
    pub manifest_path: Option<PathBuf>,
    pub args: task::AliasArgs,
}

impl TaskAliasBuilder {
    /// Depends on these commands
    pub fn with_depends_on(mut self, depends: Vec<Dependency>) -> Self {
        self.args.depends_on = depends;
        self
    }

    /// Execute the CLI command
    pub async fn execute(self) -> miette::Result<()> {
        task::execute(task::Args {
            operation: task::Operation::Alias(self.args),
            workspace_config: WorkspaceConfig {
                manifest_path: self.manifest_path,
                ..Default::default()
            },
        })
        .await
    }
}

pub struct ProjectChannelAddBuilder {
    pub workspace_config: WorkspaceConfig,
    pub args: workspace::channel::AddRemoveArgs,
}

impl ProjectChannelAddBuilder {
    /// Adds the specified channel
    pub fn with_channel(mut self, name: impl Into<String>) -> Self {
        self.args
            .channel
            .push(NamedChannelOrUrl::from_str(&name.into()).unwrap());
        self
    }

    pub fn with_priority(mut self, priority: Option<i32>) -> Self {
        self.args.priority = priority;
        self
    }

    /// Alias to add a local channel.
    pub fn with_local_channel(self, channel: impl AsRef<Path>) -> Self {
        self.with_channel(Url::from_directory_path(channel).unwrap())
    }
}

impl IntoFuture for ProjectChannelAddBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        workspace::channel::execute(workspace::channel::Args {
            workspace_config: self.workspace_config,
            command: workspace::channel::Command::Add(self.args),
        })
        .boxed_local()
    }
}

pub struct ProjectChannelRemoveBuilder {
    pub workspace_config: WorkspaceConfig,
    pub args: workspace::channel::AddRemoveArgs,
}

impl ProjectChannelRemoveBuilder {
    /// Removes the specified channel
    pub fn with_channel(mut self, name: impl Into<String>) -> Self {
        self.args
            .channel
            .push(NamedChannelOrUrl::from_str(&name.into()).unwrap());
        self
    }

    /// Alias to Remove a local channel.
    pub fn with_local_channel(self, channel: impl AsRef<Path>) -> Self {
        self.with_channel(Url::from_directory_path(channel).unwrap())
    }
}

impl IntoFuture for ProjectChannelRemoveBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        workspace::channel::execute(workspace::channel::Args {
            workspace_config: self.workspace_config,
            command: workspace::channel::Command::Remove(self.args),
        })
        .boxed_local()
    }
}

/// Contains the arguments to pass to [`install::execute()`]. Call `.await` to
/// call the CLI execute method and await the result at the same time.
pub struct InstallBuilder {
    pub args: install::Args,
}

impl InstallBuilder {
    pub fn with_locked(mut self) -> Self {
        self.args.lock_file_usage.locked = true;
        self
    }
    pub fn with_frozen(mut self) -> Self {
        self.args.lock_file_usage.frozen = true;
        self
    }
    pub fn with_skipped(mut self, names: Vec<String>) -> Self {
        self.args.skip = Some(names);
        self
    }
    pub fn with_skipped_with_deps(mut self, names: Vec<String>) -> Self {
        self.args.skip_with_deps = Some(names);
        self
    }
    pub fn with_only_package(mut self, pkg: Vec<String>) -> Self {
        self.args.only = Some(pkg);
        self
    }

    pub fn with_environment(mut self, env: Vec<String>) -> Self {
        self.args.environment = Some(env);
        self
    }
}

impl IntoFuture for InstallBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;
    fn into_future(self) -> Self::IntoFuture {
        install::execute(self.args).boxed_local()
    }
}

pub struct ProjectEnvironmentAddBuilder {
    pub args: workspace::environment::AddArgs,
    pub manifest_path: Option<PathBuf>,
}

impl ProjectEnvironmentAddBuilder {
    pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
        self.args
            .features
            .get_or_insert_with(Vec::new)
            .push(feature.into());
        self
    }

    pub fn with_no_default_features(mut self, no_default_features: bool) -> Self {
        self.args.no_default_feature = no_default_features;
        self
    }

    pub fn force(mut self, force: bool) -> Self {
        self.args.force = force;
        self
    }

    pub fn with_solve_group(mut self, solve_group: impl Into<String>) -> Self {
        self.args.solve_group = Some(solve_group.into());
        self
    }
}

impl IntoFuture for ProjectEnvironmentAddBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;
    fn into_future(self) -> Self::IntoFuture {
        workspace::environment::execute(workspace::environment::Args {
            workspace_config: WorkspaceConfig {
                manifest_path: self.manifest_path,
                ..Default::default()
            },
            command: workspace::environment::Command::Add(self.args),
        })
        .boxed_local()
    }
}

/// Contains the arguments to pass to [`update::execute()`]. Call `.await` to
/// call the CLI execute method and await the result at the same time.
pub struct UpdateBuilder {
    pub args: update::Args,
}

impl UpdateBuilder {
    pub fn with_package(mut self, package: impl ToString) -> Self {
        self.args
            .specs
            .packages
            .get_or_insert_with(Vec::new)
            .push(package.to_string());
        self
    }

    pub fn with_environment(mut self, env: impl Into<EnvironmentName>) -> Self {
        self.args
            .specs
            .environments
            .get_or_insert_with(Vec::new)
            .push(env.into());
        self
    }

    pub fn with_platform(mut self, platform: Platform) -> Self {
        self.args
            .specs
            .platforms
            .get_or_insert_with(Vec::new)
            .push(platform);
        self
    }

    pub fn dry_run(mut self, dry_run: bool) -> Self {
        self.args.dry_run = dry_run;
        self
    }

    pub fn json(mut self, json: bool) -> Self {
        self.args.json = json;
        self
    }

    pub fn with_no_install(mut self, no_install: bool) -> Self {
        self.args.no_install = no_install;
        self
    }
}

impl IntoFuture for UpdateBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;
    fn into_future(self) -> Self::IntoFuture {
        update::execute(self.args).boxed_local()
    }
}

/// Contains the arguments to pass to [`lock::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct LockBuilder {
    pub args: lock::Args,
}

impl LockBuilder {
    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
        self.args.dry_run = dry_run;
        self
    }
    pub fn with_check(mut self, check: bool) -> Self {
        self.args.check = check;
        self
    }
}

impl IntoFuture for LockBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        lock::execute(self.args).boxed_local()
    }
}

/// Contains the arguments to pass to [`build::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct BuildBuilder {
    pub args: build::Args,
}

impl BuildBuilder {
    /// Set the target platform for the build
    pub fn with_target_platform(mut self, platform: Platform) -> Self {
        self.args.target_platform = platform;
        self
    }

    /// Set the build platform for the build
    pub fn with_build_platform(mut self, platform: Platform) -> Self {
        self.args.build_platform = platform;
        self
    }

    /// Set the output directory for built artifacts
    pub fn with_output_dir(mut self, output_dir: impl Into<PathBuf>) -> Self {
        self.args.output_dir = output_dir.into();
        self
    }

    /// Set the build directory for incremental builds
    pub fn with_build_dir(mut self, build_dir: impl Into<PathBuf>) -> Self {
        self.args.build_dir = Some(build_dir.into());
        self
    }

    /// Set whether to clean the build directory before building
    pub fn with_clean(mut self, clean: bool) -> Self {
        self.args.clean = clean;
        self
    }

    /// Set the path to the package manifest or directory
    pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.args.path = Some(path.into());
        self
    }
}

impl IntoFuture for BuildBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        build::execute(self.args).boxed_local()
    }
}

/// Contains the arguments to pass to [`global::execute()`]. Call `.await` to call
/// the CLI execute method and await the result at the same time.
pub struct GlobalInstallBuilder {
    args: global::install::Args,
    tmpdir: PathBuf,
}

impl GlobalInstallBuilder {
    /// Create a new GlobalInstallBuilder
    pub fn new(
        tmpdir: PathBuf,
        backend_override: Option<pixi_build_frontend::BackendOverride>,
    ) -> Self {
        let mut args = global::install::Args::default();
        args.backend_override = backend_override;
        Self { args, tmpdir }
    }

    pub fn with_path(mut self, path: impl ToString) -> Self {
        self.args.packages.path = Some(Into::<Utf8NativePathBuf>::into(path.to_string()));
        self
    }

    pub fn with_force_reinstall(mut self, force_reinstall: bool) -> Self {
        self.args.force_reinstall = force_reinstall;
        self
    }
}

impl IntoFuture for GlobalInstallBuilder {
    type Output = miette::Result<()>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

    fn into_future(self) -> Self::IntoFuture {
        let args = global::Args {
            command: global::Command::Install(self.args),
        };

        temp_env::async_with_vars(
            [
                ("PIXI_HOME", Some(self.tmpdir.clone())),
                ("PIXI_CACHE_DIR", Some(self.tmpdir.clone())),
            ],
            async { global::execute(args).await },
        )
        .boxed_local()
    }
}
