//! [`Emit`] — the declarative result of a step or compensation handler:
//! "dispatch these actions (and optionally bound the wait with a
//! timeout)". What event completes the step, and how its payload maps to
//! the step's typed output, is declared on the **topology** via
//! [`task(...).on_response(...)`](crate::TaskBuilder::on_response) — not
//! inside the handler. That keeps handlers free of closures the durable
//! runner would otherwise have to recover by re-running them (the old
//! handler-purity contract, deleted in phase 2).

use std::time::Duration;

use serde::{Deserialize, Serialize};

use crate::event_static::EventTypeStatic;

// =====================================================================
// StepError — surfaced via `Cause` to compensation handlers and persisted
// in the envelope.
// =====================================================================

#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
pub enum StepError {
    // `u64`, not `u128`: this error is serialised to JSON on the saga
    // event stream by the durable runner, and serde_json rejects u128.
    // Milliseconds in u64 covers ~584 million years of timeout budget.
    #[error("step `{step}` timed out after {timeout_ms}ms")]
    Timeout { step: String, timeout_ms: u64 },
    #[error("step `{step}` dispatcher dead-lettered: {reason}")]
    DeadLetter { step: String, reason: String },
    #[error("step `{step}` cancelled by signal: {reason}")]
    Cancelled { step: String, reason: String },
    #[error("step `{step}` failed: {reason}")]
    Failed { step: String, reason: String },
}

/// Why building an [`Emit`] (or serialising a value-step output) failed —
/// surfaced by the runner as `StepError::Failed` so compensation cascades
/// cleanly instead of the library panicking on bad input.
#[derive(Debug, thiserror::Error)]
pub enum StepBuildError {
    #[error("could not serialise step emit payload: {0}")]
    SerialiseEmit(#[source] serde_json::Error),
    #[error("could not serialise step output: {0}")]
    SerialiseOutput(#[source] serde_json::Error),
}

/// A single dispatched action: the JSON-serialised payload plus its declared
/// `event_type` string so the runner / outbox can route by type without
/// re-discovering it from the JSON.
#[derive(Debug, Clone)]
pub struct DispatchedAction {
    pub event_type: Option<&'static str>,
    pub payload: serde_json::Value,
}

/// Errors applying a registered response map to a delivered payload.
#[derive(Debug, thiserror::Error)]
pub enum AwaitMapError {
    #[error("could not deserialise awaited event into the type declared by the step: {0}")]
    DeserialiseEvent(#[source] serde_json::Error),
    #[error("could not serialise step output: {0}")]
    SerialiseOutput(#[source] serde_json::Error),
}

// =====================================================================
// Emit — what an emit-style handler returns.
// =====================================================================

/// What an emit-style step (or compensation) handler returns: zero or
/// more typed events to dispatch, optionally with a per-activation
/// timeout for the await declared on the topology.
///
/// - [`Emit::event`] / [`Emit::all`] — dispatch typed request events.
/// - [`Emit::none`] — dispatch nothing (pure-await steps, e.g. a
///   `routes_to_step` signal target, or a no-op compensation).
/// - [`Emit::timeout`] — bound the await with a deadline computed *at
///   activation time* (e.g. from a `Dep<T>`); overrides the task-level
///   [`timeout`](crate::TaskBuilder::timeout) and the saga's
///   `default_step_timeout`.
///
/// Serialisation failures are stashed (never panic) and surfaced by the
/// runner as `StepError::Failed`, firing the compensation cascade.
///
/// Deliberately does **not** implement `Serialize` — that keeps the
/// emit-handler and value-handler impls of `StepHandler` disjoint, so
/// `task(handler)` infers the right kind from the return type alone.
pub struct Emit {
    pub(crate) dispatch: Vec<DispatchedAction>,
    pub(crate) timeout: Option<Duration>,
    pub(crate) build_error: Option<StepBuildError>,
}

impl Emit {
    /// Dispatch one typed event.
    pub fn event<A: EventTypeStatic>(action: A) -> Self {
        match serde_json::to_value(&action) {
            Ok(payload) => Self {
                dispatch: vec![DispatchedAction {
                    event_type: Some(<A as EventTypeStatic>::EVENT_TYPE),
                    payload,
                }],
                timeout: None,
                build_error: None,
            },
            Err(e) => Self {
                dispatch: Vec::new(),
                timeout: None,
                build_error: Some(StepBuildError::SerialiseEmit(e)),
            },
        }
    }

    /// Dispatch multiple events in one batch. If any serialisation fails,
    /// the whole result carries the error (no partial dispatch).
    pub fn all<A, I>(actions: I) -> Self
    where
        A: EventTypeStatic,
        I: IntoIterator<Item = A>,
    {
        let mut dispatch = Vec::new();
        for a in actions {
            match serde_json::to_value(&a) {
                Ok(payload) => dispatch.push(DispatchedAction {
                    event_type: Some(<A as EventTypeStatic>::EVENT_TYPE),
                    payload,
                }),
                Err(e) => {
                    return Self {
                        dispatch: Vec::new(),
                        timeout: None,
                        build_error: Some(StepBuildError::SerialiseEmit(e)),
                    };
                }
            }
        }
        Self {
            dispatch,
            timeout: None,
            build_error: None,
        }
    }

    /// Dispatch nothing. A step with an `.on_response(...)` declaration
    /// still suspends and awaits (the event arrives from outside — a
    /// signal or an external worker); without one, the step completes
    /// immediately with `Null` output.
    pub fn none() -> Self {
        Self {
            dispatch: Vec::new(),
            timeout: None,
            build_error: None,
        }
    }

    /// Bound this activation's await with a deadline computed at runtime
    /// (e.g. from a `Dep<T>`). Overrides the task-level `.timeout(...)`
    /// and the saga's `default_step_timeout` for this activation.
    pub fn timeout(mut self, d: Duration) -> Self {
        self.timeout = Some(d);
        self
    }
}
