//! CR1-A — Engine-composed entity creation pipeline (static specs).
//!
//! Plugin authors declare *what* an entity needs to be born via
//! [`CreationSpec`]; the engine composes the wizard UI from it. Rules
//! governing *who can create what when at what cost* live in handlers
//! on the [`crate::events::GameEvent::EntityCreationRequested`] event.
//!
//! See `docs/superpowers/specs/2026-05-07-cr1-a-engine-composed-creation-pipeline.md`
//! for the full design.
//!
//! # Quick example
//!
//! ```ignore
//! impl Hero {
//!     pub fn creation_spec() -> CreationSpec {
//!         CreationSpec::builder()
//!             .group("identity", PresentationMode::Parallel)
//!                 .text("name").label("Hero name").max_len(40).required()
//!                 .pick_one("archetype").label("Archetype").options(vec![
//!                     DecisionOption::label_only("Fighter"),
//!                     DecisionOption::label_only("Rogue"),
//!                 ])
//!             .build()
//!     }
//!
//!     pub fn compose(h: &CreationHistory, ctx: &mut ComposeCtx) -> Vec<GameEvent> {
//!         /* ... game-specific event construction ... */
//!         vec![]
//!     }
//! }
//! ```
//!
//! CR1-A: static fields and static option lists only. CR1-B will add dynamic
//! `options_from(|h| ...)` / `count_from(|h| ...)` / `group_when(|h| ...)`
//! closures. CR1-C wires level-up and adjudication-routing on top.

use std::collections::HashMap;

use crate::staged_decision::{DecisionOption, PresentationMode, StageId};
use crate::types::SeatId;

// ---------------------------------------------------------------------------
// Spec types
// ---------------------------------------------------------------------------

/// Plugin author's declarative description of how an entity is born.
/// Walked by the engine's wizard composer to produce stages.
#[derive(Debug, Clone)]
pub struct CreationSpec {
    pub groups: Vec<CreationGroup>,
}

#[derive(Debug, Clone)]
pub struct CreationGroup {
    pub local_id: String,
    pub presentation: PresentationMode,
    pub fields: Vec<CreationField>,
    /// CR1-B: optional predicate evaluated when the engine is about
    /// to open this group. When `Some(f)` and `f(history)` returns
    /// false, the engine skips the group entirely (advances past it
    /// without opening). dnd5e uses this to omit the spells group
    /// for non-casters. None (the default) means the group always
    /// opens — same behavior as CR1-A.
    pub condition: Option<fn(&CreationHistory) -> bool>,
    /// WI1-FU5: completion rule override. `None` ⇒
    /// [`crate::staged_decision::CompletionRule::All`] (the existing
    /// default; the group only closes when every stage resolves).
    /// `Some(Any)` enables the race pattern (Counterspell): the first
    /// resolved stage closes the group and abandons the rest.
    pub completion: Option<crate::staged_decision::CompletionRule>,
    /// WI1-FU5: dynamic stage construction from the trigger event +
    /// context. When `Some`, the engine calls this at OpenGroup time
    /// and uses the returned stages **in place of** `fields`-driven
    /// construction. Validated at registration: a group cannot
    /// declare both non-empty `fields` AND `stages_from_trigger`.
    /// Used by Counterspell: produces one PickOne stage per eligible
    /// reactor identified at trigger time.
    pub stages_from_trigger: Option<StagesFromTriggerFn>,
    /// WI1-FU5: paired per-stage decider list. Required when
    /// `stages_from_trigger` is set; must return a `Vec<Decider>` of
    /// the same length. The engine validates the pairing at OpenGroup
    /// time and raises Suspension on mismatch so plugin bugs surface
    /// visibly. Used by Counterspell: each stage routes to its
    /// reactor's owning seat via `Decider::EntityOwner`.
    pub decider_from_trigger: Option<DeciderFromTriggerFn>,
}

