use super::Job;
use indexmap::IndexMap;

/// Normalize a string for fuzzy matching: lowercase and treat '-' and '_' as equivalent.
fn normalize(s: &str) -> String {
    s.to_lowercase().replace('-', "_")
}

/// A stage in the resolved pipeline.
#[derive(Debug, Clone)]
pub struct Stage {
    pub name: String,
    pub job_names: Vec<String>,
}

/// The fully resolved pipeline, ready for graph building and display.
#[derive(Debug, Clone)]
pub struct Pipeline {
    pub stages: Vec<Stage>,
    pub jobs: IndexMap<String, Job>,
}

impl Pipeline {
    pub fn stage_index(&self, stage_name: &str) -> Option<usize> {
        self.stages.iter().position(|s| s.name == stage_name)
    }

    /// Find a job by name with fuzzy matching.
    ///
    /// Tries exact match, then case-insensitive, then substring.
    /// Returns the canonical job name or an error listing available jobs.
    pub fn find_job(&self, query: &str) -> Result<&str, String> {
        // Exact match
        if let Some((name, _)) = self.jobs.get_key_value(query) {
            return Ok(name.as_str());
        }

        // Normalized exact match (case-insensitive, '-' == '_')
        let norm = normalize(query);
        let ci_matches: Vec<&str> = self
            .jobs
            .keys()
            .filter(|k| normalize(k) == norm)
            .map(|k| k.as_str())
            .collect();
        if ci_matches.len() == 1 {
            return Ok(ci_matches[0]);
        }

        // Substring match (normalized)
        let substr_matches: Vec<&str> = self
            .jobs
            .keys()
            .filter(|k| normalize(k).contains(&norm))
            .map(|k| k.as_str())
            .collect();
        if substr_matches.len() == 1 {
            return Ok(substr_matches[0]);
        }

        let available: Vec<&str> = self.jobs.keys().map(|k| k.as_str()).collect();
        if substr_matches.len() > 1 {
            Err(format!(
                "Ambiguous job name '{}'. Matches: {}\nAvailable jobs: {}",
                query,
                substr_matches.join(", "),
                available.join(", ")
            ))
        } else {
            Err(format!(
                "Job '{}' not found. Available jobs: {}",
                query,
                available.join(", ")
            ))
        }
    }

    /// Find all jobs matching a query (for --skip where ambiguity is desired).
    ///
    /// Supports prefixes to restrict matching:
    /// - `stage=<name>` — match only stage names, return all jobs in matching stages
    /// - `job=<name>` — match only job names (no stage matching)
    /// - no prefix — match both stages and jobs
    ///
    /// For job matching: exact, then case-insensitive exact, then substring.
    /// Returns all matches at the best matching level.
    pub fn find_jobs(&self, query: &str) -> Vec<&str> {
        if let Some(stage_query) = query.strip_prefix("stage=") {
            return self.find_jobs_by_stage(stage_query);
        }
        let job_query = query.strip_prefix("job=").unwrap_or(query);

        // Exact match
        if let Some((name, _)) = self.jobs.get_key_value(job_query) {
            return vec![name.as_str()];
        }

        // Normalized exact matches (case-insensitive, '-' == '_')
        let norm = normalize(job_query);
        let ci_matches: Vec<&str> = self
            .jobs
            .keys()
            .filter(|k| normalize(k) == norm)
            .map(|k| k.as_str())
            .collect();
        if !ci_matches.is_empty() {
            return ci_matches;
        }

        // Substring matches on job names (normalized)
        let substr_matches: Vec<&str> = self
            .jobs
            .keys()
            .filter(|k| normalize(k).contains(&norm))
            .map(|k| k.as_str())
            .collect();
        if !substr_matches.is_empty() {
            return substr_matches;
        }

        // No job= prefix — also try matching stage names
        if query == job_query {
            let stage_matches = self.find_jobs_by_stage(job_query);
            if !stage_matches.is_empty() {
                return stage_matches;
            }
        }

        vec![]
    }

