//! WI1-FU4 — per-session registry of plugin-declared declarative
//! wizards keyed by arbitrary [`WizardKind`]. Generalizes
//! [`crate::creation_registry::CreationRegistry`] which is hardcoded
//! to fire on [`GameEvent::EntityCreationRequested`].
//!
//! Plugin authors register entries during `ModelPlugin::run_setup` via
//! `SetupContext::register_wizard_spec(kind, spec_fn, compose_fn)`.
//! The plugin's Filter handler emits
//! `Decision::Wizard(StagedDecision::new(title, kind, decider))` for
//! the registered kind from whatever trigger event makes sense
//! (e.g. `GameEvent::Moved` for Life game's career-space landing).
//! The engine's dispatch site detects the registered kind and drives
//! the composer; on resolve, the plugin's `compose_fn` runs with the
//! trigger event, the resolved-group history, and a [`ComposeCtx`].
//!
//! Difference from `CreationRegistry`:
//!
//! - Key is [`WizardKind`] (`(plugin_canonical, local_name)`), not
//!   `EntityTypeId`. A single plugin can register many composition
//!   wizards with distinct local names.
//! - `compose_fn` takes the full trigger [`GameEvent`] (not just
//!   `by_seat` / `prefill`) because non-creation triggers carry
//!   wizard-relevant data in arbitrary event variants.
//! - `compose_fn` takes `&[ResolvedGroup]` directly (rich access to
//!   per-stage `DecisionOption.payload`), not a compressed
//!   `CreationHistory`. Plugins that prefer the label-keyed view can
//!   still build one via `CreationHistory` helpers; but Life-style
//!   uses (option-payload-as-index) work natively.

use std::collections::HashMap;
use std::sync::Arc;

use crate::creation::{ComposeCtx, CreationSpec};
use crate::events::GameEvent;
use crate::staged_decision::{ResolvedGroup, WizardKind};

/// Plugin's `composition_spec` constructor. Returns the static
/// declaration of wizard fields. Mirrors
/// [`crate::creation_registry::CreationSpecFn`].
pub type CompositionSpecFn = fn() -> CreationSpec;

/// Plugin's `composition_compose` function — called when the wizard
/// resolves. Receives:
/// - the trigger event (whatever event the plugin's Filter dispatched
///   the wizard from)
/// - the resolved-group history (DecisionOption payloads accessible
///   per stage)
/// - a [`crate::plugin_slices::GameContext`] for store reads,
///   predicate evaluation, log queries, etc.
/// - a [`ComposeCtx`] for id allocation and the initiating seat
///
/// Returns a [`ComposeResult`] carrying side-effect events plus an
/// optional `pipeline_result` that threads back to the wizard's
/// filtered trigger event (used by Counterspell to deny `Activated`
/// on a successful counter; ignored by Life-style wizards opened
/// from non-Filter triggers).
///
/// Distinct from [`crate::creation_registry::ComposeFn`] in three ways:
/// 1. Trigger event is passed through (creation flows derive context
///    from `EntityCreationRequested` instead).
/// 2. `GameContext` is included so compose can read live store state
///    (e.g. for "current pool value" deltas) — creation flows don't
///    need this because they spawn fresh entities via `alloc_id`.
/// 3. History is `&[ResolvedGroup]` (rich access to per-stage
///    `DecisionOption.payload`) not the compressed `CreationHistory`.
///
/// Arc-wrapped since WASM1 Phase 3c-compose; see `ComposeFn` docs in
/// `creation_registry` for the rationale.
pub type CompositionComposeFn = Arc<
    dyn for<'a, 'b, 'c, 'd> Fn(
            &'a GameEvent,
            &'b [ResolvedGroup],
            &'c dyn crate::plugin_slices::GameContext,
            &'d mut ComposeCtx<'_>,
        ) -> ComposeResult
        + Send
        + Sync,
>;

/// WI1-FU5 — output of a [`CompositionComposeFn`] after wizard resolution.
///
/// `pipeline_result` is the bridge between wizards opened from `Filter`
/// handlers (which parked an original event via `Decision::Wizard`) and
/// the resolution path. `Some(Decision::Deny)` denies the parked trigger
/// (Counterspell's "Counter!" path); `Some(Decision::Allow)` lets it
/// through; `None` is the side-effect-only default (Life game's career /
/// lawsuit / lose_job wizards — they never deny their `Moved` trigger).
pub struct ComposeResult {
    pub effects:         Vec<GameEvent>,
    pub pipeline_result: Option<crate::handler::Decision>,
}

impl ComposeResult {
    /// The common side-effects-only path. Equivalent to the pre-FU5
    /// `Vec<GameEvent>` return — `pipeline_result` is `None`, so the
    /// wizard's trigger event proceeds untouched.
    pub fn effects_only(effects: Vec<GameEvent>) -> Self {
        Self { effects, pipeline_result: None }
    }

