//! Pure "what's the next thing to do" decision logic.
//!
//! The runner needs the answer to "given this envelope + this topology,
//! which step should we activate or skip next?" — and that answer is a
//! pure function of envelope + topology. Extracting it here keeps the
//! decision logic independently testable without IO.
//!
//! What lives here:
//! - [`DepDecision`] — how an `after`-deps check resolves (Ready / NotYet
//!   / AllSkipped).
//! - [`NextStep`] — the runner-facing answer (Activate / Skip / None).
//! - [`decide_deps_ready`] — pure: walks `after` deps under a
//!   `MergeKind`, returns the resolution.
//! - [`next_step`] — pure: walks the topology in declaration order
//!   to find the next activatable or skippable step.
//!
//! What does NOT live here: anything that mutates state, calls
//! handlers, performs IO, or knows about the runner's instance map.
//! The output of `next_step` is consumed by the *runner* to decide
//! what to commit.

use crate::builder::{MergeKind, SagaBuilder};
use crate::envelope::{SagaEnvelope, SagaStepStatus};
use crate::saga::Saga;
use crate::step::StepName;

/// What the runner should do for the next-ready step it finds.
#[derive(Debug, Clone)]
pub enum NextStep {
    /// Step is ready and not gated out — activate it.
    Activate(StepName),
    /// Step's gate or skip cascade says don't run it — mark Skipped
    /// with the included reason.
    Skip(StepName, String),
    /// Nothing to do this pass — saga is either quiescent or terminal.
    None,
}

/// How an `after`-deps check resolves.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepDecision {
    /// All required deps satisfied per merge_kind.
    Ready,
    /// At least one required dep is still in-flight.
    NotYet,
    /// All listed deps ended up `Skipped` → cascade skip.
    AllSkipped,
}

/// Resolve a list of `after` deps under a [`MergeKind`].
///
/// - `MergeKind::All` — every dep must be `Completed`. A single
///   `Skipped` upstream cascades the skip.
/// - `MergeKind::Any` — at least one `Completed` is enough.
///   If every dep resolves to a non-`Completed` terminal status
///   (`Skipped`, `Failed`, `Compensated`), cascade the skip.
pub fn decide_deps_ready<S: Saga>(
    envelope: &SagaEnvelope<S>,
    after: &[StepName],
    merge_kind: MergeKind,
) -> DepDecision {
    if after.is_empty() {
        return DepDecision::Ready;
    }
    let statuses: Vec<SagaStepStatus> = after.iter().map(|n| envelope.step_status(n)).collect();
    let any_completed = statuses
        .iter()
        .any(|s| matches!(s, SagaStepStatus::Completed));
    let all_skipped = statuses
        .iter()
        .all(|s| matches!(s, SagaStepStatus::Skipped));
    let all_resolved = statuses.iter().all(|s| {
        matches!(
            s,
            SagaStepStatus::Completed
                | SagaStepStatus::Skipped
                | SagaStepStatus::Failed
                | SagaStepStatus::Compensated
        )
    });

    if all_skipped {
        return DepDecision::AllSkipped;
    }

    match merge_kind {
        MergeKind::All => {
            if statuses
                .iter()
                .any(|s| matches!(s, SagaStepStatus::Skipped))
            {
                DepDecision::AllSkipped
            } else if statuses
                .iter()
                .all(|s| matches!(s, SagaStepStatus::Completed))
            {
                DepDecision::Ready
            } else {
                DepDecision::NotYet
            }
        }
        MergeKind::Any => {
            if any_completed {
                DepDecision::Ready
            } else if all_resolved {
                DepDecision::AllSkipped
            } else {
                DepDecision::NotYet
            }
        }
    }
}

/// Walk the topology in declaration order. Return the first step
/// that's either ready to activate or should be skipped. Considers:
/// - `after` deps (via [`decide_deps_ready`]) under `merge_kind`
/// - `state_gate` predicate (skip if false / undecodable)
/// - `gate` predicate over upstream output (skip if false / cascade
///   if upstream is skipped)
///
/// Pure function of `envelope` + `topology`. Callers commit the
/// implied state transition.
pub fn next_step<S: Saga>(envelope: &SagaEnvelope<S>, topology: &SagaBuilder<S>) -> NextStep {
    // NOTE: the terminal-status guard (review SAGA-8/DX-11 — stop activating
    // steps once the saga is Confirmed/Cancelled) is intentionally NOT here
    // yet. It is only correct under the wave-based drive loop
    // (`tasks/effects-vs-commit.md` §3), where a whole wave of ready steps
    // activates before any effect runs. On the current one-step-at-a-time
    // driver it would make parallel-sibling outcomes order-dependent (the
    // alphabetically-first branch's failure would block its ready siblings).
    // It lands with the loop rewrite.
    for entry in topology.step_entries() {
        if !matches!(envelope.step_status(entry.name), SagaStepStatus::Pending) {
            continue;
        }
        let dep_decision = decide_deps_ready(envelope, &entry.after, entry.merge_kind);
        match dep_decision {
            DepDecision::AllSkipped => {
                return NextStep::Skip(
                    entry.name,
                    format!("all `after` predecessors of `{}` were skipped", entry.name),
                );
            }
            DepDecision::NotYet => continue,
            DepDecision::Ready => {}
        }
        // Evaluate state gate first — cheaper than upstream lookup
        // when state is small.
        if let Some(sg) = &entry.state_gate {
            let state_json = match serde_json::to_value(&envelope.state) {
                Ok(v) => v,
                Err(e) => {
                    return NextStep::Skip(
                        entry.name,
                        format!("state gate could not serialise state: {e}"),
                    );
                }
            };
            match (sg.predicate)(&state_json) {
                Ok(true) => {}
                Ok(false) => {
                    return NextStep::Skip(entry.name, "state gate evaluated false".into());
                }
                Err(e) => {
                    return NextStep::Skip(
                        entry.name,
                        format!("state gate could not decode state: {e}"),
                    );
                }
            }
        }
        // Evaluate upstream-output gate.
        if let Some(gate) = &entry.gate {
            let upstream_output = envelope.outputs.get(gate.upstream);
            match upstream_output {
                Some(value) => match (gate.predicate)(value) {
                    Ok(true) => {}
                    Ok(false) => {
                        return NextStep::Skip(
                            entry.name,
                            format!("gate on upstream `{}` evaluated false", gate.upstream),
                        );
                    }
                    Err(e) => {
                        return NextStep::Skip(
                            entry.name,
                            format!("gate on upstream `{}` could not decode: {e}", gate.upstream),
                        );
                    }
                },
                None => {
                    let up_status = envelope.step_status(gate.upstream);
                    if matches!(up_status, SagaStepStatus::Skipped) {
                        return NextStep::Skip(
                            entry.name,
                            format!("gate upstream `{}` was skipped", gate.upstream),
                        );
                    }
                    // Upstream still in-flight: come back later.
                    continue;
                }
            }
        }
        return NextStep::Activate(entry.name);
    }
    NextStep::None
}
