//! SD1-A: staged decisions unified primitive.
//!
//! Spec: `docs/superpowers/specs/2026-05-04-staged-decisions-unified.md`.
//!
//! `StagedDecision` is the single primitive that subsumes both
//! `Suspension::{Single,Staged}` (the cast-flow / wizard pattern) and
//! `CharacterCreationSchema` (the out-of-pipeline form). The data
//! types here are pure shape — they describe what's pending and what
//! has been answered. The stage-narrowing logic lives plugin-side
//! behind `ComposerAction` / `ComposerResponse` callbacks; the engine
//! holds no plugin-defined narrowing state.
//!
//! ## Three orthogonal axes
//!
//! Each [`DecisionStage`] composes:
//!
//! - [`InputShape`] — what value the answer carries (text, number,
//!   pick-one, pick-many). No constraints beyond bounds; domain
//!   constraints (e.g. "abilities must permute the standard array")
//!   live in the plugin's `Advance` / `Resolve` callbacks.
//! - [`ResolutionStrategy`] — where the value comes from (decider
//!   choice, dice roll, ...). Adding a new resolution path is
//!   additive on this axis without touching `InputShape`.
//! - [`CompletionRule`] + [`PresentationMode`] — how a stage relates
//!   to its peers in the current group. `All` / `Any` / `AtLeastN`
//!   composition primitives cover AND / XOR / N-of-K.
//!
//! ## Parallel-run discipline (SD1-A landing)
//!
//! These types ship alongside the existing `Suspension` /
//! `CharacterCreationSchema` types. SD1-D migrates consumers one at
//! a time; SD1-F deletes the old types after every migration is
//! green. This phase is purely additive — workspace stays green.

use crate::events::Decider;
use crate::plugin_id::CanonicalId;
use crate::types::{DiceSpec, EntityId};

// ---------------------------------------------------------------------------
// Identifiers
// ---------------------------------------------------------------------------

/// Plugin-defined wizard discriminator. Routes the engine's
/// dispatch back to the right plugin method when stages resolve.
///
/// Format: `{canonical_id}:{local_name}`, e.g.
/// `github.com/williamweatherholtz/maat-dnd5e:magic_missile`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WizardKind {
    pub plugin: CanonicalId,
    pub local_name: String,
}

impl WizardKind {
    pub fn new(plugin: CanonicalId, local_name: impl Into<String>) -> Self {
        Self { plugin, local_name: local_name.into() }
    }

    /// `"{canonical_id}:{local_name}"` — the wire / log
    /// representation. Round-trips via `parse_str` are not
    /// implemented in v1; consumers serialize through serde.
    pub fn as_string(&self) -> String {
        format!("{}:{}", self.plugin, self.local_name)
    }
}

/// Stable identifier within a wizard so plugins can read history
/// entries by stage ergonomically. Field is private — consumers
/// build via `StageId::new(...)` / `From<&str>` / `From<String>`
/// and read via `as_str()`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StageId(String);

impl StageId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<&str> for StageId {
    fn from(s: &str) -> Self { Self(s.to_string()) }
}

impl From<String> for StageId {
    fn from(s: String) -> Self { Self(s) }
}

/// Auto-incrementing identifier for a `(open|resolved)` group within
/// one `StagedDecision`'s timeline. Engine assigns; plugin reads.
/// Field is private — consumers build via `GroupId::new(n)` and
/// read via `value()`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GroupId(u32);

impl GroupId {
    pub fn new(n: u32) -> Self { Self(n) }
    pub fn value(self) -> u32 { self.0 }
}

// ---------------------------------------------------------------------------
// The decision itself
// ---------------------------------------------------------------------------

