use indexmap::IndexMap;
use serde::de::{self, Deserializer, SeqAccess, Visitor};
use serde::{Deserialize, Serialize, Serializer};

/// One value or a list of values.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum OneOrMany<T> {
    One(T),
    Many(Vec<T>),
}

/// A variable value can be a plain string or a mapping with `value` and optional `description`.
/// YAML scalars like integers and booleans are coerced to strings.
#[derive(Debug, Clone)]
pub enum VarValue {
    Simple(String),
    Complex {
        value: String,
        description: Option<String>,
    },
}

impl VarValue {
    pub fn value(&self) -> &str {
        match self {
            VarValue::Simple(s) => s,
            VarValue::Complex { value, .. } => value,
        }
    }
}

impl serde::Serialize for VarValue {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            VarValue::Simple(s) => serializer.serialize_str(s),
            VarValue::Complex { value, description } => {
                use serde::ser::SerializeMap;
                let mut map = serializer.serialize_map(None)?;
                map.serialize_entry("value", value)?;
                if let Some(desc) = description {
                    map.serialize_entry("description", desc)?;
                }
                map.end()
            }
        }
    }
}

impl<'de> serde::Deserialize<'de> for VarValue {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = serde_yaml::Value::deserialize(deserializer)?;
        match &value {
            serde_yaml::Value::String(s) => Ok(VarValue::Simple(s.clone())),
            serde_yaml::Value::Number(n) => Ok(VarValue::Simple(n.to_string())),
            serde_yaml::Value::Bool(b) => Ok(VarValue::Simple(b.to_string())),
            serde_yaml::Value::Null => Ok(VarValue::Simple(String::new())),
            serde_yaml::Value::Mapping(m) => {
                let val = m
                    .get(serde_yaml::Value::String("value".into()))
                    .and_then(|v| match v {
                        serde_yaml::Value::String(s) => Some(s.clone()),
                        serde_yaml::Value::Number(n) => Some(n.to_string()),
                        serde_yaml::Value::Bool(b) => Some(b.to_string()),
                        _ => None,
                    })
                    .unwrap_or_default();
                let desc = m
                    .get(serde_yaml::Value::String("description".into()))
                    .and_then(|v| v.as_str().map(String::from));
                Ok(VarValue::Complex {
                    value: val,
                    description: desc,
                })
            }
            _ => Err(serde::de::Error::custom("invalid variable value")),
        }
    }
}

/// `image:` can be a plain string or a mapping.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ImageValue {
    Simple(String),
    Complex { name: String },
}

impl ImageValue {
    pub fn name(&self) -> &str {
        match self {
            ImageValue::Simple(s) => s,
            ImageValue::Complex { name } => name,
        }
    }
}

/// A single `needs:` entry.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum NeedEntry {
    Simple(String),
    Complex {
        job: String,
        #[serde(default)]
        artifacts: Option<bool>,
        #[serde(default)]
        optional: Option<bool>,
        #[serde(default)]
        parallel: Option<serde_yaml::Value>,
    },
}

impl NeedEntry {
    pub fn job_name(&self) -> &str {
        match self {
            NeedEntry::Simple(s) => s,
            NeedEntry::Complex { job, .. } => job,
        }
    }

    pub fn wants_artifacts(&self) -> bool {
        match self {
            NeedEntry::Simple(_) => true,
            NeedEntry::Complex { artifacts, .. } => artifacts.unwrap_or(true),
        }
    }

    pub fn is_optional(&self) -> bool {
        match self {
            NeedEntry::Simple(_) => false,
            NeedEntry::Complex { optional, .. } => optional.unwrap_or(false),
        }
    }

    pub fn parallel(&self) -> Option<&serde_yaml::Value> {
        match self {
            NeedEntry::Simple(_) => None,
            NeedEntry::Complex { parallel, .. } => parallel.as_ref(),
        }
    }
}

/// A single `rules:` entry.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RuleEntry {
    #[serde(rename = "if")]
    pub condition: Option<String>,
    pub changes: Option<OneOrMany<String>>,
    pub exists: Option<OneOrMany<String>>,
    #[serde(rename = "when")]
    pub when_val: Option<String>,
    pub allow_failure: Option<bool>,
    pub variables: Option<IndexMap<String, String>>,
}

