use std::collections::HashMap;

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

use crate::{
    error::{DependencyError, ValidationError},
    matcher::glob_match,
};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PipelineVersion {
    #[serde(rename = "1")]
    V1,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeType {
    #[serde(rename = "container")]
    Container,
    #[serde(rename = "host")]
    Host,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepKind {
    Task,
    Command,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TriggerEvent {
    PushBranch(String),
    PushTag(String),
    PullRequest { target: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunOn {
    #[serde(default)]
    pub push: Option<PushTrigger>,
    #[serde(default)]
    pub pull_request: Option<PullRequestTrigger>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushTrigger {
    #[serde(default)]
    pub branches: Vec<BranchPattern>,
    #[serde(default)]
    pub tags: Vec<BranchPattern>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullRequestTrigger {
    #[serde(default)]
    pub branches: Vec<BranchPattern>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BranchPattern(String);

impl BranchPattern {
    pub fn new(pattern: impl Into<String>) -> Self {
        Self(pattern.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn is_pattern(&self) -> bool {
        self.0.contains('*') || self.0.contains('?')
    }
    pub fn matches(&self, name: &str) -> bool {
        if !self.is_pattern() {
            self.0 == name
        } else {
            glob_match(&self.0, name)
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pipeline {
    pub version: PipelineVersion,
    pub name: String,
    #[serde(default)]
    pub run_on: Option<RunOn>,
    pub jobs: IndexMap<String, Job>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
    pub stage: String,
    #[serde(default)]
    pub depends_on: Vec<String>,
    pub runtime: Runtime,
    #[serde(default)]
    pub runner_labels: Vec<String>,
    pub steps: Vec<Step>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Runtime {
    #[serde(rename = "type")]
    pub runtime_type: RuntimeType,
    pub image: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Step {
    pub name: String,
    #[serde(default)]
    pub task: Option<String>,
    #[serde(default)]
    pub command: Option<String>,
    #[serde(default)]
    pub params: IndexMap<String, StepParam>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StepParam {
    String(String),
    List(Vec<String>),
}

impl Step {
    pub fn kind(&self) -> Option<StepKind> {
        if self.task.is_some() {
            Some(StepKind::Task)
        } else if self.command.is_some() {
            Some(StepKind::Command)
        } else {
            None
        }
    }
}

impl StepParam {
    pub fn as_string(&self) -> &str {
        match self {
            StepParam::String(s) => s,
            StepParam::List(_) => {
                panic!("StepParam::as_string called on a List variant")
            }
        }
    }

    pub fn as_list(&self) -> &[String] {
        match self {
            StepParam::String(_) => {
                panic!("StepParam::as_list called on a String variant")
            }
            StepParam::List(v) => v,
        }
    }
}

impl Pipeline {
    pub fn jobs_by_stage(&self, stage: &str) -> Vec<&Job> {
        self.jobs.values().filter(|j| j.stage == stage).collect()
    }

    pub fn stages(&self) -> Vec<&str> {
        let mut seen: Vec<&str> = Vec::new();
        for job in self.jobs.values() {
            if !seen.contains(&job.stage.as_str()) {
                seen.push(&job.stage);
            }
        }
        seen
    }
    pub fn should_run(&self, event: &TriggerEvent) -> bool {
        let run_on = match &self.run_on {
            None => return true,
            Some(r) => r,
        };

        match event {
            TriggerEvent::PushBranch(branch) => {
                run_on.push.as_ref().map_or(false, |p| {
                    if p.branches.is_empty() {
                        true
                    } else {
                        p.branches.iter().any(|bp| bp.matches(branch))
                    }
                })
            }
            TriggerEvent::PushTag(tag) => {
                run_on.push.as_ref().map_or(false, |p| {
                    if p.tags.is_empty() {
                        true
                    } else {
                        p.tags.iter().any(|tp| tp.matches(tag))
                    }
                })
            }
            TriggerEvent::PullRequest { target } => {
                run_on.pull_request.as_ref().map_or(false, |pr| {
                    if pr.branches.is_empty() {
                        true
                    } else {
                        pr.branches.iter().any(|bp| bp.matches(target))
                    }
                })
            }
        }
    }

    pub fn execution_order(&self) -> Result<Vec<String>, DependencyError> {
        let mut in_degree: HashMap<&str, usize> = HashMap::new();
        let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();

        for name in self.jobs.keys() {
            in_degree.insert(name, 0);
            graph.insert(name, Vec::new());
        }

        for (name, job) in &self.jobs {
            for dep in &job.depends_on {
                if !self.jobs.contains_key(dep) {
                    return Err(DependencyError::UnknownReference {
                        job: name.clone(),
                        dependency: dep.clone(),
                    });
                }
                graph.get_mut(dep.as_str()).unwrap().push(name);
                *in_degree.get_mut(name.as_str()).unwrap() += 1;
            }
        }

        let mut queue: Vec<&str> = in_degree
            .iter()
            .filter(|(_, deg)| **deg == 0)
            .map(|(name, _)| *name)
            .collect();
        queue.sort();

        let mut order: Vec<String> = Vec::new();
        while let Some(node) = queue.pop() {
            order.push(node.to_string());
            for &neighbor in &graph[node] {
                let deg = in_degree.get_mut(neighbor).unwrap();
                *deg -= 1;
                if *deg == 0 {
                    queue.push(neighbor);
                    queue.sort();
                }
            }
        }

        if order.len() != self.jobs.len() {
            let cycle_nodes: Vec<String> = self
                .jobs
                .keys()
                .filter(|k| !order.contains(k))
                .cloned()
                .collect();
            return Err(DependencyError::CircularDependency {
                chain: cycle_nodes.join(" -> "),
            });
        }

        Ok(order)
    }

    pub fn validate(&self) -> Result<(), ValidationError> {
        for (name, job) in &self.jobs {
            for step in &job.steps {
                if step.kind().is_none() {
                    return Err(ValidationError::EmptyStep {
                        job: name.clone(),
                        step: step.name.clone(),
                    });
                }
            }
        }
        self.execution_order()?;
        Ok(())
    }
}
