use std::path::{Path, PathBuf};

use indexmap::IndexMap;
use log::debug;

use crate::model::{Job, JobDebugInfo, Pipeline, Stage, Step, StepAction, shell_quote};

/// Build a child pipeline from a job's steps, where each step becomes a job.
///
/// Returns `None` if the job has no steps (GitLab jobs, or GitHub jobs
/// where steps weren't parsed).
///
/// Each step becomes a sequential stage. Step names come from
/// `step.name`, `step.uses`, or a fallback "Step N".
pub fn build_step_pipeline(parent: &Job) -> Option<Pipeline> {
    if parent.steps.is_empty() {
        return None;
    }

    let mut jobs = IndexMap::new();
    let mut stages = Vec::new();

    for (i, step) in parent.steps.iter().enumerate() {
        let mut job_name = step_job_name(step, i);
        if jobs.contains_key(&job_name) {
            job_name = format!("{} (step {})", job_name, i + 1);
        }
        let stage_name = i.to_string();

        let cfg = step_job_config(step, parent);

        let mut variables = parent.variables.clone();
        for (k, v) in &step.env {
            if !parent.is_override_var(k) {
                variables.insert(k.clone(), v.clone());
            }
        }
        for (k, v) in &cfg.extra_vars {
            if !parent.is_override_var(k) {
                variables.insert(k.clone(), v.clone());
            }
        }

        let job = Job {
            name: job_name.clone(),
            stage: stage_name.clone(),
            image: cfg.image,
            services: vec![],
            variables,
            before_script: vec![],
            script: cfg.script,
            after_script: vec![],
            needs: vec![],
            artifact_sources: Some(vec![]),
            tags: parent.tags.clone(),
            when_val: "on_success".to_string(),
            allow_failure: false,
            rules_never: cfg.rules_never,
            unresolved_extends: vec![],
            rules_summary: vec![],
            artifacts_paths: vec![],
            artifacts_exclude: vec![],
            artifacts_untracked: false,
            artifacts_when: "on_success".to_string(),
            dotenv_report: None,
            timeout: None,
            resource_group: None,
            coverage: None,
            resolved_yaml: step_resolved_yaml(step),
            trigger_file: None,
            build_context: cfg.build_context,
            docker_entrypoint: cfg.docker_entrypoint,
            step_id: step.id.clone(),
            steps: vec![],
            debug_info: JobDebugInfo::default(),
        };

        stages.push(Stage {
            name: stage_name,
            job_names: vec![job_name.clone()],
        });
        jobs.insert(job_name, job);
    }

    let checkout_path = parent
        .steps
        .iter()
        .find(|s| {
            s.uses
                .as_deref()
                .is_some_and(|u| u.starts_with("actions/checkout@"))
                && !s.with.contains_key("repository")
        })
        .and_then(|s| s.with.get("path").cloned());

    Some(Pipeline {
        stages,
        jobs,
        execution: crate::model::ExecutionMode::SharedContainer,
        checkout_path,
    })
}

/// Derive a job name from a step.
fn step_job_name(step: &Step, index: usize) -> String {
    if let Some(ref name) = step.name {
        return name.clone();
    }
    if let Some(ref uses) = step.uses {
        return uses.clone();
    }
    format!("Step {}", index + 1)
}

struct StepJobConfig {
    image: Option<String>,
    script: Vec<String>,
    rules_never: bool,
    build_context: Option<PathBuf>,
    docker_entrypoint: Option<String>,
    extra_vars: IndexMap<String, String>,
}