/// One in-flight staged decision. Replaces `Suspension::Staged` for
/// multi-stage flows and `Suspension::Single` for one-shot picks
/// (one-shot = a wizard with one `OpenGroup` containing one stage).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct StagedDecision {
    /// Human-readable label for clients / log entries.
    pub title: String,
    /// Routes engine callbacks to the providing plugin's wizard
    /// method (`#[wizard(kind = "...")]`).
    pub kind: WizardKind,
    /// Default decider used by stages that don't override.
    pub default_decider: Decider,
    /// Resolved groups in the order the engine presented them.
    /// Each entry holds the group's stages plus the
    /// [`ChoiceRecord`]s that closed it.
    pub history: Vec<ResolvedGroup>,
    /// The currently-open group, if any. `None` means the wizard is
    /// logically complete and the engine should invoke the plugin's
    /// `Resolve` callback.
    pub current: Option<OpenGroup>,
}

impl StagedDecision {
    pub fn new(title: impl Into<String>, kind: WizardKind, default_decider: Decider) -> Self {
        Self {
            title: title.into(),
            kind,
            default_decider,
            history: Vec::new(),
            current: None,
        }
    }

    /// `true` once every group the plugin produced has resolved and
    /// `current` is `None` — the engine signals `Resolve` next.
    pub fn is_complete(&self) -> bool {
        self.current.is_none()
    }
}

/// A group currently waiting on input. Cleared when its
/// [`CompletionRule`] is satisfied; the resulting `ResolvedGroup`
/// is appended to `history` and the engine asks the plugin for the
/// next group via `ComposerAction::Advance`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct OpenGroup {
    pub group_id:     GroupId,
    pub stages:       Vec<DecisionStage>,
    pub completion:   CompletionRule,
    pub presentation: PresentationMode,
    /// Choices already submitted for this group.
    pub partial: Vec<ChoiceRecord>,
}

impl OpenGroup {
    pub fn new(
        group_id: GroupId,
        stages: Vec<DecisionStage>,
        completion: CompletionRule,
        presentation: PresentationMode,
    ) -> Self {
        Self { group_id, stages, completion, presentation, partial: Vec::new() }
    }

    /// `true` once `partial` satisfies `completion`.
    pub fn is_complete(&self) -> bool {
        match self.completion {
            CompletionRule::All => self.partial.len() == self.stages.len(),
            CompletionRule::Any => !self.partial.is_empty(),
            CompletionRule::AtLeastN { n } => self.partial.len() >= n as usize,
        }
    }
}

/// A group whose `CompletionRule` has been satisfied. Lives in
/// `StagedDecision::history` for the wizard's lifetime; the
/// plugin's `Advance` / `Resolve` callbacks read history slices.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedGroup {
    pub group_id:     GroupId,
    pub stages:       Vec<DecisionStage>,
    pub completion:   CompletionRule,
    pub presentation: PresentationMode,
    pub closed_by:    Vec<ChoiceRecord>,
}

impl From<OpenGroup> for ResolvedGroup {
    fn from(g: OpenGroup) -> Self {
        Self {
            group_id: g.group_id,
            stages: g.stages,
            completion: g.completion,
            presentation: g.presentation,
            closed_by: g.partial,
        }
    }
}

/// One stage within a group — what to ask, who to ask, where the
/// answer comes from.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct DecisionStage {
    pub stage_id:   StageId,
    pub prompt:     String,
    /// Per-stage decider. `None` falls back to the wizard's
    /// `default_decider`. Per-stage override exists for flows like
    /// contested rolls where two stages in one `All` group have
    /// different deciders.
    pub decider:    Option<Decider>,
    pub input:      InputShape,
    pub resolution: ResolutionStrategy,
}

impl DecisionStage {
    /// Decider-resolved stage with no decider override.
    pub fn ask(stage_id: impl Into<StageId>, prompt: impl Into<String>, input: InputShape) -> Self {
        Self {
            stage_id: stage_id.into(),
            prompt:   prompt.into(),
            decider:  None,
            input,
            resolution: ResolutionStrategy::Decider,
        }
    }

    pub fn with_decider(mut self, decider: Decider) -> Self {
        self.decider = Some(decider);
        self
    }

    pub fn with_resolution(mut self, resolution: ResolutionStrategy) -> Self {
        self.resolution = resolution;
        self
    }

