//! Workflow step trait

use async_trait::async_trait;

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Error type for workflow steps
#[derive(Debug, thiserror::Error)]
pub enum StepError {
    /// Step execution failed
    #[error("Step execution failed: {0}")]
    ExecutionFailed(String),

    /// Rollback failed
    #[error("Rollback failed: {0}")]
    RollbackFailed(String),

    /// Step not ready
    #[error("Step not ready: {0}")]
    NotReady(String),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Serialization error
    #[error("Serialization error: {0}")]
    SerializationError(String),

    /// Checkpoint error
    #[error("Checkpoint error: {0}")]
    CheckpointError(String),
}

/// Result of a workflow step execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepResult {
    /// Step name
    pub step_name: String,
    /// Step ID
    pub step_id: String,
    /// Whether the step succeeded
    pub success: bool,
    /// Execution time in milliseconds
    pub duration_ms: u64,
    /// Result message
    pub message: Option<String>,
    /// Step output data
    pub output: serde_json::Value,
    /// Whether the step should be retried
    pub retryable: bool,
}

impl StepResult {
    /// Create a successful result
    #[must_use]
    pub fn success(step_id: String, step_name: String, duration_ms: u64) -> Self {
        Self {
            step_name,
            step_id,
            success: true,
            duration_ms,
            message: None,
            output: serde_json::Value::Object(serde_json::Map::default()),
            retryable: false,
        }
    }

    /// Create a failed result
    #[must_use]
    pub fn failed(step_id: String, step_name: String, message: String) -> Self {
        Self {
            step_name,
            step_id,
            success: false,
            duration_ms: 0,
            message: Some(message),
            output: serde_json::Value::Object(serde_json::Map::default()),
            retryable: false,
        }
    }

    /// Set output data
    #[must_use]
    pub fn with_output(mut self, output: serde_json::Value) -> Self {
        self.output = output;
        self
    }

    /// Set as retryable
    #[must_use]
    pub const fn with_retry(mut self) -> Self {
        self.retryable = true;
        self
    }
}

/// Workflow context passed between steps
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowContext {
    /// Workspace path
    pub workspace_path: String,
    /// Target version (if set)
    pub target_version: Option<String>,
    /// Dry run mode
    pub dry_run: bool,
    /// Shared state between steps
    pub state: HashMap<String, serde_json::Value>,
    /// Metrics collected during workflow
    pub metrics: WorkflowMetrics,
    /// CI environment detected
    pub ci_environment: Option<CiEnvironment>,
}

impl WorkflowContext {
    /// Create a new workflow context
    #[must_use]
    pub fn new(workspace_path: String) -> Self {
        Self {
            workspace_path,
            target_version: None,
            dry_run: false,
            state: HashMap::new(),
            metrics: WorkflowMetrics::default(),
            ci_environment: CiEnvironment::detect(),
        }
    }

    /// Set target version
    #[must_use]
    pub fn with_target_version(mut self, version: String) -> Self {
        self.target_version = Some(version);
        self
    }

    /// Set dry run mode
    #[must_use]
    pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
        self.dry_run = dry_run;
        self
    }

    /// Get a value from state
    #[must_use]
    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
        self.state.get(key)
    }

    /// Set a value in state
    pub fn set(&mut self, key: String, value: serde_json::Value) {
        self.state.insert(key, value);
    }

    /// Check if in CI environment
    #[must_use]
    pub const fn is_ci(&self) -> bool {
        self.ci_environment.is_some()
    }
}

/// Metrics collected during workflow execution
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkflowMetrics {
    /// Total execution time in milliseconds
    pub total_duration_ms: u64,
    /// Number of git operations
    pub git_operations: u64,
    /// Number of API calls
    pub api_calls: u64,
    /// Memory usage in MB
    pub memory_usage_mb: u64,
    /// Number of crates processed
    pub crates_processed: u64,
    /// Number of crates published
    pub crates_published: u64,
    /// Number of crates failed
    pub crates_failed: u64,
}