/// Derive image, script lines, rules_never, build_context, and extra variables
/// from a step's resolved action.
fn step_job_config(step: &Step, parent: &Job) -> StepJobConfig {
    match &step.action {
        Some(StepAction::Run(script)) => StepJobConfig {
            image: parent.image.clone(),
            script: vec![script.clone()],
            rules_never: false,
            build_context: None,
            docker_entrypoint: None,
            extra_vars: IndexMap::new(),
        },
        Some(StepAction::Docker(docker)) => {
            let script = docker.args.to_vec();
            let entrypoint = docker.entrypoint.clone();

            let build_context = if docker.image == "Dockerfile" {
                Some(docker.action_dir.clone())
            } else {
                None
            };

            let image = if docker.image == "Dockerfile" {
                None
            } else {
                Some(docker.image.clone())
            };

            let mut extra_vars = docker.action_env.clone();
            for (k, v) in &docker.input_vars {
                extra_vars.insert(
                    format!("INPUT_{}", k.to_uppercase().replace('-', "_")),
                    v.clone(),
                );
            }

            StepJobConfig {
                image,
                script,
                rules_never: false,
                build_context,
                docker_entrypoint: entrypoint,
                extra_vars,
            }
        }
        Some(StepAction::Node { script, input_vars }) => {
            let mut extra_vars = IndexMap::new();
            for (k, v) in input_vars {
                extra_vars.insert(
                    format!("INPUT_{}", k.to_uppercase().replace('-', "_")),
                    v.clone(),
                );
            }
            StepJobConfig {
                image: parent.image.clone(),
                script: vec![format!("node {}", shell_quote(script))],
                rules_never: false,
                build_context: None,
                docker_entrypoint: None,
                extra_vars,
            }
        }
        Some(StepAction::Skip(reason)) => StepJobConfig {
            image: parent.image.clone(),
            script: vec![format!("# skipped: {}", reason)],
            rules_never: true,
            build_context: None,
            docker_entrypoint: None,
            extra_vars: IndexMap::new(),
        },
        None => StepJobConfig {
            image: parent.image.clone(),
            script: step
                .run
                .as_ref()
                .map(|r| vec![r.clone()])
                .unwrap_or_default(),
            rules_never: false,
            build_context: None,
            docker_entrypoint: None,
            extra_vars: IndexMap::new(),
        },
    }
}

/// Build a display YAML for a step-job.
fn step_resolved_yaml(step: &Step) -> String {
    let mut lines = Vec::new();
    if let Some(ref name) = step.name {
        lines.push(format!("name: {}", name));
    }
    if let Some(ref uses) = step.uses {
        lines.push(format!("uses: {}", uses));
    }
    if let Some(ref run) = step.run {
        lines.push(format!("run: |\n  {}", run.replace('\n', "\n  ")));
    }
    if let Some(ref cond) = step.if_condition {
        lines.push(format!("if: {}", cond));
    }
    if !step.with.is_empty() {
        lines.push("with:".to_string());
        for (k, v) in &step.with {
            lines.push(format!("  {}: {}", k, v));
        }
    }
    if !step.env.is_empty() {
        lines.push("env:".to_string());
        for (k, v) in &step.env {
            lines.push(format!("  {}: {}", k, v));
        }
    }
    lines.join("\n")
}

/// Accumulated environment state from GITHUB_ENV, GITHUB_PATH, and GITHUB_OUTPUT
/// files written by previous step-jobs.
#[derive(Default)]
pub struct StepEnvState {
    pub env_vars: IndexMap<String, String>,
    pub path_prepends: Vec<String>,
    pub outputs: IndexMap<String, IndexMap<String, String>>,
}

impl StepEnvState {
    /// Read GITHUB_ENV, GITHUB_PATH, and GITHUB_OUTPUT from a shared directory
    /// after a step-job completes. Accumulates into this state.
    ///
    /// `env_dir` is the host path to the shared directory where the env files live.
    /// `step_id` is the step's `id` field (if any), used to namespace outputs.
    pub fn collect(&mut self, env_dir: &Path, step_id: Option<&str>) {
        self.read_env_file(&env_dir.join("github_env"));
        self.read_path_file(&env_dir.join("github_path"));
        if let Some(id) = step_id {
            self.read_output_file(&env_dir.join("github_output"), id);
        }
    }

    /// Apply accumulated state to a step-job's variables before launch.
    pub fn apply(&self, variables: &mut IndexMap<String, String>) {
        for (k, v) in &self.env_vars {
            variables.insert(k.clone(), v.clone());
        }
        if !self.path_prepends.is_empty() {
            let existing = variables.get("PATH").cloned().unwrap_or_else(|| {
                "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string()
            });
            let prepend = self.path_prepends.join(":");
            variables.insert("PATH".to_string(), format!("{}:{}", prepend, existing));
        }
        for (step_id, outputs) in &self.outputs {
            for (key, val) in outputs {
                let var_name = format!("_STEP_{}_{}", step_id.replace('-', "_"), key);
                variables.insert(var_name, val.clone());
            }
        }
    }