/// Artifact configuration.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ArtifactConfig {
    pub paths: Option<Vec<String>>,
    pub exclude: Option<Vec<String>>,
    pub expire_in: Option<String>,
    pub name: Option<String>,
    pub untracked: Option<bool>,
    #[serde(rename = "when")]
    pub when_val: Option<String>,
    pub reports: Option<serde_yaml::Value>,
}

/// Cache configuration.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct CacheConfig {
    pub key: Option<serde_yaml::Value>,
    pub paths: Option<Vec<String>>,
    pub policy: Option<String>,
    pub untracked: Option<bool>,
    #[serde(rename = "when")]
    pub when_val: Option<String>,
}

/// Service entry.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ServiceEntry {
    Simple(String),
    Complex {
        name: String,
        #[serde(default)]
        alias: Option<String>,
    },
}

/// A list of script lines that handles nested sequences from YAML anchors.
/// GitLab CI allows `script:` to contain nested lists (via YAML anchors),
/// which get flattened into a flat list of strings.
#[derive(Debug, Clone, Default)]
pub struct ScriptLines(Vec<String>);

impl ScriptLines {
    pub fn as_slice(&self) -> &[String] {
        &self.0
    }
}

impl From<Vec<String>> for ScriptLines {
    fn from(v: Vec<String>) -> Self {
        ScriptLines(v)
    }
}

impl<'de> Deserialize<'de> for ScriptLines {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ScriptVisitor;

        impl<'de> Visitor<'de> for ScriptVisitor {
            type Value = ScriptLines;

            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string or a (possibly nested) list of strings")
            }

            fn visit_str<E: de::Error>(self, v: &str) -> Result<ScriptLines, E> {
                Ok(ScriptLines(vec![v.to_string()]))
            }

            fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<ScriptLines, A::Error> {
                let values: Vec<serde_yaml::Value> =
                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))?;
                let mut result = Vec::new();
                flatten_value_list(&values, &mut result);
                Ok(ScriptLines(result))
            }
        }

        deserializer.deserialize_any(ScriptVisitor)
    }
}

impl Serialize for ScriptLines {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.0.serialize(serializer)
    }
}

/// Recursively flatten a YAML value list into strings.
fn flatten_value_list(values: &[serde_yaml::Value], out: &mut Vec<String>) {
    for v in values {
        match v {
            serde_yaml::Value::String(s) => out.push(s.clone()),
            serde_yaml::Value::Sequence(seq) => flatten_value_list(seq, out),
            serde_yaml::Value::Number(n) => out.push(n.to_string()),
            serde_yaml::Value::Bool(b) => out.push(b.to_string()),
            other => out.push(format!("{:?}", other)),
        }
    }
}

/// Controls which items from a category (default keywords or global variables)
/// a job inherits.
#[derive(Debug, Clone, Default)]
pub enum InheritFilter {
    #[default]
    /// Inherit everything (equivalent to `true` or omitted).
    All,
    /// Inherit nothing (equivalent to `false`).
    None,
    /// Inherit only the listed items.
    Listed(Vec<String>),
}

impl InheritFilter {
    pub fn allows(&self, key: &str) -> bool {
        match self {
            InheritFilter::All => true,
            InheritFilter::None => false,
            InheritFilter::Listed(keys) => keys.iter().any(|k| k == key),
        }
    }
}

impl<'de> Deserialize<'de> for InheritFilter {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = serde_yaml::Value::deserialize(deserializer)?;
        match value {
            serde_yaml::Value::Bool(true) => Ok(InheritFilter::All),
            serde_yaml::Value::Bool(false) => Ok(InheritFilter::None),
            serde_yaml::Value::Sequence(seq) => {
                let items: Vec<String> = seq
                    .into_iter()
                    .filter_map(|v| match v {
                        serde_yaml::Value::String(s) => Some(s),
                        _ => None,
                    })
                    .collect();
                Ok(InheritFilter::Listed(items))
            }
            _ => Err(de::Error::custom(
                "expected true, false, or a list of strings",
            )),
        }
    }
}

impl Serialize for InheritFilter {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            InheritFilter::All => serializer.serialize_bool(true),
            InheritFilter::None => serializer.serialize_bool(false),
            InheritFilter::Listed(items) => items.serialize(serializer),
        }
    }
}

