//! Pipeline failure modes — distinguish recoverable user errors from
//! invariant violations.

use thiserror::Error;

use crate::validation::Verdict;

/// Failures emitted by the runtime pipeline.
#[derive(Debug, Error)]
pub enum OpError {
    /// No operation with the requested id is registered.
    #[error("operation '{0}' is not registered")]
    UnknownOperation(String),

    /// A pre- or post-validator rejected the operation.
    #[error("validator rejected operation '{op_id}': {verdict:?}")]
    ValidatorRejected {
        /// Operation id that was rejected.
        op_id: String,
        /// Verdict returned by the rejecting validator.
        verdict: Verdict,
    },

    /// Loading the precondition slice failed.
    #[error("precondition load for '{op_id}': {reason}")]
    PreconditionLoad {
        /// Operation id whose precondition failed to load.
        op_id: String,
        /// Human-readable reason.
        reason: String,
    },

    /// The op body returned an error.
    #[error("op body for '{op_id}': {reason}")]
    OpBody {
        /// Operation id whose body returned an error.
        op_id: String,
        /// Human-readable reason.
        reason: String,
    },

    /// Render of one of the op's render paths failed.
    #[error("render '{path}': {reason}")]
    Render {
        /// Render path that failed.
        path: String,
        /// Human-readable reason.
        reason: String,
    },

    /// Actor is not permitted to invoke this op.
    #[error("actor '{actor_id}' not permitted for op '{op_id}' (allowed roles: {allowed:?})")]
    ActorPolicyDenied {
        /// Actor id that attempted the op.
        actor_id: String,
        /// Operation id whose actor policy denied the call.
        op_id: String,
        /// Roles permitted by the op's `actor_policy`.
        allowed: Vec<String>,
    },

    /// Persisting the signed witness to the oplog failed (`substrate`
    /// feature). The render of the NCL projection may already have
    /// succeeded, so the caller must treat state and proof as divergent
    /// and retry or reconcile.
    #[error("witness persist for '{op_id}': {reason}")]
    WitnessPersist {
        /// Operation id whose witness failed to persist.
        op_id: String,
        /// Human-readable reason.
        reason: String,
    },
}

impl OpError {
    /// Build an [`OpError::OpBody`] from a generic error message.
    pub fn body(op_id: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::OpBody {
            op_id: op_id.into(),
            reason: reason.into(),
        }
    }
}