    /// Convenience: dice-resolved stage. Engine rolls eagerly when
    /// the group opens and emits a `DiceRolled` event before any
    /// decider-resolved stages in the same group are presented.
    pub fn dice(stage_id: impl Into<StageId>, prompt: impl Into<String>, spec: DiceSpec) -> Self {
        Self {
            stage_id: stage_id.into(),
            prompt:   prompt.into(),
            decider:  None,
            // `Number` shape captures the rolled value (sum of dice
            // + modifier). Rolled stages with vector outputs would
            // use a richer shape; deferred until a real consumer.
            input:      InputShape::Number { min: i64::MIN, max: i64::MAX },
            resolution: ResolutionStrategy::Dice { spec },
        }
    }
}

// ---------------------------------------------------------------------------
// Input / resolution / composition axes
// ---------------------------------------------------------------------------

/// What value the answer carries. Pure shape; bounds inline.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum InputShape {
    Text     { placeholder: String, max_len: u32 },
    Number   { min: i64, max: i64 },
    /// One-of-N pick. Subsumes the legacy `SuspensionKind::Choose`
    /// and the suspension's `options: Vec<SuspensionOption>` shape.
    /// "First to claim wins" (legacy `SuspensionKind::Race`)
    ///   composes via `CompletionRule::Any` over a parallel group of
    ///   `PickOne` stages, one per eligible decider.
    PickOne  { options: Vec<DecisionOption> },
    PickMany { options: Vec<DecisionOption>, min: u32, max: u32 },
}

/// Where the value comes from. Decider-driven prompts the
/// player/GM; mechanically-resolved stages have the engine fill the
/// value from a host source. `Decider` is still recorded on the
/// stage for non-`Decider` resolutions so a rule's filter can veto
/// the resolution (e.g., advantage re-roll on a Dice stage).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum ResolutionStrategy {
    /// The decider picks. Default for `PickOne` / `PickMany` /
    /// `Text` / most `Number` stages.
    Decider,
    /// The host rolls. The decider witnesses; a rule may
    /// `Decision::Modify` the rolled value before it commits.
    /// Subsumes the legacy `SuspensionKind::DiceInput` (manual
    /// dice entry) and the auto-rolled side of cast-flow dice.
    Dice { spec: DiceSpec },
}

/// How a group closes and advances. Three primitives cover
/// AND / XOR / N-of-K composition.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionRule {
    /// All stages must resolve before the group closes. **AND.**
    All,
    /// Any one stage closes the whole group; remaining stages are
    /// abandoned. **XOR / first-to-claim-wins.**
    Any,
    /// Exactly `n` stages must resolve. The decider chooses which
    /// `n` to fill (e.g., "pick 2 of these 4 spells"). **N-of-K.**
    AtLeastN { n: u32 },
}

/// UI hint — independent of `CompletionRule`. A `Sequential` `All`
/// group walks stages one at a time; a `Parallel` `All` group
/// presents them all at once.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PresentationMode {
    Sequential,
    Parallel,
}

// ---------------------------------------------------------------------------
// Choices
// ---------------------------------------------------------------------------

/// One option offered by a `PickOne` / `PickMany` stage. The
/// `payload` carries the typed reference the wizard's `Resolve`
/// callback needs to translate the choice into pipeline effects
/// (e.g., the EntityId of a chosen target).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct DecisionOption {
    pub label:   String,
    pub payload: ChoicePayload,
}

impl DecisionOption {
    pub fn label_only(label: impl Into<String>) -> Self {
        Self { label: label.into(), payload: ChoicePayload::None }
    }

    pub fn entity(label: impl Into<String>, id: EntityId) -> Self {
        Self { label: label.into(), payload: ChoicePayload::Entity(id) }
    }

    pub fn string(label: impl Into<String>, value: impl Into<String>) -> Self {
        Self { label: label.into(), payload: ChoicePayload::String(value.into()) }
    }

    pub fn number(label: impl Into<String>, value: i64) -> Self {
        Self { label: label.into(), payload: ChoicePayload::Number(value) }
    }
}

