use std::path::Path;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::dag;
use crate::error::ReflectionError;

/// A reflection mode loaded from a Nickel `.ncl` file.
/// Corresponds to the `Mode String` contract in `reflection/schema.ncl`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflectionMode {
    pub id: String,
    pub trigger: String,
    #[serde(default)]
    pub preconditions: Vec<String>,
    #[serde(default)]
    pub guards: Vec<Guard>,
    pub steps: Vec<ActionStep>,
    #[serde(default)]
    pub postconditions: Vec<String>,
    pub converge: Option<Converge>,
}

/// Pre-flight check executed before any step.
/// Block severity aborts mode execution; Warn prints a message and continues.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Guard {
    pub id: String,
    pub cmd: String,
    pub reason: String,
    #[serde(default)]
    pub severity: GuardSeverity,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum GuardSeverity {
    #[default]
    Block,
    Warn,
}

/// Post-execution convergence loop.
/// After all steps complete, evaluates `condition` — if non-zero exit,
/// re-executes steps up to `max_iterations` times.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Converge {
    pub condition: String,
    #[serde(default = "Converge::default_max_iterations")]
    pub max_iterations: u32,
    #[serde(default)]
    pub strategy: ConvergeStrategy,
}

impl Converge {
    fn default_max_iterations() -> u32 {
        3
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConvergeStrategy {
    #[default]
    RetryFailed,
    RetryAll,
}

impl ReflectionMode {
    /// Load a `ReflectionMode` from a Nickel file by invoking `nickel export
    /// --format json`. Mirrors the pattern in
    /// `stratum-orchestrator::graph::loader::load_node_from_ncl`.
    pub fn load(mode_file: &Path) -> Result<Self> {
        let output = std::process::Command::new("nickel")
            .arg("export")
            .arg("--format")
            .arg("json")
            .arg(mode_file)
            .output()
            .with_context(|| format!("running nickel export on '{}'", mode_file.display()))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
            return Err(ReflectionError::NickelExport {
                path: mode_file.display().to_string(),
                stderr,
            }
            .into());
        }

        serde_json::from_slice::<Self>(&output.stdout)
            .map_err(ReflectionError::ParseMode)
            .with_context(|| {
                format!(
                    "parsing ReflectionMode from '{}' — confirm the .ncl exports `| (s.Mode \
                     String)`",
                    mode_file.display()
                )
            })
    }

    /// Validate the step DAG in Rust (uniqueness + referential integrity +
    /// cycle detection). Complements the Nickel-side structural +
    /// referential checks; adds cycle detection that Nickel cannot express.
    pub fn validate(&self) -> Result<(), ReflectionError> {
        dag::validate(&self.steps)
    }

    /// Return steps in a flat topological order (one valid sequencing, no
    /// parallelism). Useful for dry-run display. For parallel execution,
    /// use `dag::topological_layers`.
    pub fn execution_order(&self) -> Result<Vec<&ActionStep>, ReflectionError> {
        let layers = dag::topological_layers(&self.steps)?;
        Ok(layers
            .into_iter()
            .flat_map(|layer| {
                layer
                    .into_iter()
                    .filter_map(|id| self.steps.iter().find(|s| s.id == id))
            })
            .collect())
    }
}

/// A single executable step within a mode.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionStep {
    pub id: String,
    /// Semantic action name (string identifier in cross-project modes).
    pub action: String,
    #[serde(default)]
    pub actor: Actor,
    /// Shell command with optional `{param}` placeholders substituted from
    /// `RunContext.params`.
    pub cmd: Option<String>,
    #[serde(default)]
    pub depends_on: Vec<Dependency>,
    #[serde(default)]
    pub on_error: OnError,
    pub verify: Option<String>,
    pub note: Option<String>,
}

/// Who executes this step.
/// Nickel enum tags serialize with their exact case: `'Human` → `"Human"`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Actor {
    Human,
    Agent,
    #[default]
    Both,
}

/// Dependency edge: this step waits for another step based on `kind`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
    /// ID of the step this step depends on.
    pub step: String,
    #[serde(default)]
    pub kind: DependencyKind,
    pub condition: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum DependencyKind {
    #[default]
    Always,
    OnSuccess,
    OnFailure,
}

/// Error handling strategy for a step.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnError {
    pub strategy: ErrorStrategy,
    pub target: Option<String>,
    pub on_success: Option<String>,
    #[serde(default = "OnError::default_max")]
    pub max: u32,
    /// Backoff in seconds between retry attempts.
    #[serde(default = "OnError::default_backoff_s")]
    pub backoff_s: u64,
}

impl OnError {
    fn default_max() -> u32 {
        3
    }

    fn default_backoff_s() -> u64 {
        5
    }
}

impl Default for OnError {
    fn default() -> Self {
        Self {
            strategy: ErrorStrategy::Stop,
            target: None,
            on_success: None,
            max: Self::default_max(),
            backoff_s: Self::default_backoff_s(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorStrategy {
    Stop,
    Continue,
    Retry,
    Fallback,
    Branch,
}