/// WI1-FU5 — dynamic stage constructor. See `CreationGroup::stages_from_trigger`.
pub type StagesFromTriggerFn = fn(
    &crate::events::GameEvent,
    &dyn crate::plugin_slices::GameContext,
) -> Vec<crate::staged_decision::DecisionStage>;

/// WI1-FU5 — paired per-stage decider builder. See `CreationGroup::decider_from_trigger`.
pub type DeciderFromTriggerFn = fn(
    &crate::events::GameEvent,
    &dyn crate::plugin_slices::GameContext,
) -> Vec<crate::events::Decider>;

/// WI1-FU7 — ctx-aware dynamic option builder. See `CreationField::PickOne / PickMany.options_from_ctx`.
pub type OptionsFromCtxFn =
    fn(&CreationHistory, &dyn crate::plugin_slices::GameContext) -> Vec<DecisionOption>;

/// WI1-FU8 — trigger-aware dynamic option builder. See `CreationField::PickOne / PickMany.options_from_trigger`.
pub type OptionsFromTriggerFn = fn(
    &CreationHistory,
    &dyn crate::plugin_slices::GameContext,
    &crate::events::GameEvent,
) -> Vec<DecisionOption>;

/// WI1-FU7 — ctx-aware dynamic count builder. See `CreationField::PickMany.count_from_ctx`.
pub type CountFromCtxFn =
    fn(&CreationHistory, &dyn crate::plugin_slices::GameContext) -> (u32, u32);

/// CR1-B / 0400 — history-only dynamic count builder. Used by classes whose
/// counts vary with progression but don't need full ctx.
pub type CountFromFn = fn(&CreationHistory) -> (u32, u32);

#[derive(Debug, Clone)]
pub enum CreationField {
    Text {
        id: StageId,
        label: String,
        max_len: u32,
        required: bool,
        default: Option<String>,
    },
    Number {
        id: StageId,
        label: String,
        min: i64,
        max: i64,
        default: Option<i64>,
    },
    PickOne {
        id: StageId,
        label: String,
        /// Static options. Used iff both dynamic closures are `None`.
        options: Vec<DecisionOption>,
        /// CR1-B: dynamic options. The engine calls this with the
        /// current `CreationHistory` each time the stage opens.
        /// When `Some(_)`, the static `options` field is ignored.
        options_from: Option<fn(&CreationHistory) -> Vec<DecisionOption>>,
        /// WI1-FU7: ctx-aware dynamic options. Preferred over
        /// `options_from` when set — receives both history and the
        /// live `GameContext` (store / queries / config) so options
        /// can narrow by per-entity state (e.g. Life game's career
        /// wizard reads the activator's `life:degree` pool).
        options_from_ctx: Option<OptionsFromCtxFn>,
        /// WI1-FU8: trigger-aware dynamic options. Preferred over
        /// `options_from_ctx` when set — receives the trigger event
        /// (whatever event the plugin's Filter dispatched the wizard
        /// from) so options can narrow by per-entity state read
        /// through the trigger's entity id. Used by Life's `career`,
        /// `buy_stock`, `board_fork` wizards.
        options_from_trigger: Option<OptionsFromTriggerFn>,
        default: Option<String>,
    },
    PickMany {
        id: StageId,
        label: String,
        /// Static options. Used iff both dynamic closures are `None`.
        options: Vec<DecisionOption>,
        /// CR1-B: dynamic options.
        options_from: Option<fn(&CreationHistory) -> Vec<DecisionOption>>,
        /// WI1-FU7: ctx-aware dynamic options. Preferred over
        /// `options_from` when set.
        options_from_ctx: Option<OptionsFromCtxFn>,
        /// WI1-FU8: trigger-aware dynamic options.
        options_from_trigger: Option<OptionsFromTriggerFn>,
        /// Static `(min, max)`. Used iff both dynamic closures are `None`.
        min: u32,
        max: u32,
        /// CR1-B: dynamic `(min, max)`. dnd5e uses this for caster
        /// classes whose spell-known counts vary with class progression.
        count_from: Option<CountFromFn>,
        /// WI1-FU7: ctx-aware dynamic count. Preferred over
        /// `count_from` when set.
        count_from_ctx: Option<CountFromCtxFn>,
        defaults: Vec<String>,
    },
}

