use crate::job::Job;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
pub struct Workflow {
    pub name: Option<String>,
    #[serde(default = "default_on", with = "optional_workflow_events_formatter")]
    pub on: Option<Vec<WorkflowEvent>>,
    pub jobs: HashMap<String, Job>,
}

fn default_on() -> Option<Vec<WorkflowEvent>> {
    None
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum WorkflowEvent {
    Push,
    PullRequest,
    Tag,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum WorkflowRefType {
    Branch,
    Tag,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WorkflowRunOptions {
    pub event: WorkflowEvent,
    pub repository: String,
    pub repo_owner: String,
    pub repo_name: String,
    pub sha: String,
    pub pr_number: Option<u64>,
    pub ref_name: String,
    pub ref_type: WorkflowRefType,
    pub access_token: Option<String>,
    pub environments: Option<HashMap<String, String>>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WorkflowConfig {
    pub id: String,
    pub name: Option<String>,
    pub on: Option<Vec<WorkflowEvent>>,
    pub event: WorkflowEvent,
    pub repository: String,
    pub repo_owner: String,
    pub repo_name: String,
    pub sha: String,
    pub pr_number: Option<u64>,
    pub ref_name: String,
    pub ref_type: WorkflowRefType,
    pub environments: Option<HashMap<String, String>>,
    pub access_token: Option<String>,
    pub started_at: Option<chrono::DateTime<chrono::Utc>>,
    pub ended_at: Option<chrono::DateTime<chrono::Utc>>,
}

impl Workflow {
    pub fn from_str(str: &str) -> anyhow::Result<Self> {
        let workflow = serde_yaml::from_str(str)?;

        Ok(workflow)
    }

    pub async fn run(&mut self, options: WorkflowRunOptions) -> anyhow::Result<()> {
        let mut config = WorkflowConfig {
            id: "test".to_string(),
            name: self.name.clone(),
            on: self.on.clone(),
            event: options.event,
            repository: options.repository,
            repo_owner: options.repo_owner,
            repo_name: options.repo_name,
            sha: options.sha,
            pr_number: options.pr_number,
            ref_name: options.ref_name,
            ref_type: options.ref_type,
            access_token: options.access_token,
            environments: options.environments,
            started_at: Some(chrono::Utc::now()),
            ended_at: None,
        };

        // TODO join_all!(futures)
        for (_key, job) in self.jobs.iter_mut() {
            if let Err(_) = job.run(config.clone()).await {
                break;
            }
        }

        config.ended_at = Some(chrono::Utc::now());

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

        Ok(())
    }
}

mod optional_workflow_events_formatter {
    use super::WorkflowEvent;
    use serde::{Deserialize, Deserializer, Serializer};

    type Type = Option<Vec<WorkflowEvent>>;

    pub fn serialize<S>(on: &Type, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match on {
            Some(v) => {
                let mut events = Vec::new();
                for e in v {
                    match e {
                        WorkflowEvent::Push => events.push("push".to_string()),
                        WorkflowEvent::PullRequest => events.push("pull_request".to_string()),
                        WorkflowEvent::Tag => events.push("tag".to_string()),
                    }
                }
                serializer.serialize_some(&events)
            }
            None => serializer.serialize_none(),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Type, D::Error>
    where
        D: Deserializer<'de>,
    {
        let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
        match opt {
            Some(v) => {
                let mut events = Vec::new();
                for s in v {
                    match s.as_str() {
                        "push" => events.push(WorkflowEvent::Push),
                        "pull_request" => events.push(WorkflowEvent::PullRequest),
                        "tag" => events.push(WorkflowEvent::Tag),
                        _ => {
                            return Err(serde::de::Error::custom(format!(
                                "Invalid workflow event: {}",
                                s
                            )))
                        }
                    }
                }
                Ok(Some(events))
            }
            None => Ok(None),
        }
    }
}

mod tests {
    #[allow(unused_imports)]
    use super::{Workflow, WorkflowEvent, WorkflowRunOptions};

    #[test]
    fn test_parse() {
        let yaml = r#"
name: Test Workflow
on: [push]

jobs:
  test-job:
    name: Test Job
    steps:
      - name: Test Step
        run: echo "Hello World"
"#;

        let workflow = Workflow::from_str(yaml).unwrap();
        assert_eq!(workflow.name, Some("Test Workflow".to_string()));
        assert_eq!(workflow.on, Some(vec![WorkflowEvent::Push]));
    }

    #[tokio::test]
    async fn test_run() {
        let yaml = r#"
name: Test Workflow
on: [push]

jobs:
  test-job:
    name: Test Job
    image: node:16.0.0
    steps:
      - name: Test Step 2
        run: |
          echo "Hello World"
          echo "Current directory is $(pwd)"
          echo "Workspace is ${COODEV_WORKSPACE}"
          echo "${COODEV_REPOSITORY} ${CUSTOM_ENV}" >> test.txt
      - name: Test Step
        environments:
          - NODE_ENV=production
        run: |
          set -e
          node -v
          echo "Ref is ${COODEV_REF}"
          echo "NODE_ENV is ${NODE_ENV}"
      - name: cat file
        run: |
          echo "File content is $(cat test.txt)"
"#;

        let mut workflow = Workflow::from_str(yaml).unwrap();

        let options = WorkflowRunOptions {
            event: super::WorkflowEvent::Push,
            repository: "panghu-huang/coodev".to_string(),
            repo_owner: "panghu-huang".to_string(),
            repo_name: "coodev".to_string(),
            sha: "123456".to_string(),
            pr_number: None,
            ref_name: "main".to_string(),
            ref_type: super::WorkflowRefType::Branch,
            access_token: None,
            environments: Some(
                vec![("CUSTOM_ENV".to_string(), "custom value".to_string())]
                    .into_iter()
                    .collect(),
            ),
        };

        workflow.run(options).await.unwrap();
    }
}