    fn read_env_file(&mut self, path: &Path) {
        for (key, val) in parse_github_env_file(path) {
            self.env_vars.insert(key.clone(), val.clone());
            debug!("GITHUB_ENV: {}={}", key, val);
        }
    }

    fn read_path_file(&mut self, path: &Path) {
        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(_) => return,
        };
        for line in content.lines() {
            let line = line.trim();
            if !line.is_empty() {
                self.path_prepends.push(line.to_string());
                debug!("GITHUB_PATH: {}", line);
            }
        }
        std::fs::write(path, "").ok();
    }

    fn read_output_file(&mut self, path: &Path, step_id: &str) {
        let outputs = self.outputs.entry(step_id.to_string()).or_default();
        for (key, val) in parse_github_env_file(path) {
            outputs.insert(key.clone(), val.clone());
            debug!("GITHUB_OUTPUT[{}]: {}={}", step_id, key, val);
        }
    }
}

/// Parse a GitHub Actions env-file (GITHUB_ENV / GITHUB_OUTPUT format).
///
/// Handles single-line `KEY=VALUE`, heredoc `KEY<<DELIMITER`, and the
/// `KEY=<<DELIMITER` multiline variant. Clears the file after reading.
fn parse_github_env_file(path: &Path) -> Vec<(String, String)> {
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };
    let mut pairs = Vec::new();
    let mut lines = content.lines().peekable();
    while let Some(line) = lines.next() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        if let Some((key, delimiter)) = line.split_once("<<") {
            let mut val = String::new();
            for next in lines.by_ref() {
                if next.trim_end() == delimiter {
                    break;
                }
                if !val.is_empty() {
                    val.push('\n');
                }
                val.push_str(next);
            }
            pairs.push((key.trim_end_matches('=').to_string(), val));
        } else if let Some((key, rest)) = line.split_once('=') {
            pairs.push((key.to_string(), rest.to_string()));
        }
    }
    std::fs::write(path, "").ok();
    pairs
}