impl CreationField {
    pub fn id(&self) -> &StageId {
        match self {
            CreationField::Text { id, .. } => id,
            CreationField::Number { id, .. } => id,
            CreationField::PickOne { id, .. } => id,
            CreationField::PickMany { id, .. } => id,
        }
    }
}

// ---------------------------------------------------------------------------
// Prefill — label-keyed values supplied by callers up-front
// ---------------------------------------------------------------------------

/// CL1-B — outcome classification returned by
/// `GameSession::creation_dry_run`. Used by the client affordance to
/// know whether clicking a "Create" button will go cleanly through the
/// wizard flow (`Allow`) or trigger an adjudication suspension first
/// (`AdjudicateRequired`).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreationStatus {
    /// Filters Allow; clicking the button opens the wizard immediately.
    Allow,
    /// Filters Suspend (Narrator); the creation request will route
    /// to the GM's suspension queue. The client should surface this
    /// to the user (e.g. "⚖ adjudicates" badge on the button).
    AdjudicateRequired,
}

/// A value supplied as up-front prefill on
/// [`crate::events::GameEvent::EntityCreationRequested`]. Distinct from
/// [`crate::staged_decision::ChoiceValue`] (which uses option indices for
/// over-the-wire compactness): prefill uses labels because callers
/// constructing prefill don't know option indices, and labels remain stable
/// across plugin data refreshes whereas indices do not.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrefillValue {
    Text(String),
    Number(i64),
    Pick(String),
    PickMany(Vec<String>),
}

// ---------------------------------------------------------------------------
// History — engine-built read view passed to `compose(history, ctx)`
// ---------------------------------------------------------------------------

/// Engine-built read view of the wizard history, keyed by stage id with
/// labels resolved (option indices already cross-referenced against the
/// spec). Plugins read this inside `compose()`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CreationHistory {
    by_stage: HashMap<StageId, PrefillValue>,
}

impl CreationHistory {
    /// Build an empty history (used by the engine before populating).
    pub fn new() -> Self {
        Self::default()
    }

    /// Engine-side: insert a resolved value for a stage. Plugins should
    /// not call this — they receive a read-only `&CreationHistory`.
    pub fn insert(&mut self, id: StageId, value: PrefillValue) {
        self.by_stage.insert(id, value);
    }

    pub fn text(&self, id: &str) -> Option<&str> {
        match self.by_stage.get(&StageId::new(id))? {
            PrefillValue::Text(s) => Some(s.as_str()),
            _ => None,
        }
    }

    pub fn number(&self, id: &str) -> Option<i64> {
        match self.by_stage.get(&StageId::new(id))? {
            PrefillValue::Number(n) => Some(*n),
            _ => None,
        }
    }

    pub fn pick(&self, id: &str) -> Option<&str> {
        match self.by_stage.get(&StageId::new(id))? {
            PrefillValue::Pick(s) => Some(s.as_str()),
            _ => None,
        }
    }

    pub fn pick_many(&self, id: &str) -> Option<&[String]> {
        match self.by_stage.get(&StageId::new(id))? {
            PrefillValue::PickMany(v) => Some(v.as_slice()),
            _ => None,
        }
    }

    pub fn len(&self) -> usize {
        self.by_stage.len()
    }

    pub fn is_empty(&self) -> bool {
        self.by_stage.is_empty()
    }
}

// ---------------------------------------------------------------------------
// ComposeCtx — engine-owned context passed to plugin's compose()
// ---------------------------------------------------------------------------