impl WorkflowMetrics {
    /// Add git operation
    pub const fn add_git_op(&mut self) {
        self.git_operations += 1;
    }

    /// Add API call
    pub const fn add_api_call(&mut self) {
        self.api_calls += 1;
    }

    /// Increment crates processed
    pub const fn increment_crates_processed(&mut self) {
        self.crates_processed += 1;
    }

    /// Increment crates published
    pub const fn increment_crates_published(&mut self) {
        self.crates_published += 1;
    }

    /// Increment crates failed
    pub const fn increment_crates_failed(&mut self) {
        self.crates_failed += 1;
    }
}

/// Detected CI environment
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CiEnvironment {
    /// CI platform name
    pub platform: CiPlatform,
    /// Whether running in PR
    pub is_pull_request: bool,
    /// Branch name
    pub branch: Option<String>,
    /// Build number
    pub build_number: Option<String>,
    /// Additional CI-specific data
    pub extra: HashMap<String, String>,
}

impl CiEnvironment {
    /// Detect the current CI environment
    #[must_use]
    pub fn detect() -> Option<Self> {
        let platform = if std::env::var("GITHUB_ACTIONS").is_ok() {
            CiPlatform::GitHubActions
        } else if std::env::var("GITLAB_CI").is_ok() {
            CiPlatform::GitLabCi
        } else if std::env::var("CI").is_ok() {
            CiPlatform::Generic
        } else {
            return None;
        };

        Some(Self {
            platform,
            is_pull_request: Self::is_pr(),
            branch: std::env::var("GITHUB_REF_NAME")
                .or_else(|_| std::env::var("CI_COMMIT_REF_NAME"))
                .ok(),
            build_number: std::env::var("GITHUB_RUN_ID")
                .or_else(|_| std::env::var("CI_PIPELINE_ID"))
                .ok(),
            extra: Self::collect_extra_env(platform),
        })
    }

    fn is_pr() -> bool {
        std::env::var("GITHUB_EVENT_NAME")
            .map(|e| e == "pull_request" || e == "pull_request_target")
            .unwrap_or(false)
            || std::env::var("CI_MERGE_REQUEST_ID").is_ok()
    }

    fn collect_extra_env(platform: CiPlatform) -> HashMap<String, String> {
        let mut extra = HashMap::new();

        match platform {
            CiPlatform::GitHubActions => {
                if let Ok(val) = std::env::var("GITHUB_REPOSITORY") {
                    extra.insert("repository".to_string(), val);
                }
                if let Ok(val) = std::env::var("GITHUB_REF") {
                    extra.insert("ref".to_string(), val);
                }
                if let Ok(val) = std::env::var("GITHUB_SHA") {
                    extra.insert("sha".to_string(), val);
                }
            }
            CiPlatform::GitLabCi => {
                if let Ok(val) = std::env::var("CI_PROJECT_URL") {
                    extra.insert("project_url".to_string(), val);
                }
                if let Ok(val) = std::env::var("CI_COMMIT_SHA") {
                    extra.insert("sha".to_string(), val);
                }
            }
            CiPlatform::Generic => {}
        }

        extra
    }
}

/// CI platform types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CiPlatform {
    /// GitHub Actions
    GitHubActions,
    /// GitLab CI
    GitLabCi,
    /// Generic CI environment
    Generic,
}

/// Trait for a workflow step
#[async_trait]
pub trait WorkflowStep: Send + Sync {
    /// Get the step name (human-readable)
    fn name(&self) -> &str;

    /// Get the step ID (unique identifier)
    fn id(&self) -> &str;

    /// Execute the step
    async fn execute(&self, ctx: &mut WorkflowContext) -> Result<StepResult, StepError>;

    /// Rollback the step
    async fn rollback(&self, ctx: &WorkflowContext) -> Result<(), StepError>;

    /// Check if this step can be skipped
    async fn can_skip(&self, ctx: &WorkflowContext) -> bool {
        ctx.dry_run && !matches!(self.id(), "analyze" | "plan" | "check" | "simulate")
    }