/// Typed payload riding on a `DecisionOption`. Future variants can
/// be added additively (Tag, Vec<EntityId>, etc.) when consumers
/// surface them.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChoicePayload {
    None,
    Entity(EntityId),
    String(String),
    Number(i64),
}

/// One entry in a `(open|resolved) group`'s choices: the stage that
/// got answered + the typed value submitted.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ChoiceRecord {
    pub stage_id: StageId,
    pub value:    ChoiceValue,
}

/// Typed answer matched to its stage's `InputShape`.
///
/// Variants pair with shapes:
/// - `Text(_)` ⟷ `InputShape::Text`
/// - `Number(_)` ⟷ `InputShape::Number`
/// - `Picked(_)` ⟷ `InputShape::PickOne` (index into options)
/// - `PickedMany(_)` ⟷ `InputShape::PickMany` (sorted indices)
/// - `Rolled { .. }` ⟷ `ResolutionStrategy::Dice` (carries the
///   full roll shape so downstream handlers see the same audit
///   trail they'd see if the roll had been emitted as a regular
///   `DiceRolled` event)
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChoiceValue {
    Text(String),
    Number(i64),
    Picked(u32),
    PickedMany(Vec<u32>),
    Rolled { results: Vec<u32>, total: i64 },
}

impl ChoiceValue {
    /// Read the text value for a `Text` stage.
    pub fn as_text(&self) -> Option<&str> {
        if let ChoiceValue::Text(s) = self { Some(s) } else { None }
    }

    /// Read the number value for a `Number` stage.
    pub fn as_number(&self) -> Option<i64> {
        if let ChoiceValue::Number(n) = self { Some(*n) } else { None }
    }

    /// Read the picked-option index for a `PickOne` stage.
    pub fn as_picked(&self) -> Option<u32> {
        if let ChoiceValue::Picked(i) = self { Some(*i) } else { None }
    }

    /// Read the picked-option indices for a `PickMany` stage.
    pub fn as_picked_many(&self) -> Option<&[u32]> {
        if let ChoiceValue::PickedMany(v) = self { Some(v) } else { None }
    }

    /// Read the dice total for a `Dice`-resolved stage.
    pub fn as_dice_total(&self) -> Option<i64> {
        if let ChoiceValue::Rolled { total, .. } = self { Some(*total) } else { None }
    }
}

// ---------------------------------------------------------------------------
// Static schema declarations — for load-time validation (WZ1)
// ---------------------------------------------------------------------------

/// A static description of one group in a wizard's expected flow.
/// Plugins return these from `declared_wizard_schemas()` so the
/// engine can validate structural invariants at load time (before
/// any player ever triggers the wizard).
///
/// `GroupDecl` mirrors `OpenGroup` but omits the runtime state
/// (`group_id`, `partial`) and carries only the structural shape.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct GroupDecl {
    /// Stages that may appear in this group.
    pub stages: Vec<DecisionStage>,
    /// How the group closes (All / Any / AtLeastN).
    pub completion: CompletionRule,
    /// How the UI presents the stages (Sequential / Parallel).
    pub presentation: PresentationMode,
}

impl GroupDecl {
    pub fn new(
        stages: Vec<DecisionStage>,
        completion: CompletionRule,
        presentation: PresentationMode,
    ) -> Self {
        Self { stages, completion, presentation }
    }
}

/// A static schema for one wizard declared by a plugin.
/// Returned by `ModelPlugin::declared_wizard_schemas()`.
///
/// `groups` is the *complete* set of groups the wizard may open
/// across its lifetime (across all branches). Branch-dependent
/// groups (e.g., the spells group that only opens for casters)
/// should still be listed here so the validator can check their
/// shape. The validator does not enforce which branches are
/// reachable — only that each group's structural invariants hold.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct StagedDecisionDecl {
    /// The wizard this schema describes.
    pub kind: WizardKind,
    /// All groups (across all branches) this wizard may open.
    pub groups: Vec<GroupDecl>,
}

impl StagedDecisionDecl {
    pub fn new(kind: WizardKind, groups: Vec<GroupDecl>) -> Self {
        Self { kind, groups }
    }
}