/// Context passed to `compose(history, ctx)`. Engine-owned. Plugins use
/// `(ctx.alloc_id)()` to allocate fresh entity IDs without holding
/// `&mut GameSession` (the alloc closure encapsulates the session
/// reference at the call site).
pub struct ComposeCtx<'a> {
    /// Allocate a fresh entity id. Each call returns a new id.
    pub alloc_id: &'a mut dyn FnMut() -> crate::types::EntityId,
    /// The seat that initiated creation.
    pub by_seat: SeatId,
}

// ---------------------------------------------------------------------------
// Builder API
// ---------------------------------------------------------------------------

impl CreationSpec {
    pub fn builder() -> CreationSpecBuilder {
        CreationSpecBuilder { groups: Vec::new() }
    }
}

pub struct CreationSpecBuilder {
    groups: Vec<CreationGroup>,
}

impl CreationSpecBuilder {
    /// Open a new group. Subsequent field calls (`.text(...)`, `.pick_one(...)`,
    /// etc.) append to this group until `.group(...)` opens another or
    /// `.build()` finishes.
    pub fn group(
        self,
        local_id: impl Into<String>,
        presentation: PresentationMode,
    ) -> CreationGroupBuilder {
        CreationGroupBuilder {
            spec: self,
            current: CreationGroup {
                local_id: local_id.into(),
                presentation,
                fields: Vec::new(),
                condition: None,
                completion: None,
                stages_from_trigger: None,
                decider_from_trigger: None,
            },
        }
    }

    pub fn build(self) -> CreationSpec {
        CreationSpec { groups: self.groups }
    }
}

pub struct CreationGroupBuilder {
    spec: CreationSpecBuilder,
    current: CreationGroup,
}

impl CreationGroupBuilder {
    pub fn text(self, id: impl Into<StageId>) -> TextFieldBuilder {
        TextFieldBuilder {
            group: self,
            id: id.into(),
            label: String::new(),
            max_len: 80,
            required: false,
            default: None,
        }
    }

    pub fn number(self, id: impl Into<StageId>) -> NumberFieldBuilder {
        NumberFieldBuilder {
            group: self,
            id: id.into(),
            label: String::new(),
            min: i64::MIN,
            max: i64::MAX,
            default: None,
        }
    }

    pub fn pick_one(self, id: impl Into<StageId>) -> PickOneFieldBuilder {
        PickOneFieldBuilder {
            group: self,
            id: id.into(),
            label: String::new(),
            options: Vec::new(),
            options_from: None,
            options_from_ctx: None,
            options_from_trigger: None,
            default: None,
        }
    }

    pub fn pick_many(self, id: impl Into<StageId>) -> PickManyFieldBuilder {
        PickManyFieldBuilder {
            group: self,
            id: id.into(),
            label: String::new(),
            options: Vec::new(),
            options_from: None,
            options_from_ctx: None,
            options_from_trigger: None,
            min: 0,
            max: u32::MAX,
            count_from: None,
            count_from_ctx: None,
            defaults: Vec::new(),
        }
    }

    /// Close the current group and start a new one.
    pub fn group(
        mut self,
        local_id: impl Into<String>,
        presentation: PresentationMode,
    ) -> CreationGroupBuilder {
        self.spec.groups.push(self.current);
        self.spec.group(local_id, presentation)
    }

    /// CR1-B — close the current group and start a new conditional one.
    /// The engine evaluates `condition(history)` when about to open
    /// this group; if it returns false the group is skipped (advance
    /// proceeds to the next group). dnd5e uses this for the spells
    /// group, which only opens for caster classes.
    pub fn group_when(
        mut self,
        local_id: impl Into<String>,
        presentation: PresentationMode,
        condition: fn(&CreationHistory) -> bool,
    ) -> CreationGroupBuilder {
        self.spec.groups.push(self.current);
        let mut next = self.spec.group(local_id, presentation);
        next.current.condition = Some(condition);
        next
    }

    pub fn build(mut self) -> CreationSpec {
        self.spec.groups.push(self.current);
        self.spec.build()
    }