    /// Check if the step is idempotent
    fn is_idempotent(&self) -> bool {
        false
    }

    /// Serialize the step state for checkpointing
    ///
    /// # Errors
    ///
    /// Returns `StepError` if serialization fails
    fn serialize_state(&self) -> Result<serde_json::Value, StepError> {
        Ok(serde_json::json!({
            "id": self.id(),
            "name": self.name(),
        }))
    }

    /// Get dependencies (step IDs that must complete before this step)
    fn dependencies(&self) -> Vec<String> {
        Vec::new()
    }
}

/// Error handling strategy for workflows
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ErrorHandlingStrategy {
    /// Stop on first error
    #[default]
    Stop,
    /// Skip failed crate, continue with others
    SkipAndContinue,
    /// Retry failed operations
    Retry {
        /// Maximum number of retry attempts
        max_attempts: usize,
        /// Delay in seconds between retries
        delay_secs: u64,
    },
    /// Rollback all changes on error
    Rollback,
}

/// A batch of steps that can be executed in parallel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepBatch {
    /// Batch number
    pub batch_number: usize,
    /// Steps in this batch
    pub steps: Vec<String>,
    /// Whether steps in this batch can run in parallel
    pub can_parallelize: bool,
}

impl StepBatch {
    /// Create a new batch
    #[must_use]
    pub const fn new(batch_number: usize, steps: Vec<String>, can_parallelize: bool) -> Self {
        Self {
            batch_number,
            steps,
            can_parallelize,
        }
    }

    /// Check if the batch is empty
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.steps.is_empty()
    }

    /// Get the number of steps in the batch
    #[must_use]
    pub const fn len(&self) -> usize {
        self.steps.len()
    }
}

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

    #[test]
    fn test_ci_detection() {
        // In CI, this would detect the platform
        // For testing, we just check the struct
        let env = CiEnvironment {
            platform: CiPlatform::GitHubActions,
            is_pull_request: true,
            branch: Some("main".to_string()),
            build_number: Some("42".to_string()),
            extra: HashMap::new(),
        };
        assert_eq!(env.platform, CiPlatform::GitHubActions);
        assert!(env.is_pull_request);
    }

    #[test]
    fn test_workflow_context() {
        let mut ctx = WorkflowContext::new("/test".to_string())
            .with_target_version("1.0.0".to_string())
            .with_dry_run(true);

        assert_eq!(ctx.target_version, Some("1.0.0".to_string()));
        assert!(ctx.dry_run);

        ctx.set("key".to_string(), serde_json::json!("value"));
        assert_eq!(ctx.get("key"), Some(&serde_json::json!("value")));
    }

    #[test]
    fn test_workflow_metrics() {
        let mut metrics = WorkflowMetrics::default();
        metrics.add_git_op();
        metrics.add_api_call();
        metrics.increment_crates_processed();
        metrics.increment_crates_published();

        assert_eq!(metrics.git_operations, 1);
        assert_eq!(metrics.api_calls, 1);
        assert_eq!(metrics.crates_processed, 1);
        assert_eq!(metrics.crates_published, 1);
        assert_eq!(metrics.crates_failed, 0);
    }

    #[test]
    fn test_step_result() {
        let result = StepResult::success("step-1".to_string(), "Step 1".to_string(), 100);
        assert!(result.success);
        assert_eq!(result.step_id, "step-1");
        assert_eq!(result.duration_ms, 100);

        let failed = StepResult::failed(
            "step-2".to_string(),
            "Step 2".to_string(),
            "Error".to_string(),
        );
        assert!(!failed.success);
        assert_eq!(failed.message, Some("Error".to_string()));
    }

    #[test]
    fn test_step_batch() {
        let batch = StepBatch::new(1, vec!["step-1".to_string(), "step-2".to_string()], false);
        assert_eq!(batch.batch_number, 1);
        assert_eq!(batch.len(), 2);
        assert!(!batch.can_parallelize);
    }
}