    /// Deny the wizard's trigger and emit `effects` on the way out.
    pub fn deny(reason: impl Into<String>, effects: Vec<GameEvent>) -> Self {
        Self {
            effects,
            pipeline_result: Some(crate::handler::Decision::Deny(reason.into())),
        }
    }

    /// Explicitly allow the wizard's trigger and emit `effects`.
    /// Functionally identical to `effects_only` for `Filter`-opened
    /// wizards (both result in the trigger proceeding) but expresses
    /// the "decision made, allow" intent — e.g. Counterspell when
    /// every reactor passed.
    pub fn allow(effects: Vec<GameEvent>) -> Self {
        Self {
            effects,
            pipeline_result: Some(crate::handler::Decision::Allow),
        }
    }
}

/// Ergonomic conversion so existing side-effect-only compose closures
/// migrate by appending `.into()` to their returns instead of wrapping
/// each one in `ComposeResult::effects_only(...)`. Equivalent semantics.
impl From<Vec<GameEvent>> for ComposeResult {
    fn from(effects: Vec<GameEvent>) -> Self {
        Self::effects_only(effects)
    }
}

/// One registry entry: a paired `(spec_fn, compose_fn)`.
#[derive(Clone)]
pub struct CompositionEntry {
    pub spec_fn: CompositionSpecFn,
    pub compose_fn: CompositionComposeFn,
}

/// Per-session lookup from `WizardKind` to its composition pair.
#[derive(Default)]
pub struct CompositionRegistry {
    by_kind: HashMap<WizardKind, CompositionEntry>,
}

impl CompositionRegistry {
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a composition pair. Last write wins for a duplicate
    /// kind — matches `CreationRegistry::register`. Plugins should
    /// pick unique `local_name` values within their `canonical_id`.
    pub fn register(
        &mut self,
        kind: WizardKind,
        spec_fn: CompositionSpecFn,
        compose_fn: CompositionComposeFn,
    ) {
        self.by_kind.insert(kind, CompositionEntry { spec_fn, compose_fn });
    }

    pub fn entry_for(&self, kind: &WizardKind) -> Option<&CompositionEntry> {
        self.by_kind.get(kind)
    }

    pub fn spec_for(&self, kind: &WizardKind) -> Option<CreationSpec> {
        self.entry_for(kind).map(|e| (e.spec_fn)())
    }

    pub fn compose_fn_for(&self, kind: &WizardKind) -> Option<CompositionComposeFn> {
        self.entry_for(kind).map(|e| e.compose_fn.clone())
    }

    pub fn contains(&self, kind: &WizardKind) -> bool {
        self.by_kind.contains_key(kind)
    }

    pub fn registered_kinds(&self) -> Vec<WizardKind> {
        self.by_kind.keys().cloned().collect()
    }

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

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugin_id::CanonicalId;
    use crate::staged_decision::PresentationMode;

    fn make_kind(local: &str) -> WizardKind {
        let canonical = CanonicalId::new("github.com/test/plugin")
            .unwrap_or_else(|_| panic!("test fixture"));
        WizardKind::new(canonical, local)
    }

    fn empty_spec() -> CreationSpec {
        CreationSpec::builder()
            .group("g", PresentationMode::Sequential)
            .build()
    }

    fn empty_compose(
        _t: &GameEvent,
        _h: &[ResolvedGroup],
        _gc: &dyn crate::plugin_slices::GameContext,
        _c: &mut ComposeCtx<'_>,
    ) -> ComposeResult {
        ComposeResult::effects_only(Vec::new())
    }

    #[test]
    fn fu4_register_and_lookup_round_trip() {
        let mut reg = CompositionRegistry::new();
        let kind = make_kind("career");
        reg.register(kind.clone(), empty_spec, Arc::new(empty_compose));
        assert!(reg.contains(&kind));
        assert_eq!(reg.len(), 1);
        assert!(reg.entry_for(&kind).is_some());
    }

    #[test]
    fn fu4_lookup_misses_for_unregistered_kind() {
        let reg = CompositionRegistry::new();
        let kind = make_kind("never_registered");
        assert!(!reg.contains(&kind));
        assert!(reg.entry_for(&kind).is_none());
    }

    #[test]
    fn fu4_re_register_overwrites() {
        let mut reg = CompositionRegistry::new();
        let kind = make_kind("career");
        reg.register(kind.clone(), empty_spec, Arc::new(empty_compose));
        reg.register(kind.clone(), empty_spec, Arc::new(empty_compose));
        assert_eq!(reg.len(), 1, "re-register replaces, not duplicates");
    }
}