    /// WI1-FU5 — override the group's completion rule. Defaults to
    /// [`crate::staged_decision::CompletionRule::All`] when not set.
    /// `CompletionRule::Any` enables the race pattern (Counterspell).
    pub fn completion(mut self, rule: crate::staged_decision::CompletionRule) -> Self {
        self.current.completion = Some(rule);
        self
    }

    /// WI1-FU5 — install a trigger-aware stage generator. The closure
    /// runs at OpenGroup time and produces the parallel stage list.
    /// Mutually exclusive with field-driven groups: a group must
    /// declare either `fields` or `stages_from_trigger`, not both.
    /// When set, you must also call [`Self::decider_from_trigger`].
    pub fn stages_from_trigger(
        mut self,
        f: fn(
            &crate::events::GameEvent,
            &dyn crate::plugin_slices::GameContext,
        ) -> Vec<crate::staged_decision::DecisionStage>,
    ) -> Self {
        self.current.stages_from_trigger = Some(f);
        self
    }

    /// WI1-FU5 — install a paired per-stage decider generator. The
    /// closure must return a `Vec<Decider>` of the same length as the
    /// paired [`Self::stages_from_trigger`] output; the engine
    /// validates this at OpenGroup time.
    pub fn decider_from_trigger(
        mut self,
        f: fn(
            &crate::events::GameEvent,
            &dyn crate::plugin_slices::GameContext,
        ) -> Vec<crate::events::Decider>,
    ) -> Self {
        self.current.decider_from_trigger = Some(f);
        self
    }

    fn push_field(mut self, field: CreationField) -> CreationGroupBuilder {
        self.current.fields.push(field);
        self
    }
}

// --- per-field builders ---------------------------------------------------

pub struct TextFieldBuilder {
    group: CreationGroupBuilder,
    id: StageId,
    label: String,
    max_len: u32,
    required: bool,
    default: Option<String>,
}

impl TextFieldBuilder {
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
    pub fn max_len(mut self, n: u32) -> Self {
        self.max_len = n;
        self
    }
    pub fn required(mut self) -> CreationGroupBuilder {
        self.required = true;
        self.finish()
    }
    pub fn optional(mut self) -> CreationGroupBuilder {
        self.required = false;
        self.finish()
    }
    pub fn default(mut self, value: impl Into<String>) -> Self {
        self.default = Some(value.into());
        self
    }
    pub fn finish(self) -> CreationGroupBuilder {
        let TextFieldBuilder { group, id, label, max_len, required, default } = self;
        group.push_field(CreationField::Text { id, label, max_len, required, default })
    }
}

pub struct NumberFieldBuilder {
    group: CreationGroupBuilder,
    id: StageId,
    label: String,
    min: i64,
    max: i64,
    default: Option<i64>,
}

impl NumberFieldBuilder {
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
    pub fn min(mut self, n: i64) -> Self {
        self.min = n;
        self
    }
    pub fn max(mut self, n: i64) -> Self {
        self.max = n;
        self
    }
    pub fn range(mut self, range: std::ops::RangeInclusive<i64>) -> Self {
        self.min = *range.start();
        self.max = *range.end();
        self
    }
    pub fn default(mut self, value: i64) -> Self {
        self.default = Some(value);
        self
    }
    pub fn finish(self) -> CreationGroupBuilder {
        let NumberFieldBuilder { group, id, label, min, max, default } = self;
        group.push_field(CreationField::Number { id, label, min, max, default })
    }
}

pub struct PickOneFieldBuilder {
    group: CreationGroupBuilder,
    id: StageId,
    label: String,
    options: Vec<DecisionOption>,
    options_from: Option<fn(&CreationHistory) -> Vec<DecisionOption>>,
    options_from_ctx: Option<OptionsFromCtxFn>,
    options_from_trigger: Option<OptionsFromTriggerFn>,
    default: Option<String>,
}

