use crate::{command::Command, job::JobConfig};
use serde::{Deserialize, Serialize};
use tokio::{fs, io::AsyncWriteExt};

#[derive(Serialize, Deserialize, Debug)]
pub struct Step {
    pub name: Option<String>,
    pub image: Option<String>,
    pub run: String,
    pub environments: Option<Vec<String>>,
    pub started_at: Option<chrono::DateTime<chrono::Utc>>,
    pub ended_at: Option<chrono::DateTime<chrono::Utc>>,
}

impl Step {
    pub async fn run(&mut self, config: JobConfig) -> anyhow::Result<()> {
        self.started_at = Some(chrono::Utc::now());

        let docker_name = format!("test-{}", config.id);
        let entrypoint_path = config.container_working_dir.join("entrypoint.sh");

        let mut file = fs::File::create(&entrypoint_path).await?;
        file.write_all(self.run.as_bytes()).await?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            log::info!("Unix Set file permission to 777");
            let metadata = file.metadata().await?;
            let mut permissions = metadata.permissions();
            permissions.set_mode(0o777);
        }

        #[cfg(target_os = "linux")]
        {
            use std::os::unix::fs::PermissionsExt;
            log::info!("Linux Set file permission to 777");
            let metadata = file.metadata().await?;
            let mut permissions = metadata.permissions();
            permissions.set_mode(0o777);
        }

        let entrypoint_path_string = entrypoint_path
            .to_str()
            .ok_or(anyhow::anyhow!("Failed to get path"))?;

        let mut command = self.generate_docker_command(
            docker_name.clone(),
            entrypoint_path_string.to_string(),
            &config,
        )?;

        let mut failure_reason: Option<String> = None;
        if let Err(err) = command.run().await {
            failure_reason = Some(err.to_string());
        }
        // End time
        self.ended_at = Some(chrono::Utc::now());

        log::info!("Stop and remove docker container");
        // stop and remove docker
        Command::new(format!("docker stop {}", docker_name))
            .exec()
            .await?;
        Command::new(format!("docker rm {}", docker_name))
            .exec()
            .await?;

        log::info!("Cleanup files");
        fs::remove_file(entrypoint_path).await?;

        if self.started_at.is_some() && self.ended_at.is_some() {
            let started_at = self.started_at.unwrap();
            let ended_at = self.ended_at.unwrap();

            log::info!(
                "Duration: {}ms",
                ended_at.timestamp_millis() - started_at.timestamp_millis()
            )
        }

        if failure_reason.is_some() {
            return Err(anyhow::anyhow!(failure_reason.unwrap()));
        }

        Ok(())
    }

    /// ```shell
    /// docker run --rm -v ${PWD}/entrypoint.sh:/home/entrypoint.sh --name test node:16.0.0 /bin/bash -c "/home/entrypoint.sh"
    /// ```
    fn generate_docker_command(
        &self,
        docker_name: String,
        entrypoint_path: String,
        config: &JobConfig,
    ) -> anyhow::Result<Command> {
        let mut command = vec!["docker", "run"];
        let runner_dir = "/home/runner";
        let workspace_dir = "/home/runner/work";

        // entrypoint volume
        let entrypoint_mapping = format!("{}:{}/entrypoint.sh", entrypoint_path, runner_dir);
        command.push("--volume");
        command.push(&entrypoint_mapping);

        // user data volume
        command.push("--volume");
        let user_dir = config
            .container_user_dir
            .to_str()
            .ok_or(anyhow::anyhow!("Failed to parse user dir"))?;
        let user_mapping = format!("{}:{}", user_dir, workspace_dir);
        command.push(&user_mapping);

        // docker name
        command.push("--name");
        command.push(docker_name.as_str());

        // working dir
        command.push("--workdir");
        command.push(workspace_dir);

        let mut command: Vec<String> = command.iter().map(|item| item.to_string()).collect();

        // environments options from `workflow.run`
        if let Some(environments) = config.workflow_config.environments.clone() {
            for (key, value) in environments {
                command.push("--env".to_string());
                command.push(format!("{}=\"{}\"", key, value));
            }
        }

        // environments from job config
        if let Some(environments) = self.environments.clone() {
            for env in environments {
                // If command type is Vec<&str>
                // `env` does not live long enough. borrowed value does not live long enough
                // let env = env.as_str();
                command.push("--env".to_string());
                command.push(format!("\"{}\"", env));
            }
        }

        // Coodev preset environments
        let preset_environments = self.preset_environments(workspace_dir.to_string(), config);
        for env in preset_environments {
            command.push("--env".to_string());
            command.push(format!("\"{}\"", env));
        }

        // image
        let job_default_image = config.image.clone();
        let image = self.image.clone().unwrap_or(job_default_image);
        command.push(image);

        // run command
        command.push("/bin/bash -c /home/runner/entrypoint.sh".to_string());

        let command = command.join(" ");
        log::info!("Docker command: {}", command);

        Ok(Command::new(command))
    }

    // preset environment variables
    fn preset_environments(&self, workspace: String, config: &JobConfig) -> Vec<String> {
        let mut environments = vec![];

        environments.push(format!("COODEV_WORKFLOW_ID={}", config.workflow_config.id));

        environments.push(format!("COODEV_JOB_ID={}", config.id));

        environments.push(format!("COODEV_WORKSPACE={}", workspace));

        environments.push(format!(
            "COODEV_REPOSITORY={}",
            config.workflow_config.repository
        ));

        environments.push(format!("COODEV_REF={}", config.workflow_config.ref_name));

        // environments.push(format!(
        //     "COODEV_EVENT_NAME={}",
        //     config.workflow_config.event
        // ));

        environments.push(format!(
            "COODEV_REPO_OWNER={}",
            config.workflow_config.repo_owner
        ));

        environments.push(format!(
            "COODEV_REPO_NAME={}",
            config.workflow_config.repo_name
        ));

        environments.push(format!("COODEV_SHA={}", config.workflow_config.sha));

        if config.workflow_config.pr_number.is_some() {
            environments.push(format!(
                "COODEV_PR_NUMBER={}",
                config.workflow_config.pr_number.unwrap()
            ));
        } else {
            environments.push("COODEV_PR_NUMBER=".to_string());
        }

        // if config.workflow_config.access_token.is_some() {
        //     environments.push(format!(
        //         "COODEV_ACCESS_TOKEN={}",
        //         config.workflow_config.access_token.clone().unwrap()
        //     ));
        // } else {
        //     environments.push("COODEV_ACCESS_TOKEN=".to_string());
        // }

        environments
    }
}