/// Create a shared temp directory for GitHub env files and return its path.
///
/// The directory contains empty `github_env`, `github_path`, `github_output`,
/// and `github_step_summary` files. Mount this at `/tmp` in each step container
/// (or repoint the GITHUB_* variables) so steps can communicate.
pub fn create_env_dir() -> std::io::Result<tempfile::TempDir> {
    let dir = tempfile::tempdir()?;
    for name in [
        "github_env",
        "github_path",
        "github_output",
        "github_step_summary",
    ] {
        std::fs::write(dir.path().join(name), "")?;
    }
    Ok(dir)
}

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

    fn make_parent_job(steps: Vec<Step>) -> Job {
        Job {
            name: "ci".to_string(),
            stage: "0".to_string(),
            image: Some("ubuntu:latest".to_string()),
            services: vec![],
            variables: IndexMap::new(),
            before_script: vec![],
            script: vec![],
            after_script: vec![],
            needs: vec![],
            artifact_sources: Some(vec![]),
            tags: vec!["ubuntu-latest".to_string()],
            when_val: "on_success".to_string(),
            allow_failure: false,
            rules_never: false,
            unresolved_extends: vec![],
            rules_summary: vec![],
            artifacts_paths: vec![],
            artifacts_exclude: vec![],
            artifacts_untracked: false,
            artifacts_when: "on_success".to_string(),
            dotenv_report: None,
            timeout: None,
            resource_group: None,
            coverage: None,
            resolved_yaml: String::new(),
            trigger_file: None,
            build_context: None,
            docker_entrypoint: None,
            step_id: None,
            steps,
            debug_info: JobDebugInfo::default(),
        }
    }

    fn make_step(name: Option<&str>, action: Option<StepAction>) -> Step {
        Step {
            name: name.map(|s| s.to_string()),
            uses: None,
            run: None,
            with: IndexMap::new(),
            env: IndexMap::new(),
            if_condition: None,
            id: None,
            action,
        }
    }

    #[test]
    fn test_no_steps_returns_none() {
        let job = make_parent_job(vec![]);
        assert!(build_step_pipeline(&job).is_none());
    }

    #[test]
    fn test_run_steps_become_jobs() {
        let steps = vec![
            make_step(
                Some("Checkout"),
                Some(StepAction::Skip("repo already mounted".into())),
            ),
            make_step(Some("Build"), Some(StepAction::Run("cargo build".into()))),
            make_step(Some("Test"), Some(StepAction::Run("cargo test".into()))),
        ];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();

        assert_eq!(pipeline.stages.len(), 3);
        assert_eq!(pipeline.jobs.len(), 3);

        let checkout = &pipeline.jobs["Checkout"];
        assert!(checkout.rules_never);

        let build = &pipeline.jobs["Build"];
        assert_eq!(build.script, vec!["cargo build"]);
        assert_eq!(build.image, Some("ubuntu:latest".to_string()));

        let test = &pipeline.jobs["Test"];
        assert_eq!(test.script, vec!["cargo test"]);

        assert_eq!(
            pipeline.execution,
            crate::model::ExecutionMode::SharedContainer
        );
    }

    #[test]
    fn test_docker_step_uses_action_image() {
        let steps = vec![make_step(
            Some("CI"),
            Some(StepAction::Docker(Box::new(DockerAction {
                action_dir: std::path::PathBuf::from("/tmp/actions-cache/owner/repo/ref"),
                image: "Dockerfile".to_string(),
                entrypoint: Some("/entrypoint.sh".to_string()),
                args: vec!["--verbose".to_string()],
                action_env: IndexMap::new(),
                input_vars: IndexMap::new(),
            }))),
        )];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();

        let ci = &pipeline.jobs["CI"];
        assert_eq!(ci.image, None);
        assert_eq!(
            ci.build_context,
            Some(std::path::PathBuf::from(
                "/tmp/actions-cache/owner/repo/ref"
            ))
        );
        assert_eq!(ci.docker_entrypoint.as_deref(), Some("/entrypoint.sh"));
        assert_eq!(ci.script, vec!["--verbose"]);
    }

    #[test]
    fn test_step_names_fallback() {
        let mut step_no_name = make_step(None, Some(StepAction::Run("echo hi".into())));
        step_no_name.uses = Some("actions/setup-node@v4".to_string());

        let steps = vec![
            step_no_name,
            make_step(None, Some(StepAction::Run("echo bye".into()))),
        ];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();

        assert!(pipeline.jobs.contains_key("actions/setup-node@v4"));
        assert!(pipeline.jobs.contains_key("Step 2"));
    }

    #[test]
    fn test_checkout_path_extracted() {
        let mut checkout_step = make_step(
            Some("Checkout"),
            Some(StepAction::Skip("repo already mounted".into())),
        );
        checkout_step.uses = Some("actions/checkout@v4".to_string());
        checkout_step
            .with
            .insert("path".to_string(), "src".to_string());

        let steps = vec![
            checkout_step,
            make_step(Some("Build"), Some(StepAction::Run("make".into()))),
        ];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();
        assert_eq!(pipeline.checkout_path.as_deref(), Some("src"));
    }

    #[test]
    fn test_checkout_path_not_set_for_external_repo() {
        let mut checkout_step = make_step(
            Some("Checkout"),
            Some(StepAction::Skip("repo already mounted".into())),
        );
        checkout_step.uses = Some("actions/checkout@v4".to_string());
        checkout_step
            .with
            .insert("repository".to_string(), "other/repo".to_string());
        checkout_step
            .with
            .insert("path".to_string(), "other".to_string());

        let steps = vec![
            checkout_step,
            make_step(Some("Build"), Some(StepAction::Run("make".into()))),
        ];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();
        assert_eq!(pipeline.checkout_path, None);
    }

    #[test]
    fn test_checkout_path_none_when_no_path() {
        let mut checkout_step = make_step(
            Some("Checkout"),
            Some(StepAction::Skip("repo already mounted".into())),
        );
        checkout_step.uses = Some("actions/checkout@v4".to_string());

        let steps = vec![
            checkout_step,
            make_step(Some("Build"), Some(StepAction::Run("make".into()))),
        ];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();
        assert_eq!(pipeline.checkout_path, None);
    }

    #[test]
    fn test_docker_step_injects_input_vars_and_action_env() {
        let mut action_env = IndexMap::new();
        action_env.insert("ACTION_TOKEN".to_string(), "secret".to_string());
        let mut input_vars = IndexMap::new();
        input_vars.insert("task".to_string(), "ci".to_string());
        input_vars.insert("upstream-repo".to_string(), "bluez/bluez".to_string());

        let steps = vec![make_step(
            Some("CI"),
            Some(StepAction::Docker(Box::new(DockerAction {
                action_dir: PathBuf::from("/tmp/action"),
                image: "alpine:3.18".to_string(),
                entrypoint: None,
                args: vec![],
                action_env,
                input_vars,
            }))),
        )];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();

        let ci = &pipeline.jobs["CI"];
        assert_eq!(ci.variables["ACTION_TOKEN"], "secret");
        assert_eq!(ci.variables["INPUT_TASK"], "ci");
        assert_eq!(ci.variables["INPUT_UPSTREAM_REPO"], "bluez/bluez");
    }

    #[test]
    fn test_node_step_injects_input_vars() {
        let mut input_vars = IndexMap::new();
        input_vars.insert("version".to_string(), "20".to_string());

        let steps = vec![make_step(
            Some("Setup"),
            Some(StepAction::Node {
                script: "/tmp/actions-cache/actions/setup-node/v4/dist/index.js".to_string(),
                input_vars,
            }),
        )];
        let job = make_parent_job(steps);
        let pipeline = build_step_pipeline(&job).unwrap();

        let setup = &pipeline.jobs["Setup"];
        assert_eq!(setup.variables["INPUT_VERSION"], "20");
        assert!(setup.script[0].starts_with("node "));
    }

    #[test]
    fn test_env_state_collect_and_apply() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("github_env"), "FOO=bar\nBAZ=qux\n").unwrap();
        std::fs::write(dir.path().join("github_path"), "/custom/bin\n").unwrap();
        std::fs::write(dir.path().join("github_output"), "result=success\n").unwrap();

        let mut state = StepEnvState::default();
        state.collect(dir.path(), Some("setup"));

        assert_eq!(state.env_vars["FOO"], "bar");
        assert_eq!(state.env_vars["BAZ"], "qux");
        assert_eq!(state.path_prepends, vec!["/custom/bin"]);
        assert_eq!(state.outputs["setup"]["result"], "success");

        let mut vars = IndexMap::new();
        vars.insert("EXISTING".to_string(), "keep".to_string());
        state.apply(&mut vars);

        assert_eq!(vars["EXISTING"], "keep");
        assert_eq!(vars["FOO"], "bar");
        assert!(vars["PATH"].starts_with("/custom/bin:"));
        assert_eq!(vars["_STEP_setup_result"], "success");
    }

    #[test]
    fn test_env_state_clears_files() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("github_env"), "A=1\n").unwrap();
        std::fs::write(dir.path().join("github_path"), "").unwrap();

        let mut state = StepEnvState::default();
        state.collect(dir.path(), None);

        let content = std::fs::read_to_string(dir.path().join("github_env")).unwrap();
        assert!(
            content.is_empty(),
            "github_env should be cleared after collect"
        );
    }

    #[test]
    fn test_env_file_heredoc_without_equals() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(
            dir.path().join("github_env"),
            "MULTILINE<<EOF\nline1\nline2\nEOF\nSIMPLE=val\n",
        )
        .unwrap();
        std::fs::write(dir.path().join("github_path"), "").unwrap();

        let mut state = StepEnvState::default();
        state.collect(dir.path(), None);

        assert_eq!(state.env_vars["MULTILINE"], "line1\nline2");
        assert_eq!(state.env_vars["SIMPLE"], "val");
    }

    #[test]
    fn test_env_file_heredoc_with_equals() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("github_env"), "KEY=<<DELIM\nvalue\nDELIM\n").unwrap();
        std::fs::write(dir.path().join("github_path"), "").unwrap();

        let mut state = StepEnvState::default();
        state.collect(dir.path(), None);

        assert_eq!(state.env_vars["KEY"], "value");
    }

    #[test]
    fn test_create_env_dir() {
        let dir = create_env_dir().unwrap();
        assert!(dir.path().join("github_env").exists());
        assert!(dir.path().join("github_path").exists());
        assert!(dir.path().join("github_output").exists());
        assert!(dir.path().join("github_step_summary").exists());
    }
}