impl PickOneFieldBuilder {
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
    pub fn options(mut self, options: Vec<DecisionOption>) -> Self {
        self.options = options;
        self
    }
    /// CR1-B — supply a function that returns the option list given
    /// the current `CreationHistory`. Wins over the static `options`
    /// list when both are set. Use a free fn (not a closure with
    /// captured state); `CreationHistory` accessors expose every
    /// resolved choice from prior stages.
    pub fn options_from(
        mut self,
        f: fn(&CreationHistory) -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from = Some(f);
        self
    }
    /// WI1-FU7 — ctx-aware variant of `options_from`. The closure
    /// receives both the history and a `&dyn GameContext` so it can
    /// narrow options by live store state (e.g. Life game's career
    /// wizard reading the player's `life:degree` pool). Preferred
    /// over `options_from` when both are set.
    pub fn options_from_ctx(
        mut self,
        f: fn(&CreationHistory, &dyn crate::plugin_slices::GameContext)
            -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from_ctx = Some(f);
        self
    }
    /// WI1-FU8 — trigger-aware variant. The closure additionally
    /// receives the trigger event so options can narrow by per-entity
    /// state read via the trigger's entity id (e.g. Life's `career`
    /// wizard filters careers by the moved Car's `life:degree` pool).
    /// Preferred over both `options_from_ctx` and `options_from` when set.
    pub fn options_from_trigger(
        mut self,
        f: fn(
            &CreationHistory,
            &dyn crate::plugin_slices::GameContext,
            &crate::events::GameEvent,
        ) -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from_trigger = Some(f);
        self
    }
    pub fn default(mut self, label: impl Into<String>) -> Self {
        self.default = Some(label.into());
        self
    }
    pub fn finish(self) -> CreationGroupBuilder {
        let PickOneFieldBuilder { group, id, label, options, options_from, options_from_ctx, options_from_trigger, default } = self;
        group.push_field(CreationField::PickOne {
            id, label, options, options_from, options_from_ctx, options_from_trigger, default,
        })
    }
}

pub struct PickManyFieldBuilder {
    group: CreationGroupBuilder,
    id: StageId,
    label: String,
    options: Vec<DecisionOption>,
    options_from: Option<fn(&CreationHistory) -> Vec<DecisionOption>>,
    options_from_ctx: Option<OptionsFromCtxFn>,
    options_from_trigger: Option<OptionsFromTriggerFn>,
    min: u32,
    max: u32,
    count_from: Option<CountFromFn>,
    count_from_ctx: Option<CountFromCtxFn>,
    defaults: Vec<String>,
}

impl PickManyFieldBuilder {
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
    pub fn options(mut self, options: Vec<DecisionOption>) -> Self {
        self.options = options;
        self
    }
    pub fn min(mut self, n: u32) -> Self {
        self.min = n;
        self
    }
    pub fn max(mut self, n: u32) -> Self {
        self.max = n;
        self
    }
    pub fn count(mut self, n: u32) -> Self {
        self.min = n;
        self.max = n;
        self
    }
    /// CR1-B — supply a function returning `(min, max)` given the
    /// current `CreationHistory`. dnd5e uses this for class-progression
    /// spell-known counts (Wizard learns 6 1st-level spells; Sorcerer
    /// learns 2). Wins over the static `min`/`max` when set.
    pub fn count_from(
        mut self,
        f: fn(&CreationHistory) -> (u32, u32),
    ) -> Self {
        self.count_from = Some(f);
        self
    }
    /// CR1-B — supply a function returning the option list given the
    /// current `CreationHistory`. Wins over the static `options` list.
    pub fn options_from(
        mut self,
        f: fn(&CreationHistory) -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from = Some(f);
        self
    }
    /// WI1-FU7 — ctx-aware variant of `options_from`.
    pub fn options_from_ctx(
        mut self,
        f: fn(&CreationHistory, &dyn crate::plugin_slices::GameContext)
            -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from_ctx = Some(f);
        self
    }
    /// WI1-FU8 — trigger-aware variant.
    pub fn options_from_trigger(
        mut self,
        f: fn(
            &CreationHistory,
            &dyn crate::plugin_slices::GameContext,
            &crate::events::GameEvent,
        ) -> Vec<DecisionOption>,
    ) -> Self {
        self.options_from_trigger = Some(f);
        self
    }
    /// WI1-FU7 — ctx-aware variant of `count_from`.
    pub fn count_from_ctx(
        mut self,
        f: fn(&CreationHistory, &dyn crate::plugin_slices::GameContext) -> (u32, u32),
    ) -> Self {
        self.count_from_ctx = Some(f);
        self
    }
    pub fn defaults(mut self, labels: Vec<String>) -> Self {
        self.defaults = labels;
        self
    }
    pub fn finish(self) -> CreationGroupBuilder {
        let PickManyFieldBuilder {
            group, id, label, options, options_from, options_from_ctx, options_from_trigger,
            min, max, count_from, count_from_ctx, defaults,
        } = self;
        group.push_field(CreationField::PickMany {
            id, label, options, options_from, options_from_ctx, options_from_trigger,
            min, max, count_from, count_from_ctx, defaults,
        })
    }
}

