use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CirSequenceStepKind {
    Full,
    Delta,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CirSequenceStep {
    pub step_id: String,
    pub kind: CirSequenceStepKind,
    pub validation_time: String,
    pub cir_path: String,
    pub ccr_path: String,
    pub report_path: String,
    #[serde(default)]
    pub timing_path: Option<String>,
    #[serde(default)]
    pub stdout_log_path: Option<String>,
    #[serde(default)]
    pub stderr_log_path: Option<String>,
    #[serde(default)]
    pub artifact_prefix: Option<String>,
    pub previous_step_id: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CirSequenceManifest {
    pub version: u32,
    pub repo_bytes_db_path: String,
    pub steps: Vec<CirSequenceStep>,
}

impl CirSequenceManifest {
    pub fn validate(&self) -> Result<(), String> {
        if self.version == 0 {
            return Err("sequence.version must be positive".to_string());
        }
        if self.repo_bytes_db_path.trim().is_empty() {
            return Err("sequence.repo_bytes_db_path must not be empty".to_string());
        }
        if self.steps.is_empty() {
            return Err("sequence.steps must not be empty".to_string());
        }
        let mut previous_ids = std::collections::BTreeSet::new();
        for (idx, step) in self.steps.iter().enumerate() {
            if step.step_id.trim().is_empty() {
                return Err(format!("sequence.steps[{idx}].step_id must not be empty"));
            }
            if !previous_ids.insert(step.step_id.clone()) {
                return Err(format!("sequence.steps[{idx}].step_id must be unique"));
            }
            if step.validation_time.trim().is_empty() {
                return Err(format!(
                    "sequence.steps[{idx}].validation_time must not be empty"
                ));
            }
            if let Some(timing_path) = &step.timing_path
                && timing_path.trim().is_empty()
            {
                return Err(format!(
                    "sequence.steps[{idx}].timing_path must not be empty when set"
                ));
            }
            if let Some(stdout_log_path) = &step.stdout_log_path
                && stdout_log_path.trim().is_empty()
            {
                return Err(format!(
                    "sequence.steps[{idx}].stdout_log_path must not be empty when set"
                ));
            }
            if let Some(stderr_log_path) = &step.stderr_log_path
                && stderr_log_path.trim().is_empty()
            {
                return Err(format!(
                    "sequence.steps[{idx}].stderr_log_path must not be empty when set"
                ));
            }
            if let Some(artifact_prefix) = &step.artifact_prefix
                && artifact_prefix.trim().is_empty()
            {
                return Err(format!(
                    "sequence.steps[{idx}].artifact_prefix must not be empty when set"
                ));
            }
            if step.cir_path.trim().is_empty()
                || step.ccr_path.trim().is_empty()
                || step.report_path.trim().is_empty()
            {
                return Err(format!(
                    "sequence.steps[{idx}] output paths must not be empty"
                ));
            }
            match step.kind {
                CirSequenceStepKind::Full => {
                    if idx != 0 {
                        return Err("full step must be the first step".to_string());
                    }
                    if step.previous_step_id.is_some() {
                        return Err("full step must not reference previous_step_id".to_string());
                    }
                }
                CirSequenceStepKind::Delta => {
                    if idx == 0 {
                        return Err("delta step cannot be the first step".to_string());
                    }
                    let previous = step.previous_step_id.as_ref().ok_or_else(|| {
                        format!("sequence.steps[{idx}] delta step must set previous_step_id")
                    })?;
                    if !previous_ids.contains(previous) {
                        return Err(format!(
                            "sequence.steps[{idx}] previous_step_id must reference an earlier step"
                        ));
                    }
                }
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};

    fn sample_manifest() -> CirSequenceManifest {
        CirSequenceManifest {
            version: 1,
            repo_bytes_db_path: "repo-bytes.db".to_string(),
            steps: vec![
                CirSequenceStep {
                    step_id: "full".to_string(),
                    kind: CirSequenceStepKind::Full,
                    validation_time: "2026-04-09T00:00:00Z".to_string(),
                    cir_path: "full/input.cir".to_string(),
                    ccr_path: "full/result.ccr".to_string(),
                    report_path: "full/report.json".to_string(),
                    timing_path: Some("full/timing.json".to_string()),
                    stdout_log_path: Some("full/stdout.log".to_string()),
                    stderr_log_path: Some("full/stderr.log".to_string()),
                    artifact_prefix: Some("2026-04-09T00:00:00Z-test".to_string()),
                    previous_step_id: None,
                },
                CirSequenceStep {
                    step_id: "delta-001".to_string(),
                    kind: CirSequenceStepKind::Delta,
                    validation_time: "2026-04-09T00:10:00Z".to_string(),
                    cir_path: "delta-001/input.cir".to_string(),
                    ccr_path: "delta-001/result.ccr".to_string(),
                    report_path: "delta-001/report.json".to_string(),
                    timing_path: Some("delta-001/timing.json".to_string()),
                    stdout_log_path: Some("delta-001/stdout.log".to_string()),
                    stderr_log_path: Some("delta-001/stderr.log".to_string()),
                    artifact_prefix: Some("2026-04-09T00:10:00Z-test".to_string()),
                    previous_step_id: Some("full".to_string()),
                },
            ],
        }
    }

    #[test]
    fn sequence_manifest_validate_accepts_minimal_chain() {
        sample_manifest().validate().expect("valid sequence");
    }

    #[test]
    fn sequence_manifest_validate_rejects_bad_order_and_duplicates() {
        let mut bad = sample_manifest();
        bad.steps.swap(0, 1);
        let err = bad.validate().expect_err("full must be first");
        assert!(
            err.contains("delta step cannot be the first step")
                || err.contains("full step must be the first step")
        );

        let mut dup = sample_manifest();
        dup.steps[1].step_id = "full".to_string();
        let err = dup.validate().expect_err("duplicate id must fail");
        assert!(err.contains("must be unique"));
    }

    #[test]
    fn sequence_manifest_validate_rejects_missing_previous_reference() {
        let mut bad = sample_manifest();
        bad.steps[1].previous_step_id = Some("missing".to_string());
        let err = bad.validate().expect_err("missing previous must fail");
        assert!(err.contains("previous_step_id"));
    }

    #[test]
    fn sequence_manifest_validate_rejects_empty_repo_bytes_backend() {
        let mut manifest = sample_manifest();
        manifest.repo_bytes_db_path = " ".to_string();
        let err = manifest
            .validate()
            .expect_err("empty repo bytes backend must fail");
        assert!(err.contains("repo_bytes_db_path"));
    }
}