/// Controls inheritance of `default:` keywords and global `variables:` for a job.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct InheritConfig {
    #[serde(default)]
    pub default: InheritFilter,
    #[serde(default)]
    pub variables: InheritFilter,
}

/// A single step in a `run:` list (GitLab Functions).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RunStep {
    pub name: String,
    #[serde(default)]
    pub script: Option<String>,
    #[serde(default)]
    pub func: Option<String>,
    #[serde(default)]
    pub inputs: Option<IndexMap<String, serde_yaml::Value>>,
    #[serde(default)]
    pub env: Option<IndexMap<String, VarValue>>,
}

/// The raw job configuration before extends/default resolution.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RawJobConfig {
    pub extends: Option<OneOrMany<String>>,
    pub stage: Option<String>,
    pub image: Option<ImageValue>,
    pub services: Option<Vec<ServiceEntry>>,
    pub variables: Option<IndexMap<String, VarValue>>,
    pub before_script: Option<ScriptLines>,
    pub script: Option<ScriptLines>,
    pub after_script: Option<ScriptLines>,
    pub run: Option<Vec<RunStep>>,
    pub rules: Option<serde_yaml::Value>,
    pub needs: Option<Vec<NeedEntry>>,
    pub dependencies: Option<Vec<String>>,
    pub artifacts: Option<ArtifactConfig>,
    pub cache: Option<CacheConfig>,
    pub tags: Option<Vec<String>>,
    #[serde(rename = "when")]
    pub when_val: Option<String>,
    pub allow_failure: Option<serde_yaml::Value>,
    pub retry: Option<serde_yaml::Value>,
    pub timeout: Option<String>,
    pub resource_group: Option<String>,
    pub environment: Option<serde_yaml::Value>,
    pub coverage: Option<String>,
    pub parallel: Option<serde_yaml::Value>,
    pub trigger: Option<serde_yaml::Value>,
    pub inherit: Option<InheritConfig>,
    pub only: Option<serde_yaml::Value>,
    pub except: Option<serde_yaml::Value>,
    pub interruptible: Option<bool>,
    pub release: Option<serde_yaml::Value>,
    pub secrets: Option<serde_yaml::Value>,
    pub id_tokens: Option<serde_yaml::Value>,
    /// Catch-all for any unrecognized keys (e.g. `pages`, `dast_configuration`, etc.)
    /// so they are preserved in the resolved YAML output.
    #[serde(flatten)]
    pub extra: IndexMap<String, serde_yaml::Value>,
}

