//! HandlerResponse — orthogonal decision + side effects.
//!
//! See spec docs/superpowers/specs/2026-04-30-composable-rules-and-capability-layer.md
//! § "HandlerResponse — orthogonal decision + side effects".
//!
//! `RuleResult` (the predecessor) collapsed two orthogonal axes — what
//! happens to the proposed event, and what additional events fire — into a
//! single enum, which forced compound variants like `ModifyAndEmit`,
//! `DenyAndEmit`, etc. as new combinations were needed. `HandlerResponse`
//! separates those axes:
//!
//! - `decision: Decision` — the verdict on the proposed event.
//! - `also: Vec<GameEvent>` — side effects queued for the React phase
//!   (or the immediate cascade if the handler is already running in React).
//!   Independent of `decision` — denying an event can still emit side
//!   effects, e.g. an audit log entry or an "out of resources" notification.
//!
//! Handlers that don't need side effects keep returning a single
//! `Decision::*` and the `From<Decision>` impl converts it to a
//! `HandlerResponse` with an empty `also` list.

use crate::events::{GameEvent, Suspension};
use crate::phases::PhaseNext;

/// A handler's full response: a verdict on the proposed event plus any
/// side-effect events to fire alongside it.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct HandlerResponse {
    /// The verdict on the proposed event.
    pub decision: Decision,
    /// Events emitted alongside the decision. Queued for the React phase
    /// (or immediate cascade if already in React). Independent of `decision`.
    pub also: Vec<GameEvent>,
}

/// What happens to the proposed event. Orthogonal to the side-effect
/// `also` list on [`HandlerResponse`].
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
// handler return type: Modify/Wizard are uncommon; Allow/Deny dominate
#[allow(clippy::large_enum_variant)]
pub enum Decision {
    /// The event proceeds unchanged.
    Allow,
    /// Game-logic denial — the action is invalid by the rules.
    Deny(String),
    /// Replace the proposed event with this one (still subject to further
    /// Modify handlers in the chain).
    Modify(GameEvent),
    /// Pause the pipeline and present a choice to a decider.
    /// **Legacy path** — deleted at SD1-F once all consumers
    /// migrate to `Wizard`. Coexists during SD1 migration.
    Suspend(Suspension),
    /// SD1: pause the pipeline and open a wizard. The engine
    /// stores the `StagedDecision` in `sus_queue.pending_decision`
    /// and emits a `GameEvent::WizardOpened` audit event.
    /// Subsequent `submit_choice` calls drive the wizard to
    /// completion.
    Wizard(crate::staged_decision::StagedDecision),
    /// Handler implementation bug. Logged server-side; treated as Deny to
    /// halt the pipeline without surfacing internals to clients.
    Error(String),
    /// Returned by phase_end_deciders: the next phase transition to take.
    DecidePhase(PhaseNext),
}

impl From<Decision> for HandlerResponse {
    fn from(d: Decision) -> Self {
        Self { decision: d, also: Vec::new() }
    }
}

impl HandlerResponse {
    /// Construct an Allow response that emits side-effect events for the
    /// React phase. This is the migration target for the old
    /// `RuleResult::Emit(events)` shape: when a handler wants to allow the
    /// proposed event AND queue additional events, it returns
    /// `HandlerResponse::allow_with(events)`.
    pub fn allow_with(events: Vec<GameEvent>) -> Self {
        Self { decision: Decision::Allow, also: events }
    }

    /// Construct an Allow response with no side effects. Equivalent to
    /// `Decision::Allow.into()` but reads as a clear method when the call
    /// site already has a `HandlerResponse` context.
    pub fn allow() -> Self {
        Self { decision: Decision::Allow, also: Vec::new() }
    }

    /// Construct a Deny response with no side effects.
    pub fn deny(msg: impl Into<String>) -> Self {
        Self { decision: Decision::Deny(msg.into()), also: Vec::new() }
    }

    /// Construct a Modify response with no side effects.
    pub fn modify(event: GameEvent) -> Self {
        Self { decision: Decision::Modify(event), also: Vec::new() }
    }

    /// Construct a Suspend response with no side effects.
    pub fn suspend(s: Suspension) -> Self {
        Self { decision: Decision::Suspend(s), also: Vec::new() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::events::GameEvent;
    use crate::types::SeatId;

    #[test]
    fn decision_converts_to_handler_response_with_empty_also() {
        let r: HandlerResponse = Decision::Allow.into();
        assert!(r.also.is_empty());
        assert!(matches!(r.decision, Decision::Allow));
    }

    #[test]
    fn handler_response_carries_decision_and_also() {
        let dummy: GameEvent = GameEvent::TurnEnded { seat: SeatId::from_raw(0) };
        let r = HandlerResponse {
            decision: Decision::Deny("nope".into()),
            also: vec![dummy],
        };
        assert_eq!(r.also.len(), 1);
        match r.decision {
            Decision::Deny(m) => assert_eq!(m, "nope"),
            _ => panic!("expected Deny"),
        }
    }

    #[test]
    fn deny_with_side_effects_round_trip_in_struct() {
        let r1 = HandlerResponse::allow_with(vec![]);
        let r2 = r1.clone();
        assert_eq!(r2.also.len(), 0);
        assert!(matches!(r2.decision, Decision::Allow));
    }
}
