use std::collections::{BTreeMap, HashSet};

use serde::{Deserialize, Serialize};

/// Generic, ecosystem-agnostic markers that indicate a runner failed because of
/// its own infrastructure (missing harness wrapper, bootstrap aborted, etc.)
/// rather than because the component under test had findings.
///
/// Core only knows these vocabulary-neutral phrases. Any ecosystem- or
/// product-specific failure signatures (e.g. a particular interpreter crash or
/// a bootstrap script path) are the responsibility of the extension that owns
/// that ecosystem — core must not branch on them. Detection of those belongs
/// in the extension's own runner output, not in this list.
pub const GENERIC_INFRASTRUCTURE_FAILURE_MARKERS: &[&str] = &[
    "bootstrap failure:",
    "test harness infrastructure failure",
    "lint runner infrastructure failure",
];

/// Shared verification phase vocabulary for isolated commands and composed runners.
///
/// `homeboy lint`, `homeboy audit`, and `homeboy test` stay independent. A
/// future composed command can run these phases in canonical order while reusing
/// the same phase reports and exit-code semantics.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum VerificationPhase {
    Syntax,
    Lint,
    Typecheck,
    Audit,
    Test,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum PhaseStatus {
    Passed,
    Failed,
    Error,
    Skipped,
    NotRun,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PhaseFailureCategory {
    Findings,
    Infrastructure,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PhaseReport {
    pub phase: VerificationPhase,
    pub status: PhaseStatus,
    pub exit_code: Option<i32>,
    pub summary: String,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PhaseFailure {
    pub phase: VerificationPhase,
    pub category: PhaseFailureCategory,
    pub summary: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExtensionPhaseTiming {
    pub name: String,
    pub duration_ms: u64,
    /// Provider-declared generic state for this phase, for example `running`,
    /// `waiting`, `blocked`, `queued`, `passed`, or `failed`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Human-readable provider summary for the phase. Core treats this as
    /// opaque text and does not infer tool-specific behavior from it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub artifacts: Vec<serde_json::Value>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub metadata: BTreeMap<String, serde_json::Value>,
}

pub fn phase_status_from_exit_code(exit_code: i32) -> PhaseStatus {
    if exit_code == 0 {
        PhaseStatus::Passed
    } else if exit_code >= 2 {
        PhaseStatus::Error
    } else {
        PhaseStatus::Failed
    }
}

pub fn phase_failure_category_from_exit_code(exit_code: i32) -> PhaseFailureCategory {
    if exit_code >= 2 {
        PhaseFailureCategory::Infrastructure
    } else {
        PhaseFailureCategory::Findings
    }
}

/// Generic step filter contract for extension runner scripts.
#[derive(Debug, Clone, Default)]
pub struct RunnerStepFilter {
    pub step: Option<String>,
    pub skip: Option<String>,
}

impl RunnerStepFilter {
    /// Returns true if a step should run under current filter settings.
    pub fn should_run(&self, step_name: &str) -> bool {
        let step_name = step_name.trim();
        if step_name.is_empty() {
            return true;
        }

        let selected = csv_set(self.step.as_deref());
        if !selected.is_empty() && !selected.contains(step_name) {
            return false;
        }

        let skipped = csv_set(self.skip.as_deref());
        if skipped.contains(step_name) {
            return false;
        }

        true
    }

    /// Convert filter to env vars understood by extension scripts.
    pub fn to_env_pairs(&self) -> Vec<(String, String)> {
        let mut env = Vec::new();
        if let Some(step) = &self.step {
            env.push((super::exec_context::STEP.to_string(), step.clone()));
        }
        if let Some(skip) = &self.skip {
            env.push((super::exec_context::SKIP.to_string(), skip.clone()));
        }
        env
    }
}

fn csv_set(value: Option<&str>) -> HashSet<String> {
    value
        .unwrap_or_default()
        .split(',')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(ToString::to_string)
        .collect()
}

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

    #[test]
    fn test_should_run_with_no_filters() {
        let filter = RunnerStepFilter::default();
        assert!(filter.should_run("lint"));
    }

    #[test]
    fn test_should_run_honors_step_include() {
        let filter = RunnerStepFilter {
            step: Some("lint,test".to_string()),
            skip: None,
        };
        assert!(filter.should_run("lint"));
        assert!(!filter.should_run("deploy"));
    }

    #[test]
    fn test_should_run_honors_skip() {
        let filter = RunnerStepFilter {
            step: None,
            skip: Some("lint".to_string()),
        };
        assert!(!filter.should_run("lint"));
        assert!(filter.should_run("test"));
    }

    #[test]
    fn test_to_env_pairs_exports_step_and_skip() {
        let filter = RunnerStepFilter {
            step: Some("a".to_string()),
            skip: Some("b".to_string()),
        };
        let env = filter.to_env_pairs();
        assert_eq!(env.len(), 2);
        assert!(env.iter().any(|(k, v)| k == "HOMEBOY_STEP" && v == "a"));
        assert!(env.iter().any(|(k, v)| k == "HOMEBOY_SKIP" && v == "b"));
    }

    #[test]
    fn test_csv_set() {
        let set = csv_set(Some("lint, test,,"));
        assert!(set.contains("lint"));
        assert!(set.contains("test"));
        assert_eq!(set.len(), 2);
    }

    #[test]
    fn test_should_run() {
        let filter = RunnerStepFilter {
            step: Some("lint,test".to_string()),
            skip: Some("test".to_string()),
        };
        assert!(filter.should_run("lint"));
        assert!(!filter.should_run("test"));
        assert!(!filter.should_run("deploy"));
    }

    #[test]
    fn test_to_env_pairs() {
        let filter = RunnerStepFilter {
            step: Some("a".to_string()),
            skip: Some("b".to_string()),
        };
        let env = filter.to_env_pairs();
        assert!(env.iter().any(|(k, v)| k == "HOMEBOY_STEP" && v == "a"));
        assert!(env.iter().any(|(k, v)| k == "HOMEBOY_SKIP" && v == "b"));
    }

    #[test]
    fn phase_exit_codes_are_classified() {
        assert_eq!(phase_status_from_exit_code(0), PhaseStatus::Passed);
        assert_eq!(phase_status_from_exit_code(1), PhaseStatus::Failed);
        assert_eq!(phase_status_from_exit_code(2), PhaseStatus::Error);
        assert_eq!(
            phase_failure_category_from_exit_code(1),
            PhaseFailureCategory::Findings
        );
        assert_eq!(
            phase_failure_category_from_exit_code(2),
            PhaseFailureCategory::Infrastructure
        );
    }
}

#[cfg(test)]
#[path = "../../../tests/core/extension/runner_contract_test.rs"]
mod runner_contract_test;