/// Workflow configuration.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct WorkflowConfig {
    pub rules: Option<serde_yaml::Value>,
    pub name: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_var_value_string() {
        let v: VarValue = serde_yaml::from_str("\"hello\"").unwrap();
        assert_eq!(v.value(), "hello");
    }

    #[test]
    fn test_var_value_number() {
        let v: VarValue = serde_yaml::from_str("42").unwrap();
        assert_eq!(v.value(), "42");
    }

    #[test]
    fn test_var_value_bool() {
        let v: VarValue = serde_yaml::from_str("true").unwrap();
        assert_eq!(v.value(), "true");
    }

    #[test]
    fn test_var_value_null() {
        let v: VarValue = serde_yaml::from_str("null").unwrap();
        assert_eq!(v.value(), "");
    }

    #[test]
    fn test_var_value_complex() {
        let v: VarValue = serde_yaml::from_str("value: hello\ndescription: a greeting").unwrap();
        assert_eq!(v.value(), "hello");
        match &v {
            VarValue::Complex { description, .. } => {
                assert_eq!(description.as_deref(), Some("a greeting"));
            }
            _ => panic!("expected Complex"),
        }
    }

    #[test]
    fn test_var_value_roundtrip_simple() {
        let v = VarValue::Simple("hello".to_string());
        let yaml = serde_yaml::to_string(&v).unwrap();
        let v2: VarValue = serde_yaml::from_str(&yaml).unwrap();
        assert_eq!(v2.value(), "hello");
    }

    #[test]
    fn test_image_value_simple() {
        let v: ImageValue = serde_yaml::from_str("\"rust:latest\"").unwrap();
        assert_eq!(v.name(), "rust:latest");
    }

    #[test]
    fn test_image_value_complex() {
        let v: ImageValue = serde_yaml::from_str("name: rust:latest").unwrap();
        assert_eq!(v.name(), "rust:latest");
    }

    #[test]
    fn test_need_entry_simple() {
        let v: NeedEntry = serde_yaml::from_str("\"build\"").unwrap();
        assert_eq!(v.job_name(), "build");
    }

    #[test]
    fn test_need_entry_complex() {
        let v: NeedEntry =
            serde_yaml::from_str("job: build\nartifacts: true\noptional: false").unwrap();
        assert_eq!(v.job_name(), "build");
    }

    #[test]
    fn test_script_lines_string() {
        let v: ScriptLines = serde_yaml::from_str("\"echo hello\"").unwrap();
        assert_eq!(v.as_slice(), &["echo hello"]);
    }

    #[test]
    fn test_script_lines_list() {
        let v: ScriptLines = serde_yaml::from_str("[\"echo a\", \"echo b\"]").unwrap();
        assert_eq!(v.as_slice(), &["echo a", "echo b"]);
    }

    #[test]
    fn test_script_lines_nested() {
        let v: ScriptLines =
            serde_yaml::from_str("[[\"echo a\", \"echo b\"], \"echo c\"]").unwrap();
        assert_eq!(v.as_slice(), &["echo a", "echo b", "echo c"]);
    }

    #[test]
    fn test_script_lines_with_numbers() {
        let v: ScriptLines = serde_yaml::from_str("[42, true]").unwrap();
        assert_eq!(v.as_slice(), &["42", "true"]);
    }

    #[test]
    fn test_raw_job_config_deserialize() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            stage: build
            image: rust:latest
            script: [cargo build]
            tags: [shared]
            when: manual
            "#,
        )
        .unwrap();
        assert_eq!(job.stage.as_deref(), Some("build"));
        assert_eq!(job.image.as_ref().unwrap().name(), "rust:latest");
        assert_eq!(job.tags.as_ref().unwrap(), &["shared"]);
        assert_eq!(job.when_val.as_deref(), Some("manual"));
    }

    #[test]
    fn test_raw_job_config_unknown_keys_preserved() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            script: [echo hi]
            release:
              tag_name: v1.0
            "#,
        )
        .unwrap();
        assert!(job.release.is_some());
    }

    #[test]
    fn test_script_lines_from_vec() {
        let lines = vec!["echo a".to_string(), "echo b".to_string()];
        let sl = ScriptLines::from(lines);
        assert_eq!(sl.as_slice(), &["echo a", "echo b"]);
    }

    #[test]
    fn test_script_lines_from_empty_vec() {
        let sl = ScriptLines::from(vec![]);
        assert!(sl.as_slice().is_empty());
    }

    #[test]
    fn test_raw_job_config_extra_fields() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            script: [echo hi]
            pages:
              publish: public
            "#,
        )
        .unwrap();
        assert!(job.extra.contains_key("pages"));
    }

    #[test]
    fn test_inherit_filter_true() {
        let v: InheritFilter = serde_yaml::from_str("true").unwrap();
        assert!(matches!(v, InheritFilter::All));
        assert!(v.allows("image"));
    }

    #[test]
    fn test_inherit_filter_false() {
        let v: InheritFilter = serde_yaml::from_str("false").unwrap();
        assert!(matches!(v, InheritFilter::None));
        assert!(!v.allows("image"));
    }

    #[test]
    fn test_inherit_filter_listed() {
        let v: InheritFilter = serde_yaml::from_str("[image, tags]").unwrap();
        assert!(v.allows("image"));
        assert!(v.allows("tags"));
        assert!(!v.allows("retry"));
    }

    #[test]
    fn test_inherit_config_both() {
        let v: InheritConfig = serde_yaml::from_str(
            r#"
            default: false
            variables: [VAR1, VAR2]
            "#,
        )
        .unwrap();
        assert!(matches!(v.default, InheritFilter::None));
        assert!(v.variables.allows("VAR1"));
        assert!(v.variables.allows("VAR2"));
        assert!(!v.variables.allows("VAR3"));
    }

    #[test]
    fn test_inherit_config_partial() {
        let v: InheritConfig = serde_yaml::from_str("default: [image]").unwrap();
        assert!(v.default.allows("image"));
        assert!(!v.default.allows("tags"));
        assert!(matches!(v.variables, InheritFilter::All));
    }

    #[test]
    fn test_inherit_roundtrip() {
        let cfg = InheritConfig {
            default: InheritFilter::Listed(vec!["image".into(), "tags".into()]),
            variables: InheritFilter::None,
        };
        let yaml = serde_yaml::to_value(&cfg).unwrap();
        let cfg2: InheritConfig = serde_yaml::from_value(yaml).unwrap();
        assert!(cfg2.default.allows("image"));
        assert!(!cfg2.default.allows("retry"));
        assert!(matches!(cfg2.variables, InheritFilter::None));
    }

    #[test]
    fn test_raw_job_with_inherit() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            script: [echo hi]
            inherit:
              default: false
              variables: [MY_VAR]
            "#,
        )
        .unwrap();
        let inherit = job.inherit.unwrap();
        assert!(matches!(inherit.default, InheritFilter::None));
        assert!(inherit.variables.allows("MY_VAR"));
        assert!(!inherit.variables.allows("OTHER"));
    }

    #[test]
    fn test_run_step_deserialize_script() {
        let step: RunStep = serde_yaml::from_str(
            r#"
            name: setup
            script: echo "hello"
            env:
              MY_VAR: value
            "#,
        )
        .unwrap();
        assert_eq!(step.name, "setup");
        assert_eq!(step.script.as_deref(), Some("echo \"hello\""));
        assert!(step.func.is_none());
        assert_eq!(step.env.as_ref().unwrap()["MY_VAR"].value(), "value");
    }

    #[test]
    fn test_run_step_deserialize_func() {
        let step: RunStep = serde_yaml::from_str(
            r#"
            name: deploy
            func: registry.gitlab.com/org/deploy:1.0
            inputs:
              environment: production
              replicas: 3
            "#,
        )
        .unwrap();
        assert_eq!(step.name, "deploy");
        assert!(step.script.is_none());
        assert_eq!(
            step.func.as_deref(),
            Some("registry.gitlab.com/org/deploy:1.0")
        );
        let inputs = step.inputs.as_ref().unwrap();
        assert_eq!(inputs["environment"].as_str(), Some("production"));
        assert_eq!(inputs["replicas"].as_u64(), Some(3));
    }

    #[test]
    fn test_artifact_config_with_exclude() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            script: [cargo build]
            artifacts:
              paths:
                - target/release/mybin
                - docs/
              exclude:
                - "**/*.o"
                - "**/*.tmp"
              expire_in: 1 week
            "#,
        )
        .unwrap();
        let arts = job.artifacts.unwrap();
        assert_eq!(arts.paths.unwrap(), vec!["target/release/mybin", "docs/"]);
        assert_eq!(arts.exclude.unwrap(), vec!["**/*.o", "**/*.tmp"]);
        assert_eq!(arts.expire_in.as_deref(), Some("1 week"));
    }

    #[test]
    fn test_raw_job_with_run() {
        let job: RawJobConfig = serde_yaml::from_str(
            r#"
            stage: deploy
            run:
              - name: setup
                script: echo "setup"
              - name: deploy
                func: registry.gitlab.com/org/deploy:1.0
                inputs:
                  env: prod
            "#,
        )
        .unwrap();
        assert!(job.script.is_none());
        let steps = job.run.as_ref().unwrap();
        assert_eq!(steps.len(), 2);
        assert_eq!(steps[0].name, "setup");
        assert!(steps[0].script.is_some());
        assert_eq!(steps[1].name, "deploy");
        assert!(steps[1].func.is_some());
    }
}

/// The raw pipeline as parsed from YAML, before resolution.
#[derive(Debug, Clone, Default)]
pub struct RawPipeline {
    pub stages: Vec<String>,
    pub variables: IndexMap<String, VarValue>,
    pub default: Option<RawJobConfig>,
    #[allow(dead_code)] // Parsed for future workflow rules support
    pub workflow: Option<WorkflowConfig>,
    /// All jobs, including hidden ones (names starting with `.`).
    pub jobs: IndexMap<String, RawJobConfig>,
    /// Maps top-level key names (job names, template names) to their source file and line.
    pub source_map: IndexMap<String, crate::model::SourceLocation>,
    /// GitHub context values detected from the local git repo (empty for GitLab).
    pub github_context: std::collections::HashMap<String, String>,
}