    /// Find all jobs belonging to stages that match the query.
    /// Exact, then case-insensitive, then substring.
    fn find_jobs_by_stage(&self, query: &str) -> Vec<&str> {
        let norm = normalize(query);

        // Exact stage match
        for stage in &self.stages {
            if stage.name == query {
                return stage.job_names.iter().map(|s| s.as_str()).collect();
            }
        }

        // Normalized exact (case-insensitive, '-' == '_')
        let ci: Vec<&Stage> = self
            .stages
            .iter()
            .filter(|s| normalize(&s.name) == norm)
            .collect();
        if !ci.is_empty() {
            return ci
                .iter()
                .flat_map(|s| s.job_names.iter().map(|n| n.as_str()))
                .collect();
        }

        // Substring (normalized)
        let substr: Vec<&Stage> = self
            .stages
            .iter()
            .filter(|s| normalize(&s.name).contains(&norm))
            .collect();
        substr
            .iter()
            .flat_map(|s| s.job_names.iter().map(|n| n.as_str()))
            .collect()
    }
}

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

    fn make_job(name: &str, stage: &str) -> Job {
        Job {
            name: name.to_string(),
            stage: stage.to_string(),
            image: None,
            services: vec![],
            variables: IndexMap::new(),
            before_script: vec![],
            script: vec!["echo hi".to_string()],
            after_script: vec![],
            needs: vec![],
            tags: vec![],
            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_when: "on_success".to_string(),
            timeout: None,
            resource_group: None,
            coverage: None,
            resolved_yaml: String::new(),
            trigger_file: None,
            steps: vec![],
            debug_info: JobDebugInfo::default(),
        }
    }

    fn make_pipeline(job_names: &[&str]) -> Pipeline {
        let jobs: IndexMap<String, Job> = job_names
            .iter()
            .map(|name| (name.to_string(), make_job(name, "test")))
            .collect();
        Pipeline {
            stages: vec![Stage {
                name: "test".to_string(),
                job_names: job_names.iter().map(|s| s.to_string()).collect(),
            }],
            jobs,
        }
    }

    fn make_staged_pipeline(stages: &[(&str, &[&str])]) -> Pipeline {
        let mut jobs = IndexMap::new();
        let mut stage_list = Vec::new();
        for (stage_name, job_names) in stages {
            stage_list.push(Stage {
                name: stage_name.to_string(),
                job_names: job_names.iter().map(|s| s.to_string()).collect(),
            });
            for name in *job_names {
                jobs.insert(name.to_string(), make_job(name, stage_name));
            }
        }
        Pipeline {
            stages: stage_list,
            jobs,
        }
    }

    #[test]
    fn test_find_job_exact() {
        let p = make_pipeline(&["build", "test", "deploy"]);
        assert_eq!(p.find_job("build"), Ok("build"));
    }

    #[test]
    fn test_find_job_case_insensitive() {
        let p = make_pipeline(&["Build", "test"]);
        assert_eq!(p.find_job("build"), Ok("Build"));
    }

    #[test]
    fn test_find_job_substring() {
        let p = make_pipeline(&["build [os: ubuntu-latest]", "test"]);
        assert_eq!(p.find_job("build"), Ok("build [os: ubuntu-latest]"));
    }

    #[test]
    fn test_find_job_ambiguous() {
        let p = make_pipeline(&["build-debug", "build-release"]);
        let err = p.find_job("build").unwrap_err();
        assert!(err.contains("Ambiguous"));
        assert!(err.contains("build-debug"));
        assert!(err.contains("build-release"));
    }

    #[test]
    fn test_find_job_not_found() {
        let p = make_pipeline(&["build", "test"]);
        let err = p.find_job("deploy").unwrap_err();
        assert!(err.contains("not found"));
        assert!(err.contains("build"));
    }

    #[test]
    fn test_find_jobs_by_stage() {
        let p = make_staged_pipeline(&[
            ("build", &["compile", "lint"]),
            ("test", &["unit", "integration"]),
        ]);
        let mut matches = p.find_jobs("build");
        matches.sort();
        assert_eq!(matches, vec!["compile", "lint"]);
    }

    #[test]
    fn test_find_jobs_stage_prefix() {
        let p = make_staged_pipeline(&[
            ("build", &["compile", "lint"]),
            ("test", &["unit", "build-check"]),
        ]);
        // stage=build should only match the stage, not job names
        let mut matches = p.find_jobs("stage=build");
        matches.sort();
        assert_eq!(matches, vec!["compile", "lint"]);
    }

    #[test]
    fn test_find_jobs_job_prefix() {
        let p = make_staged_pipeline(&[
            ("build", &["compile", "lint"]),
            ("test", &["unit", "build-check"]),
        ]);
        // job=build should only match job names, not stages
        let matches = p.find_jobs("job=build");
        assert_eq!(matches, vec!["build-check"]);
    }

    #[test]
    fn test_find_jobs_prefers_job_over_stage() {
        let p = make_staged_pipeline(&[
            ("build", &["compile", "lint"]),
            ("test", &["unit", "build"]),
        ]);
        // bare "build" should match the job name first (exact match)
        let matches = p.find_jobs("build");
        assert_eq!(matches, vec!["build"]);
    }

    #[test]
    fn test_find_jobs_falls_back_to_stage() {
        let p = make_staged_pipeline(&[
            ("container", &["build-image", "push-image"]),
            ("test", &["unit"]),
        ]);
        // "container" doesn't match any job name, falls back to stage
        let mut matches = p.find_jobs("container");
        matches.sort();
        assert_eq!(matches, vec!["build-image", "push-image"]);
    }

    #[test]
    fn test_find_jobs_stage_substring() {
        let p = make_staged_pipeline(&[("build-for-tests", &["compile"]), ("test", &["unit"])]);
        let matches = p.find_jobs("stage=build-for");
        assert_eq!(matches, vec!["compile"]);
    }

    #[test]
    fn test_find_job_dash_underscore_equivalence() {
        let p = make_pipeline(&["build-kernel"]);
        assert_eq!(p.find_job("build_kernel").unwrap(), "build-kernel");
        assert_eq!(p.find_job("Build_Kernel").unwrap(), "build-kernel");

        let p2 = make_pipeline(&["test_suite"]);
        assert_eq!(p2.find_job("test-suite").unwrap(), "test_suite");
    }

    #[test]
    fn test_find_jobs_dash_underscore_equivalence() {
        let p = make_pipeline(&["build-kernel", "build-tools"]);
        let matches = p.find_jobs("build_kernel");
        assert_eq!(matches, vec!["build-kernel"]);
    }

    #[test]
    fn test_find_jobs_stage_dash_underscore() {
        let p = make_staged_pipeline(&[("unit-tests", &["test-a"]), ("lint", &["check"])]);
        let matches = p.find_jobs("stage=unit_tests");
        assert_eq!(matches, vec!["test-a"]);
    }
}
