use std::{collections::HashMap, path::Path, process::Command};

use clingwrap::runner::CommandRunner;
use serde::{Deserialize, Serialize};

use crate::{
    action::{ActionError, Context},
    action_impl::ActionImpl,
};

/// Execute a custom action.
///
/// Run the named executable from `.ambient` at the root of the source
/// tree. The arguments in `args` are used to set environment variables
/// and are also passed in via the standard input, both as JSON. Note that
/// there is no way for Ambient to check that the necessary arguments are
/// provided.
///
/// An `args` value of `foo` results in `AMBIENT_CI_foo` being set in the
/// environment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Custom {
    /// Name of custom action executable in `.ambient`.
    pub name: String,

    /// Arguments to custom action.
    #[serde(default)]
    pub args: HashMap<String, serde_norway::Value>,
}

impl Custom {
    /// Create a new custom action.
    pub fn new(name: String, args: HashMap<String, serde_norway::Value>) -> Self {
        Self { name, args }
    }
}

impl ActionImpl for Custom {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let source = context.source_dir().to_path_buf();
        let exe = Path::new(".ambient").join(&self.name);
        let json = serde_json::to_string(&self.args).map_err(CustomError::ArgsToJson)?;

        eprintln!("custom: source={}", source.display());
        eprintln!("custom: exe={exe:?} exists={}", exe.exists());
        let mut cmd = Command::new(exe);
        cmd.current_dir(&source);
        cmd.envs(context.env());
        for (key, value) in self.args.iter() {
            let key = format!("AMBIENT_CI_{key}");
            let value = serde_json::to_string(value).map_err(CustomError::ArgsToJson)?;
            cmd.env(key, value);
        }
        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(json.as_bytes());
        let output = runner
            .execute()
            .map_err(|err| CustomError::Custom(self.name.to_string(), err))?;
        println!(
            "custom action {:?} exit code {:?}",
            self.name,
            output.status.code()
        );
        Ok(())
    }
}

/// Errors from a custom action.
#[derive(Debug, thiserror::Error)]
pub enum CustomError {
    /// Can't run custom action.
    #[error("failed to run custom action {0:?}")]
    Custom(String, #[source] clingwrap::runner::CommandError),

    /// Can't convert custom action arguments to JSON.
    #[error("failed to convert custom action into JSON")]
    ArgsToJson(#[source] serde_json::Error),
}

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