// ---------------------------------------------------------------------------
// Builder ergonomic glue: terminate a field-builder when the next method is a
// group-level call (.text, .pick_one, .group, .build). The `.required()` /
// `.optional()` calls on TextFieldBuilder finalise; for other fields the
// caller terminates with `.finish()` or by chaining a new field call.
//
// To support the most ergonomic chain on Number/PickOne/PickMany we provide
// `into_group()` aliases that callers can use, and supply Deref-like routing
// via these convenience pass-throughs:
// ---------------------------------------------------------------------------

macro_rules! impl_group_passthrough {
    ($builder:ident) => {
        impl $builder {
            pub fn text(self, id: impl Into<StageId>) -> TextFieldBuilder {
                self.finish().text(id)
            }
            pub fn number(self, id: impl Into<StageId>) -> NumberFieldBuilder {
                self.finish().number(id)
            }
            pub fn pick_one(self, id: impl Into<StageId>) -> PickOneFieldBuilder {
                self.finish().pick_one(id)
            }
            pub fn pick_many(self, id: impl Into<StageId>) -> PickManyFieldBuilder {
                self.finish().pick_many(id)
            }
            pub fn group(
                self,
                local_id: impl Into<String>,
                presentation: PresentationMode,
            ) -> CreationGroupBuilder {
                self.finish().group(local_id, presentation)
            }
            pub fn group_when(
                self,
                local_id: impl Into<String>,
                presentation: PresentationMode,
                condition: fn(&CreationHistory) -> bool,
            ) -> CreationGroupBuilder {
                self.finish().group_when(local_id, presentation, condition)
            }
            pub fn build(self) -> CreationSpec {
                self.finish().build()
            }
        }
    };
}

// TextFieldBuilder uses `.required()` / `.optional()` as terminators; both
// route through `finish()` defined in its main impl block above. The Deref
// impl lets callers hop straight to a field builder via deref coercion.

impl_group_passthrough!(TextFieldBuilder);
impl_group_passthrough!(NumberFieldBuilder);
impl_group_passthrough!(PickOneFieldBuilder);
impl_group_passthrough!(PickManyFieldBuilder);

