use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::{
    action::{ActionError, Context},
    action_impl::{spawn, ActionImpl},
    util::{mkdir, UtilError},
};

/// Subdirectory of the dependencies directory for files `rustup` downloads.
pub const RUSTUP_DIR: &str = "rustup";

const DEFAULT_CHANNEL: &str = "stable";

/// Install Rust toolchains and components, using rustup.
/// The host needs to have `rustup` installed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rustup {
    channel: String,
    target: Option<String>,
    components: Option<Vec<String>>,
    profile: Option<String>,
}

impl Rustup {
    /// Create a new `Rustup` action.
    pub fn new(
        channel: &Option<String>,
        target: &Option<String>,
        components: &Option<Vec<String>>,
        profile: &Option<String>,
    ) -> Self {
        Self {
            channel: channel.clone().unwrap_or(DEFAULT_CHANNEL.to_string()),
            target: target.to_owned(),
            components: components.to_owned(),
            profile: profile.to_owned(),
        }
    }
}

impl ActionImpl for Rustup {
    const CARRY_OVER_ENVS: &[(&'static str, &'static str)] = &[("RUSTUP_HOME", "/ci/deps/rustup")];

    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        context.runlog().debug(format!("rustup {self:?}"));

        let dir = context.deps_dir().join(RUSTUP_DIR);
        if !dir.exists() {
            mkdir(&dir).map_err(|err| RustupError::RustupDir(dir.clone(), err))?;
        }

        let mut argv_strings = vec![
            "env".to_string(),
            format!("RUSTUP_HOME={}", dir.display()),
            "rustup".to_string(),
            "install".to_string(),
            "--no-self-update".to_string(),
            "--no-update".to_string(),
            "--force".to_string(),
            self.channel.clone(),
        ];
        if let Some(v) = &self.target {
            argv_strings.push("--target".to_string());
            argv_strings.push(v.to_string());
        }
        if let Some(v) = &self.profile {
            argv_strings.push("--profile".to_string());
            argv_strings.push(v.to_string());
        }
        if let Some(list) = &self.components {
            argv_strings.push("--component".to_string());
            let mut v = String::new();
            for c in list {
                if !v.is_empty() {
                    v.push(',');
                }
                v.push_str(c);
            }
            argv_strings.push(v);
        }
        let argv: Vec<&str> = argv_strings.iter().map(String::as_ref).collect();
        spawn(context, &argv)
    }
}

/// Errors from the Pwd action.
#[derive(Debug, thiserror::Error)]
pub enum RustupError {
    /// Can't create dependencies dirs for rustup.
    #[error("failed to create rustup directory {0}")]
    RustupDir(PathBuf, #[source] UtilError),
}

impl From<RustupError> for ActionError {
    fn from(value: RustupError) -> Self {
        Self::Rustup(value)
    }
}