// ---------------------------------------------------------------------------
// Tests — unit-level for CR1A-1 and CR1A-7 (history accessors).
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cr1a_1_single_text_field_has_configured_max_len_and_required() {
        let spec = CreationSpec::builder()
            .group("identity", PresentationMode::Parallel)
                .text("name").label("Hero name").max_len(40).required()
            .build();

        assert_eq!(spec.groups.len(), 1);
        assert_eq!(spec.groups[0].local_id, "identity");
        assert_eq!(spec.groups[0].presentation, PresentationMode::Parallel);
        assert_eq!(spec.groups[0].fields.len(), 1);

        match &spec.groups[0].fields[0] {
            CreationField::Text { id, label, max_len, required, default } => {
                assert_eq!(id.as_str(), "name");
                assert_eq!(label, "Hero name");
                assert_eq!(*max_len, 40);
                assert!(*required);
                assert!(default.is_none());
            }
            other => panic!("expected Text field, got {:?}", other),
        }
    }

    #[test]
    fn cr1a_1_multiple_groups_and_field_kinds() {
        let spec = CreationSpec::builder()
            .group("identity", PresentationMode::Parallel)
                .text("name").required()
                .pick_one("class").options(vec![
                    DecisionOption::label_only("Fighter"),
                    DecisionOption::label_only("Mage"),
                ])
            .group("scores", PresentationMode::Parallel)
                .number("str").range(3..=20).default(10)
                .number("dex").range(3..=20).default(10)
            .build();

        assert_eq!(spec.groups.len(), 2);
        assert_eq!(spec.groups[0].fields.len(), 2);
        assert_eq!(spec.groups[1].fields.len(), 2);

        match &spec.groups[0].fields[1] {
            CreationField::PickOne { options, .. } => assert_eq!(options.len(), 2),
            _ => panic!("expected PickOne"),
        }
        match &spec.groups[1].fields[0] {
            CreationField::Number { min, max, default, .. } => {
                assert_eq!(*min, 3);
                assert_eq!(*max, 20);
                assert_eq!(*default, Some(10));
            }
            _ => panic!("expected Number"),
        }
    }

    #[test]
    fn cr1a_1_pick_many_with_count() {
        let spec = CreationSpec::builder()
            .group("skills", PresentationMode::Parallel)
                .pick_many("skills")
                    .options(vec![
                        DecisionOption::label_only("Athletics"),
                        DecisionOption::label_only("Stealth"),
                        DecisionOption::label_only("Arcana"),
                    ])
                    .count(2)
            .build();

        match &spec.groups[0].fields[0] {
            CreationField::PickMany { options, min, max, .. } => {
                assert_eq!(options.len(), 3);
                assert_eq!(*min, 2);
                assert_eq!(*max, 2);
            }
            _ => panic!("expected PickMany"),
        }
    }

    #[test]
    fn cr1a_7_history_accessors_return_inserted_values() {
        let mut h = CreationHistory::new();
        h.insert(StageId::new("name"), PrefillValue::Text("Bob".into()));
        h.insert(StageId::new("str"), PrefillValue::Number(15));
        h.insert(StageId::new("class"), PrefillValue::Pick("Fighter".into()));
        h.insert(
            StageId::new("skills"),
            PrefillValue::PickMany(vec!["Athletics".into(), "Stealth".into()]),
        );

        assert_eq!(h.text("name"), Some("Bob"));
        assert_eq!(h.number("str"), Some(15));
        assert_eq!(h.pick("class"), Some("Fighter"));
        assert_eq!(
            h.pick_many("skills"),
            Some(&["Athletics".to_string(), "Stealth".to_string()][..])
        );

        assert_eq!(h.len(), 4);
        assert!(!h.is_empty());
    }

    #[test]
    fn cr1a_7_history_accessors_return_none_for_foreign_stage() {
        let mut h = CreationHistory::new();
        h.insert(StageId::new("name"), PrefillValue::Text("Bob".into()));
        assert!(h.text("nonexistent").is_none());
        assert!(h.number("nonexistent").is_none());
        assert!(h.pick("nonexistent").is_none());
        assert!(h.pick_many("nonexistent").is_none());
    }

    #[test]
    fn cr1a_7_history_accessors_return_none_for_wrong_type() {
        let mut h = CreationHistory::new();
        h.insert(StageId::new("name"), PrefillValue::Text("Bob".into()));
        // "name" is Text; reading as number/pick/pick_many returns None.
        assert!(h.number("name").is_none());
        assert!(h.pick("name").is_none());
        assert!(h.pick_many("name").is_none());
    }
}
