//! # Module boundary (M1 / M5)
//!
//! Two structs live here, with a deliberate split between them:
//!
//! **`Entity`** (single-entity concerns) -- owns per-entity field data and
//! field-level invariants. Trait-data accessors (`ownable()`, `tangible()`,
//! `quantified()`, ...), builder helpers, and any per-entity invariant
//! checks belong on `Entity`. When you add a new trait field, the accessor
//! method and any single-entity-side invariant live here.
//!
//! **`EntityState`** (collection concerns) -- owns cross-entity
//! relationships and indexes that span all live entities. Reverse maps
//! (`contained_by`, `attached_to`), derived sets (`temporal_ids`,
//! `sustained_index`), and any operation that touches more than one
//! entity belong on `EntityState`. When you add a new trait that
//! introduces a relationship, the index + its maintenance helpers live
//! here.
//!
//! The boundary is internal to this file rather than across files (the
//! M1 v11 work landed them in one module; the `store.rs` filename
//! mentioned in older comments never materialised). Both structs are
//! free to share private helpers, but cross-file readers should still
//! be able to predict which struct owns which concern from this
//! distinction.
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use crate::trait_data::{TraitSet, NamedData, SustainedData, TemporalData, RangedData, ArealData, FeaturedData, ObservableData, ObscurableData, OwnableData, AttachedData, TangibleData, ConnectorData, MovableData, ContainerData, QuantifiedData, ActivatableData, MorphableData, NarratedData, GatedData, SenseData, SustainGroup, TraitChangeSet};
#[cfg(test)]
use crate::trait_data::{MovementType, ContainerType, CONCENTRATION, PERFORMANCE};
use crate::types::{EntityId, EntityTypeId, TagSet, Owner, SeatId, Position};
#[cfg(test)]
use crate::board::FaceId;

// ---------------------------------------------------------------------------
// Entity — unified per-entity record.
//
// Phase 4 of the TraitSet refactor: this struct is the *sole* source of truth
// for per-entity trait data. The previous per-trait `HashMap<EntityId, ...Data>`
// fields and the `entity_traits: HashMap<EntityId, TraitSet>` mirror have been
// deleted. Reads go through `store.entity(id)`; writes go through the
// `add_X`/`remove_X` helpers (which now just call setters on the `Entity`).
//
// `cached_traits` is a 32-bit bitfield maintained by the setters below in
// lockstep with the `Option<...Data>` fields, so `Entity::has_traits(req)` is
// O(1).
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Entity {
    pub id: EntityId,
    pub type_id: EntityTypeId,
    /// PRIVATE: only mutated by the bit-aware setters below so it always
    /// stays in lockstep with the per-trait `Option<...Data>` fields. Read
    /// via `traits()` / `has_trait()`.
    cached_traits: TraitSet,

    // PRIVATE per-trait data. Mutate via `set_xxx(data)` / `clear_xxx()`;
    // read via `xxx()` / `xxx_mut()`. The bit in `cached_traits` is updated
    // atomically with each setter so the invariant "bit set iff data is
    // Some" cannot be broken without going through `unsafe` or a debug
    // helper.

    // Arc-wrapped fields: O(1) clone in entity_view_for (atomic refcount bump).
    //
    // INVARIANT: Do not store Arc<TraitData> clones across propose() boundaries.
    // If the pipeline rolls back (on Suspend or Deny), the inverse-op log restores
    // EntityStore to its pre-proposal state — but external Arc clones still point
    // to the pre-rollback data. Handlers that are stateless functions (the normal
    // case) are unaffected because their locals are dropped on return. Async handlers
    // or handler-side caches that outlive a propose() call will observe stale data.
    named:       Option<Arc<NamedData>>,
    sustained:   Option<SustainedData>,
    temporal:    Option<TemporalData>,
    ranged:      Option<RangedData>,
    areal:       Option<ArealData>,
    featured:    Option<FeaturedData>,
    observable:  Option<ObservableData>,
    obscurable:  Option<ObscurableData>,
    ownable:     Option<OwnableData>,
    attached:    Option<AttachedData>,
    tangible:    Option<TangibleData>,
    connector:   Option<Arc<ConnectorData>>,
    movable:     Option<MovableData>,
    container:   Option<Arc<ContainerData>>,
    quantified:  Option<Arc<QuantifiedData>>,
    activatable: Option<Arc<ActivatableData>>,
    morphable:   Option<Arc<MorphableData>>,
    narrated:    Option<NarratedData>,
    gated:       Option<GatedData>,
    sense:       Option<SenseData>,
    pub tags:    TagSet,
}

/// Generate the four-method accessor pattern (read / read-mut / set / clear)
/// for one trait field of `Entity`. Setter and clearer flip the matching
/// `cached_traits` bit atomically with the data update so the bitfield
/// invariant is upheld by construction.
macro_rules! trait_accessor {
    ($field:ident, $ty:ident, $bit:ident, $setter:ident, $clearer:ident, $getter_mut:ident) => {
        #[doc = concat!("Read-only borrow of the `", stringify!($field), "` trait data, if present.")]
        #[inline]
        pub fn $field(&self) -> Option<&$ty> { self.$field.as_ref() }
        #[doc = concat!("Mutable borrow of the inner `", stringify!($field), "` data; trait presence is unchanged.")]
        #[inline]
        pub fn $getter_mut(&mut self) -> Option<&mut $ty> { self.$field.as_mut() }
        #[doc = concat!("Install `", stringify!($ty), "` and set the matching trait bit atomically.")]
        #[inline]
        pub fn $setter(&mut self, data: $ty) {
            self.$field = Some(data);
            self.cached_traits |= TraitSet::$bit;
        }
        #[doc = concat!("Clear the `", stringify!($field), "` data and matching trait bit atomically.")]
        #[inline]
        pub fn $clearer(&mut self) {
            self.$field = None;
            self.cached_traits.remove(TraitSet::$bit);
        }
    };
}

/// Arc-backed variant of `trait_accessor!` for fields whose data structs
/// contain Strings or Vecs (i.e., heap-allocated content). The field stores
/// `Option<Arc<T>>` instead of `Option<T>`. This makes `entity_view_for`
/// O(1) for these fields: 500 player views of the same entity share one Arc,
/// each view construction is an atomic refcount increment rather than a deep clone.
///
/// External API is identical to `trait_accessor!`:
/// - `$field()` returns `Option<&T>` via `Arc::deref()`.
/// - `$getter_mut()` uses `Arc::make_mut()` for COW semantics.
/// - `$setter()` wraps the value in `Arc::new()`.
/// - `$arc_getter()` returns `Option<&Arc<T>>` — used by `entity_view_for`
///   to perform an O(1) `Arc::clone()` into `EntityView`.
macro_rules! arc_trait_accessor {
    ($field:ident, $ty:ident, $bit:ident, $setter:ident, $clearer:ident, $getter_mut:ident, $arc_getter:ident) => {
        #[doc = concat!("Read-only borrow of the `", stringify!($field), "` trait data (via Arc deref), if present.")]
        #[inline]
        pub fn $field(&self) -> Option<&$ty> { self.$field.as_deref() }
        #[doc = concat!("Borrow the Arc pointer itself — use in `entity_view_for` to do O(1) `Arc::clone()`.")]
        #[inline]
        pub fn $arc_getter(&self) -> Option<&Arc<$ty>> { self.$field.as_ref() }
        #[doc = concat!("Mutable borrow using `Arc::make_mut` (COW: clones if refcount > 1).")]
        #[inline]
        pub fn $getter_mut(&mut self) -> Option<&mut $ty> { self.$field.as_mut().map(Arc::make_mut) }
        #[doc = concat!("Install `", stringify!($ty), "` (wrapped in Arc) and set the matching trait bit atomically.")]
        #[inline]
        pub fn $setter(&mut self, data: $ty) {
            self.$field = Some(Arc::new(data));
            self.cached_traits |= TraitSet::$bit;
        }
        #[doc = concat!("Clear the `", stringify!($field), "` data and matching trait bit atomically.")]
        #[inline]
        pub fn $clearer(&mut self) {
            self.$field = None;
            self.cached_traits.remove(TraitSet::$bit);
        }
    };
}

impl Entity {
    pub fn new(id: EntityId, type_id: EntityTypeId) -> Self {
        Self {
            id, type_id,
            cached_traits: TraitSet::empty(),
            named: None, sustained: None, temporal: None, ranged: None, areal: None,
            featured: None, observable: None, obscurable: None, ownable: None,
            attached: None, tangible: None, connector: None, movable: None,
            container: None, quantified: None, activatable: None, morphable: None,
            narrated: None, gated: None, sense: None, tags: TagSet::new(),
        }
    }

    /// Cached bitfield reflecting which `Option<...Data>` fields are `Some`.
    /// Maintained by the `set_xxx` setters below; readable in O(1).
    pub fn trait_set(&self) -> TraitSet { self.cached_traits }

    /// Convenience alias for `trait_set()`.
    #[inline]
    pub fn traits(&self) -> TraitSet { self.cached_traits }

    pub fn has_traits(&self, required: TraitSet) -> bool {
        self.cached_traits.contains(required)
    }

    /// True if the entity has *any* of the bits in `t`. (Single-bit calls
    /// behave the same as `has_traits` because `t.contains(t)` is trivially
    /// true on the bit set.)
    #[inline]
    pub fn has_trait(&self, t: TraitSet) -> bool { self.cached_traits.contains(t) }

    /// Recompute `cached_traits` from the actual `Option<_>` fields. Use only
    /// for sanity tests / debug invariant checks; setters keep the bitfield
    /// in sync atomically.
    pub fn derive_traits(&self) -> TraitSet {
        // Walk the private fields directly — equivalent to `self.named().is_some()` etc.
        let mut t = TraitSet::empty();
        if self.named.is_some()       { t |= TraitSet::NAMED; }
        if self.sustained.is_some()   { t |= TraitSet::SUSTAINED; }
        if self.temporal.is_some()    { t |= TraitSet::TEMPORAL; }
        if self.ranged.is_some()      { t |= TraitSet::RANGED; }
        if self.areal.is_some()       { t |= TraitSet::AREAL; }
        if self.featured.is_some()    { t |= TraitSet::FEATURED; }
        if self.observable.is_some()  { t |= TraitSet::OBSERVABLE; }
        if self.obscurable.is_some()  { t |= TraitSet::OBSCURABLE; }
        if self.ownable.is_some()     { t |= TraitSet::OWNABLE; }
        if self.attached.is_some()    { t |= TraitSet::ATTACHED; }
        if self.tangible.is_some()    { t |= TraitSet::TANGIBLE; }
        if self.connector.is_some()   { t |= TraitSet::CONNECTOR; }
        if self.movable.is_some()     { t |= TraitSet::MOVABLE; }
        if self.container.is_some()   { t |= TraitSet::CONTAINER; }
        if self.quantified.is_some()  { t |= TraitSet::QUANTIFIED; }
        if self.activatable.is_some() { t |= TraitSet::ACTIVATABLE; }
        if self.morphable.is_some()   { t |= TraitSet::MORPHABLE; }
        if self.gated.is_some()       { t |= TraitSet::GATED; }
        if self.sense.is_some()       { t |= TraitSet::SENSE; }
        // PRESENT, LOCATED, and OBSERVABLE (when used as a marker without ObservableData)
        // are set via TraitAdded without a backing struct. Propagate from cached_traits so
        // verify_invariant does not spuriously fail. The OR is a no-op when the struct is Some.
        t |= self.cached_traits & (TraitSet::PRESENT | TraitSet::LOCATED | TraitSet::OBSERVABLE);
        t
    }

    // ---- Encapsulated accessors via `trait_accessor!` macro --------------
    // Each invocation generates four methods:
    //   pub fn $field(&self) -> Option<&$ty>
    //   pub fn $getter_mut(&mut self) -> Option<&mut $ty>
    //   pub fn $setter(&mut self, data: $ty)         // sets bit + data atomically
    //   pub fn $clearer(&mut self)                    // clears bit + data atomically
    // Direct field assignment is impossible from outside this module because
    // every per-trait `Option<XxxData>` field is private.
    // Arc-backed heavy fields (contain String or Vec — O(1) clone in entity_view_for)
    arc_trait_accessor!(named,        NamedData,        NAMED,        set_named,        clear_named,        named_mut,        named_arc);
    arc_trait_accessor!(connector,    ConnectorData,    CONNECTOR,    set_connector,    clear_connector,    connector_mut,    connector_arc);
    arc_trait_accessor!(container,    ContainerData,    CONTAINER,    set_container,    clear_container,    container_mut,    container_arc);
    arc_trait_accessor!(quantified,   QuantifiedData,   QUANTIFIED,   set_quantified,   clear_quantified,   quantified_mut,   quantified_arc);
    arc_trait_accessor!(activatable,  ActivatableData,  ACTIVATABLE,  set_activatable,  clear_activatable,  activatable_mut,  activatable_arc);
    arc_trait_accessor!(morphable,    MorphableData,    MORPHABLE,    set_morphable,    clear_morphable,    morphable_mut,    morphable_arc);

    // Light fields (Copy-ish: no heap allocations worth wrapping in Arc)
    trait_accessor!(sustained,    SustainedData,    SUSTAINED,    set_sustained,    clear_sustained,    sustained_mut);
    trait_accessor!(temporal,     TemporalData,     TEMPORAL,     set_temporal,     clear_temporal,     temporal_mut);
    trait_accessor!(ranged,       RangedData,       RANGED,       set_ranged,       clear_ranged,       ranged_mut);
    trait_accessor!(areal,        ArealData,        AREAL,        set_areal,        clear_areal,        areal_mut);
    trait_accessor!(featured,     FeaturedData,     FEATURED,     set_featured,     clear_featured,     featured_mut);
    trait_accessor!(observable,   ObservableData,   OBSERVABLE,   set_observable,   clear_observable,   observable_mut);
    trait_accessor!(obscurable,   ObscurableData,   OBSCURABLE,   set_obscurable,   clear_obscurable,   obscurable_mut);
    trait_accessor!(ownable,      OwnableData,      OWNABLE,      set_ownable,      clear_ownable,      ownable_mut);
    trait_accessor!(attached,     AttachedData,     ATTACHED,     set_attached,     clear_attached,     attached_mut);
    trait_accessor!(tangible,     TangibleData,     TANGIBLE,     set_tangible,     clear_tangible,     tangible_mut);
    trait_accessor!(movable,      MovableData,      MOVABLE,      set_movable,      clear_movable,      movable_mut);
    trait_accessor!(gated,        GatedData,        GATED,        set_gated,        clear_gated,        gated_mut);
    trait_accessor!(sense,        SenseData,        SENSE,        set_sense,        clear_sense,        sense_mut);

    // Narrated has no trait bit (open-ended fallback) — manage the field
    // directly without touching `cached_traits`.
    /// Read the narrated data, if present.
    #[inline]
    pub fn narrated(&self) -> Option<&NarratedData> { self.narrated.as_ref() }
    /// Mutably borrow the narrated data, if present.
    #[inline]
    pub fn narrated_mut(&mut self) -> Option<&mut NarratedData> { self.narrated.as_mut() }
    /// Install narrated data (no trait bit).
    #[inline]
    pub fn set_narrated(&mut self, data: NarratedData) { self.narrated = Some(data); }
    /// Clear narrated data.
    #[inline]
    pub fn clear_narrated(&mut self) { self.narrated = None; }

    // ---- Privileged sanity-check escape hatches (test-only) --------------
    //
    // These exist solely to support `verify_invariant` regression tests that
    // *deliberately* break the invariant in order to confirm the check fires.
    // They are gated on `cfg(debug_assertions)` and `#[doc(hidden)]`. There
    // is no production path that should call them.
    #[cfg(debug_assertions)]
    #[doc(hidden)]
    pub fn debug_force_set_field_no_bit_named(&mut self, data: NamedData) { self.named = Some(Arc::new(data)); }
    #[cfg(debug_assertions)]
    #[doc(hidden)]
    pub fn debug_force_set_field_no_bit_tangible(&mut self, data: TangibleData) { self.tangible = Some(data); }
    #[cfg(debug_assertions)]
    #[doc(hidden)]
    pub fn debug_force_set_bit_no_field(&mut self, bits: TraitSet) { self.cached_traits |= bits; }
}

// ---------------------------------------------------------------------------
// EntityState — single `entities: HashMap<EntityId, Entity>` source of truth.
//
// M5 boundary: **cross-entity relationships and collection-level indexes**.
//
// Fields on `EntityState` (this struct) own cross-entity concerns:
//   • indexes keyed by more than one `EntityId` (e.g. `contained_by`, `attached_to`)
//   • derived sets that span all live entities (e.g. `temporal_ids`, `sustained_index`)
//
// When adding a new trait that introduces a relationship (reverse pointer,
// group-exclusivity index, etc.), the index and its maintenance helpers live
// here. Per-field accessors and single-entity invariants live on `Entity` above.
// ---------------------------------------------------------------------------

#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EntityState {
    /// `Cell<u64>` doesn't derive serde — go through u64 via a custom
    /// `with` helper. The counter must round-trip exactly so replay
    /// doesn't reuse freed ids.
    #[cfg_attr(feature = "serde", serde(with = "cell_u64_serde"))]
    next_id: Cell<u64>,
    pub alive: HashSet<EntityId>,

    /// Authoritative per-entity record. All trait data and the cached
    /// `TraitSet` bitfield live here. Entity state tags (boolean markers
    /// like `TAG_KING`, `is_stunned`) live on `Entity.tags`.
    pub entities: HashMap<EntityId, Entity>,

    /// Container index: `contained_by[child] = parent` for entities currently
    /// in another entity's `ContainerData.contents`. Maintained by the
    /// pipeline alongside `ContainerData.contents` mutations.
    pub contained_by: HashMap<EntityId, EntityId>,

    /// Attachment indices: `attached_to[child] = target` and the reverse
    /// `attachments[target] = [children]`. Maintained by the builder /
    /// `attach`/`detach` helpers. `AttachedData.target` carries the
    /// authoritative pointer.
    pub attached_to: HashMap<EntityId, EntityId>,
    pub attachments: HashMap<EntityId, Vec<EntityId>>,

    /// AR-5: O(1) set of live Temporal entities. Maintained by
    /// `add_temporal` / `remove_temporal` / `destroy`. Replaces the
    /// O(n) scan of all entities in `temporal_tick_for_event`.
    pub temporal_ids: HashSet<EntityId>,

    /// AR-2: O(1) lookup for Sustained exclusivity. Maps (group, owner) →
    /// the live entity occupying that slot. Maintained by `add_sustained` /
    /// `remove_sustained` / `destroy`. Replaces the O(n) scan in
    /// `sustained_conflict_for_event`.
    pub sustained_index: HashMap<(SustainGroup, Owner), EntityId>,
}

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

    /// Allocate the next entity ID, advancing the counter.
    /// Available on `&self` so handlers can allocate IDs through a `GameContext`.
    pub fn alloc_id(&self) -> EntityId {
        let id = EntityId::from_raw(self.next_id.get());
        self.next_id.set(id.raw() + 1);
        id
    }

    /// Ensure the counter is beyond `id` (used when replaying EntityCreated from the log).
    pub fn ensure_id_beyond(&self, id: EntityId) {
        let required = id.raw() + 1;
        if required > self.next_id.get() {
            self.next_id.set(required);
        }
    }

    /// Read the next-id allocator's current cursor. Pipeline rollback
    /// uses this to capture-and-restore `next_id` around `EntityCreated`.
    pub fn next_id_get(&self) -> u64 {
        self.next_id.get()
    }

    /// Set the next-id allocator's cursor directly. Pipeline rollback
    /// only.
    pub fn next_id_set(&self, value: u64) {
        self.next_id.set(value);
    }

    /// Direct-mutation spawn for store-level tests and internal fixtures.
    /// Game logic must go through `GameSession::spawn()` → `propose(EntityCreated)`.
    pub fn spawn(&mut self, type_id: EntityTypeId) -> EntityBuilder<'_> {
        let id = self.alloc_id();
        self.alive.insert(id);
        self.entities.insert(id, Entity::new(id, type_id));
        EntityBuilder { store: self, id }
    }

    /// Read-only accessor for the unified entity record.
    /// Returns `None` if the entity does not exist.
    pub fn entity(&self, id: EntityId) -> Option<&Entity> {
        self.entities.get(&id)
    }

    pub fn entity_mut(&mut self, id: EntityId) -> Option<&mut Entity> {
        self.entities.get_mut(&id)
    }

    pub fn destroy(&mut self, id: EntityId) {
        if !self.alive.remove(&id) {
            return;
        }
        // Clean up AR-5 and AR-2 indexes before the entity record is dropped.
        self.temporal_ids.remove(&id);
        if let Some(e) = self.entities.get(&id) {
            if let (Some(s), Some(o)) = (e.sustained(), e.ownable()) {
                let key = (s.group, o.owner);
                if self.sustained_index.get(&key) == Some(&id) {
                    self.sustained_index.remove(&key);
                }
            }
        }
        self.entities.remove(&id);

        // Clean up container index
        if let Some(container) = self.contained_by.remove(&id) {
            if let Some(parent) = self.entities.get_mut(&container) {
                if let Some(c) = parent.container.as_mut().map(Arc::make_mut) {
                    c.contents.retain(|e| *e != id);
                }
            }
        }

        // Clean up attachment index
        if let Some(target) = self.attached_to.remove(&id) {
            if let Some(attached) = self.attachments.get_mut(&target) {
                attached.retain(|e| *e != id);
            }
        }
        self.attachments.remove(&id);
    }

    pub fn is_alive(&self, id: EntityId) -> bool {
        self.alive.contains(&id)
    }

    pub fn attach(&mut self, entity: EntityId, target: EntityId) {
        self.attached_to.insert(entity, target);
        self.attachments.entry(target).or_default().push(entity);
        // Keep the AttachedData in sync — if the entity has the ATTACHED trait declared,
        // update its target to match.
        if let Some(e) = self.entities.get_mut(&entity) {
            if let Some(a) = e.attached_mut() {
                a.target = target;
            }
        }
    }

    pub fn detach(&mut self, entity: EntityId) {
        if let Some(target) = self.attached_to.remove(&entity) {
            if let Some(attached) = self.attachments.get_mut(&target) {
                attached.retain(|e| *e != entity);
            }
        }
    }

    // ---------------------------------------------------------------------------
    // Query helpers
    // ---------------------------------------------------------------------------

    /// Iterate every alive entity whose cached trait bitfield contains
    /// `required`. O(n) over alive entities; the bitfield check is a single
    /// integer AND, so the per-entity cost is essentially free.
    pub fn query_ids(&self, required: TraitSet) -> impl Iterator<Item = EntityId> + '_ {
        self.entities
            .values()
            .filter(move |e| e.has_traits(required))
            .map(|e| e.id)
    }

    pub fn has_traits(&self, id: EntityId, required: TraitSet) -> bool {
        self.entities
            .get(&id)
            .map(|e| e.has_traits(required))
            .unwrap_or(false)
    }

    /// Cached trait bitfield for `id`, or empty if unknown.
    pub fn entity_traits(&self, id: EntityId) -> TraitSet {
        self.entities.get(&id).map(|e| e.trait_set()).unwrap_or_default()
    }

    pub fn owned_by(&self, seat: SeatId) -> impl Iterator<Item = EntityId> + '_ {
        self.entities
            .values()
            .filter(move |e| e.ownable().map(|o| o.owner.is_seat(seat)).unwrap_or(false))
            .map(|e| e.id)
    }

    pub fn at_position(&self, pos: Position) -> Vec<EntityId> {
        self.entities
            .values()
            .filter(|e| e.tangible().map(|t| t.position == pos).unwrap_or(false))
            .map(|e| e.id)
            .filter(|id| self.alive.contains(id))
            .collect()
    }

    pub fn contents_of(&self, container: EntityId) -> &[EntityId] {
        self.entities
            .get(&container)
            .and_then(|e| e.container())
            .map(|c| c.contents.as_slice())
            .unwrap_or(&[])
    }

    pub fn attachments_of(&self, entity: EntityId) -> &[EntityId] {
        self.attachments
            .get(&entity)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }

    /// O(k) reverse lookup: every entity currently attached to `parent`
    /// via its `AttachedData.target`. Returned slice is in insertion order;
    /// attachments behave as an ordered Vec because conditions/auras/buffs
    /// often care about precedence (e.g. "first applied wins").
    ///
    /// Equivalent to `attachments_of` — provided as a clearer name for
    /// the reverse-index pattern (parent → children).
    pub fn attached_children(&self, parent: EntityId) -> &[EntityId] {
        self.attachments
            .get(&parent)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }

    /// O(1) reverse lookup: which container (if any) currently holds
    /// `child` in its `ContainerData.contents`?
    pub fn container_of(&self, child: EntityId) -> Option<EntityId> {
        self.contained_by.get(&child).copied()
    }

    // ---------------------------------------------------------------------------
    // Typed trait mutation — operates on the unified `Entity` record.
    // Use these instead of direct `Entity` field assignment when adding/removing
    // traits at runtime; the setters keep `cached_traits` in lockstep.
    // ---------------------------------------------------------------------------

    pub fn add_named(&mut self, id: EntityId, data: NamedData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_named(data); }
    }
    pub fn remove_named(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_named(); }
    }

    pub fn add_sustained(&mut self, id: EntityId, data: SustainedData) {
        if let Some(e) = self.entities.get_mut(&id) {
            let group = data.group;
            e.set_sustained(data);
            // AR-2: only index when we also have an owner.
            if let Some(owner) = e.ownable().map(|o| o.owner) {
                self.sustained_index.insert((group, owner), id);
            }
        }
    }
    pub fn remove_sustained(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) {
            if let (Some(s), Some(o)) = (e.sustained(), e.ownable()) {
                let key = (s.group, o.owner);
                if self.sustained_index.get(&key) == Some(&id) {
                    self.sustained_index.remove(&key);
                }
            }
            e.clear_sustained();
        }
    }

    pub fn add_temporal(&mut self, id: EntityId, data: TemporalData) {
        if let Some(e) = self.entities.get_mut(&id) {
            e.set_temporal(data);
            self.temporal_ids.insert(id); // AR-5
        }
    }
    pub fn remove_temporal(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) {
            e.clear_temporal();
            self.temporal_ids.remove(&id); // AR-5
        }
    }

    pub fn add_ranged(&mut self, id: EntityId, data: RangedData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_ranged(data); }
    }
    pub fn remove_ranged(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_ranged(); }
    }

    pub fn add_areal(&mut self, id: EntityId, data: ArealData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_areal(data); }
    }
    pub fn remove_areal(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_areal(); }
    }

    pub fn add_featured(&mut self, id: EntityId, data: FeaturedData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_featured(data); }
    }
    pub fn remove_featured(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_featured(); }
    }

    pub fn add_tangible(&mut self, id: EntityId, data: TangibleData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_tangible(data); }
    }
    pub fn remove_tangible(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_tangible(); }
    }

    pub fn add_quantified(&mut self, id: EntityId, data: QuantifiedData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_quantified(data); }
    }
    pub fn remove_quantified(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_quantified(); }
    }

    pub fn add_ownable(&mut self, id: EntityId, data: OwnableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_ownable(data); }
    }
    pub fn remove_ownable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_ownable(); }
    }

    pub fn add_activatable(&mut self, id: EntityId, data: ActivatableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_activatable(data); }
    }
    pub fn remove_activatable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_activatable(); }
    }

    pub fn add_morphable(&mut self, id: EntityId, data: MorphableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_morphable(data); }
    }
    pub fn remove_morphable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_morphable(); }
    }

    pub fn add_gated(&mut self, id: EntityId, data: GatedData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_gated(data); }
    }
    pub fn remove_gated(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_gated(); }
    }

    pub fn add_sense(&mut self, id: EntityId, data: SenseData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_sense(data); }
    }
    pub fn remove_sense(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_sense(); }
    }

    pub fn add_observable(&mut self, id: EntityId, data: ObservableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_observable(data); }
    }
    pub fn remove_observable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_observable(); }
    }

    pub fn add_obscurable(&mut self, id: EntityId, data: ObscurableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_obscurable(data); }
    }
    pub fn remove_obscurable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_obscurable(); }
    }

    pub fn add_attached(&mut self, id: EntityId, data: AttachedData) {
        let new_target = data.target;
        // Snapshot the previous target (if any) so we can detach from it.
        let old_target = self.attached_to.get(&id).copied();
        if let Some(e) = self.entities.get_mut(&id) { e.set_attached(data); }
        // Reverse-index maintenance: keep `attached_to` (child→target) and
        // `attachments` (target→children) consistent with the trait data.
        if old_target != Some(new_target) {
            if let Some(old) = old_target {
                if let Some(v) = self.attachments.get_mut(&old) {
                    v.retain(|e| *e != id);
                }
            }
            self.attached_to.insert(id, new_target);
            let v = self.attachments.entry(new_target).or_default();
            if !v.contains(&id) {
                v.push(id);
            }
        }
    }
    pub fn remove_attached(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_attached(); }
        // Reverse-index maintenance: drop the forward pointer and pull `id`
        // out of the previous target's children list.
        if let Some(old) = self.attached_to.remove(&id) {
            if let Some(v) = self.attachments.get_mut(&old) {
                v.retain(|e| *e != id);
            }
        }
    }

    pub fn add_connector(&mut self, id: EntityId, data: ConnectorData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_connector(data); }
    }
    pub fn remove_connector(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_connector(); }
    }

    pub fn add_movable(&mut self, id: EntityId, data: MovableData) {
        if let Some(e) = self.entities.get_mut(&id) { e.set_movable(data); }
    }
    pub fn remove_movable(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_movable(); }
    }

    pub fn add_container(&mut self, id: EntityId, data: ContainerData) {
        // Capture the new contents up front so we can reverse-index them.
        let new_contents = data.contents.clone();
        // Snapshot the previous contents (if any) to spot children that
        // disappeared from this container in this update.
        let old_contents: Vec<EntityId> = self
            .entities
            .get(&id)
            .and_then(|e| e.container())
            .map(|c| c.contents.clone())
            .unwrap_or_default();
        if let Some(e) = self.entities.get_mut(&id) { e.set_container(data); }
        // Reverse-index maintenance: `contained_by[child] = parent` for every
        // child now listed; and clear stale entries for children removed.
        for child in &old_contents {
            if !new_contents.contains(child) {
                // Only clear if we still own the back-pointer — another
                // container may have already claimed this child.
                if self.contained_by.get(child) == Some(&id) {
                    self.contained_by.remove(child);
                }
            }
        }
        for child in &new_contents {
            self.contained_by.insert(*child, id);
        }
    }
    pub fn remove_container(&mut self, id: EntityId) {
        // Snapshot contents BEFORE clearing so we can purge `contained_by`.
        let old_contents: Vec<EntityId> = self
            .entities
            .get(&id)
            .and_then(|e| e.container())
            .map(|c| c.contents.clone())
            .unwrap_or_default();
        if let Some(e) = self.entities.get_mut(&id) { e.clear_container(); }
        for child in &old_contents {
            if self.contained_by.get(child) == Some(&id) {
                self.contained_by.remove(child);
            }
        }
    }

    pub fn add_narrated(&mut self, id: EntityId, data: NarratedData) {
        // Narrated has no trait bit (open-ended fallback).
        if let Some(e) = self.entities.get_mut(&id) { e.set_narrated(data); }
    }
    pub fn remove_narrated(&mut self, id: EntityId) {
        if let Some(e) = self.entities.get_mut(&id) { e.clear_narrated(); }
    }

    /// OR a set of trait bits into the entity's `cached_traits`. Used by bare
    /// `TraitAdded` events that flip a bit without inserting any new trait
    /// data (the data is assumed to be inserted via a separate path or
    /// already present).
    pub fn add_trait_bits(&mut self, id: EntityId, bits: TraitSet) {
        if let Some(e) = self.entities.get_mut(&id) { e.cached_traits |= bits; }
    }
    /// AND-NOT a set of trait bits out of the entity's `cached_traits`.
    pub fn remove_trait_bits(&mut self, id: EntityId, bits: TraitSet) {
        if let Some(e) = self.entities.get_mut(&id) { e.cached_traits.remove(bits); }
    }

    /// Add a tag to an entity. `Entity.tags` is the authoritative store.
    pub fn add_tag(&mut self, id: EntityId, tag: &str) {
        if let Some(e) = self.entities.get_mut(&id) { e.tags.add(tag); }
    }
    /// Remove a tag from an entity. `Entity.tags` is the authoritative store.
    pub fn remove_tag(&mut self, id: EntityId, tag: &str) {
        if let Some(e) = self.entities.get_mut(&id) { e.tags.remove(tag); }
    }

    /// Convenience: does the entity have the given tag?
    pub fn entity_has_tag(&self, id: EntityId, tag: &str) -> bool {
        self.entities.get(&id).map(|e| e.tags.has(tag)).unwrap_or(false)
    }

    // ---------------------------------------------------------------------------
    // Debug invariant verification (R03)
    // ---------------------------------------------------------------------------

    /// Verify that every alive entity's `cached_traits` matches the bitfield
    /// derived from its `Option<...Data>` fields. Called after every
    /// successful `propose()` in debug builds. No-op (compiled out) in
    /// release.
    #[cfg(debug_assertions)]
    pub fn verify_invariant(&self) {
        for (&id, e) in &self.entities {
            let derived = e.derive_traits();
            assert_eq!(
                e.cached_traits, derived,
                "Entity {:?}: cached_traits {:?} != derive_traits() {:?} (diff {:?})",
                id, e.cached_traits, derived, e.cached_traits ^ derived
            );

            // Cross-trait invariant: FEATURED implies NAMED. Sidebar entries
            // must carry display data — handlers and renderers rely on it.
            if e.cached_traits.contains(TraitSet::FEATURED) {
                assert!(
                    e.cached_traits.contains(TraitSet::NAMED),
                    "FEATURED-implies-NAMED invariant violation for entity {:?}: \
                     entity has FEATURED but not NAMED. Add NamedData via \
                     `.named(NamedData::new(\"...\"))` or `store.add_named(...)`.",
                    id
                );
            }
        }
    }

    #[cfg(not(debug_assertions))]
    #[inline]
    pub fn verify_invariant(&self) {}

    /// Apply a TraitChangeSet to an entity (used during Morphed events).
    pub fn apply_trait_changes(&mut self, id: EntityId, changes: &TraitChangeSet) {
        // Apply set_X fields (add trait data + set bit)
        if let Some(data) = changes.set_named.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_named(data); }
        }
        if let Some(data) = changes.set_sustained {
            if let Some(e) = self.entities.get_mut(&id) { e.set_sustained(data); }
        }
        if let Some(data) = changes.set_temporal {
            if let Some(e) = self.entities.get_mut(&id) { e.set_temporal(data); }
        }
        if let Some(data) = changes.set_ranged.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_ranged(data); }
        }
        if let Some(data) = changes.set_areal {
            if let Some(e) = self.entities.get_mut(&id) { e.set_areal(data); }
        }
        if let Some(data) = changes.set_featured.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_featured(data); }
        }
        if let Some(data) = changes.set_observable.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_observable(data); }
        }
        if let Some(data) = changes.set_ownable.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_ownable(data); }
        }
        if let Some(data) = changes.set_tangible {
            if let Some(e) = self.entities.get_mut(&id) { e.set_tangible(data); }
        }
        if let Some(data) = changes.set_movable.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_movable(data); }
        }
        if let Some(data) = changes.set_quantified.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_quantified(data); }
        }
        if let Some(data) = changes.set_activatable.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_activatable(data); }
        }
        if let Some(data) = changes.set_morphable.clone() {
            if let Some(e) = self.entities.get_mut(&id) { e.set_morphable(data); }
        }

        // Apply remove_X fields (clear bit + remove data)
        if changes.remove_named {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_named(); }
        }
        if changes.remove_sustained {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_sustained(); }
        }
        if changes.remove_temporal {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_temporal(); }
        }
        if changes.remove_ranged {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_ranged(); }
        }
        if changes.remove_areal {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_areal(); }
        }
        if changes.remove_featured {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_featured(); }
        }
        if changes.remove_morphable {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_morphable(); }
        }
        if changes.remove_activatable {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_activatable(); }
        }
        if changes.remove_movable {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_movable(); }
        }
        if changes.remove_observable {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_observable(); }
        }
        if changes.remove_tangible {
            if let Some(e) = self.entities.get_mut(&id) { e.clear_tangible(); }
        }
    }
}

// ---------------------------------------------------------------------------
// EntityBuilder — fluent API
// ---------------------------------------------------------------------------

pub struct EntityBuilder<'s> {
    store: &'s mut EntityState,
    pub id: EntityId,
}

impl<'s> EntityBuilder<'s> {
    /// Add display data (name/icon/description) to this entity. `Featured`
    /// requires `Named`, so call this before `.featured(...)` for sidebar
    /// entries.
    pub fn named(self, data: NamedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_named(data); }
        self
    }

    /// Mark this entity as a sustained effect (concentration, performance,
    /// etc.). The host enforces mutual exclusivity within a `(group, owner)`
    /// pair. See [`SustainedData`].
    pub fn sustained(self, data: SustainedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_sustained(data); }
        self
    }

    /// Give this entity a finite duration. The host automatically decrements
    /// `remaining` at the matching [`TickTrigger`] and synthesizes
    /// `EntityDestroyed` when it reaches 0. See [`TemporalData`].
    pub fn temporal(self, data: TemporalData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_temporal(data); }
        self
    }

    /// Declare this entity's range constraint. The host validates distance from
    /// the activator to the target on `Activated` events when the entity also
    /// has `Activatable`. See [`RangedData`].
    pub fn ranged(self, data: RangedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_ranged(data); }
        self
    }

    /// Declare this entity's area-of-effect shape. Plugins use
    /// `GameContext::entities_in_area` to enumerate entities inside the
    /// declared shape. See [`ArealData`] / [`AreaShape`].
    pub fn areal(self, data: ArealData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_areal(data); }
        self
    }

    pub fn featured(self, data: FeaturedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_featured(data); }
        self
    }

    /// Place this entity on the board at the given position.
    /// Use `TangibleData::at(Position::Face(FaceId((((row) * 8 + (col)) as u32))))`.
    pub fn tangible(self, data: TangibleData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_tangible(data); }
        self
    }

    pub fn connector(self, data: ConnectorData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_connector(data); }
        self
    }

    pub fn observable(self, data: ObservableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_observable(data); }
        self
    }

    pub fn obscurable(self, data: ObscurableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_obscurable(data); }
        self
    }

    pub fn ownable(self, data: OwnableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_ownable(data); }
        self
    }

    pub fn attached(self, data: AttachedData) -> Self {
        let target = data.target;
        let entity = self.id;
        if let Some(e) = self.store.entities.get_mut(&entity) { e.set_attached(data); }
        // Keep the attached_to / attachments indices in sync with the trait data.
        self.store.attached_to.insert(entity, target);
        self.store.attachments.entry(target).or_default().push(entity);
        self
    }

    pub fn movable(self, data: MovableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_movable(data); }
        self
    }

    pub fn container(self, data: ContainerData) -> Self {
        // Seed the `contained_by` reverse index for any pre-populated contents
        // so callers that build a container with children already inside see
        // consistent reverse lookups.
        let contents = data.contents.clone();
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_container(data); }
        for child in contents {
            self.store.contained_by.insert(child, self.id);
        }
        self
    }

    pub fn quantified(self, data: QuantifiedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_quantified(data); }
        self
    }

    pub fn activatable(self, data: ActivatableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_activatable(data); }
        self
    }

    pub fn morphable(self, data: MorphableData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_morphable(data); }
        self
    }

    pub fn gated(self, data: GatedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_gated(data); }
        self
    }

    pub fn narrated(self, data: NarratedData) -> Self {
        if let Some(e) = self.store.entities.get_mut(&self.id) { e.set_narrated(data); }
        self
    }

    pub fn build(self) -> EntityId {
        self.id
    }
}

// ---------------------------------------------------------------------------
// UC-29 Phase A — per-recipient gate status alongside Arc-shared Activatable.
// ---------------------------------------------------------------------------

/// Per-recipient evaluation result for an `Activatable.when` predicate.
///
/// Lives on `EntityView` (outside the Arc-shared `ActivatableData`) because
/// the predicate's resolved values depend on the recipient seat's perspective.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateStatus {
    /// Predicate evaluates to true for this recipient — action is available.
    Pass,
    /// Predicate evaluates to false. `reason` is the flattened explanation
    /// produced by `maat_sdk_types::predicate::explain::flatten_explain`.
    Fail { reason: String },
}

// ---------------------------------------------------------------------------
// EntityView — per-player filtered snapshot (D14: no TraitSet, no Observable)
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct EntityView {
    pub id: EntityId,
    /// Entity type, included so the client can look up the correct ViewTemplate.
    pub type_id: Option<EntityTypeId>,
    /// Display data — `display_name`, `icon`, `description`. Present whenever the
    /// server allowed visibility. Required for `Featured` entities; optional for
    /// auxiliary entities like spells, conditions, items.
    /// Arc-backed: O(1) clone in entity_view_for (shared across player views).
    pub named:       Option<Arc<NamedData>>,
    /// Sustained-effect grouping — `Some` when the entity is an active sustained
    /// effect bound to its owner. The host enforces `(group, owner)`
    /// exclusivity.
    pub sustained:   Option<SustainedData>,
    /// Finite-duration counter — `Some` when the host is auto-ticking this
    /// entity at its declared `TickTrigger`. Reaches 0 → host synthesizes
    /// `EntityDestroyed`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub temporal:    Option<TemporalData>,
    /// Range constraint for activation — `Some` when this entity carries a
    /// distance budget the host enforces on `Activated` events. The client
    /// uses this to draw a targeting overlay (range ring / valid cells).
    #[cfg_attr(feature = "serde", serde(default))]
    pub ranged:      Option<RangedData>,
    /// Area-of-effect shape — `Some` when this entity covers a spatial
    /// extent. The client uses this to draw an AoE preview; plugins use
    /// `GameContext::entities_in_area` to enumerate covered entities.
    #[cfg_attr(feature = "serde", serde(default))]
    pub areal:       Option<ArealData>,
    pub featured:    Option<FeaturedData>,
    /// Board position — `Some` means entity is on the board at `tangible.position`.
    pub tangible:    Option<TangibleData>,
    /// Arc-backed: O(1) clone in entity_view_for.
    pub connector:   Option<Arc<ConnectorData>>,
    pub observable:  Option<ObservableData>,  // always None (D14) — field present but never populated
    pub ownable:     Option<OwnableData>,
    pub attached:    Option<AttachedData>,
    pub movable:     Option<MovableData>,
    /// Arc-backed: O(1) clone in entity_view_for.
    pub quantified:  Option<Arc<QuantifiedData>>,
    /// Arc-backed: O(1) clone in entity_view_for.
    pub activatable: Option<Arc<ActivatableData>>,
    pub obscurable:  Option<ObscurableData>,
    /// Arc-backed: O(1) clone in entity_view_for.
    pub morphable:   Option<Arc<MorphableData>>,
    /// Arc-backed: O(1) clone in entity_view_for.
    pub container:   Option<Arc<ContainerData>>,
    /// Predicate gate — `Some` when this entity is conditionally selectable
    /// / activatable. The client uses this together with
    /// `Predicate::explain` to grey out unselectable options and surface
    /// replacement candidates.
    #[cfg_attr(feature = "serde", serde(default))]
    pub gated:       Option<GatedData>,
    /// Entity-state tag set — boolean markers like `TAG_KING`. Read by
    /// view-sdk's `EntitySelector::HasTag` predicate. `#[serde(default)]`
    /// keeps old snapshots readable.
    #[cfg_attr(feature = "serde", serde(default))]
    pub tags: TagSet,
    /// UC-29 Phase A — per-recipient gate evaluation for `Activatable.when`.
    /// `None` iff `activatable.is_none()`. `Some(Pass)` ⇔ predicate evaluates
    /// true for the recipient seat. `Some(Fail { reason })` ⇔ predicate is
    /// false; reason is flattened from `predicate.explain(&ctx)`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub gate_status: Option<GateStatus>,
    /// UC-29-E1 — per-recipient drag-source permission. `None` ⇔ entity
    /// has no `Ownable` and is not a drag source from anyone's view.
    /// `Some(Pass)` ⇔ recipient may pick this up. `Some(Fail { reason })`
    /// ⇔ drag should be blocked / advisory per `EnforcementMode`. Reason
    /// flattened from the default-drag-predicate explain output.
    #[cfg_attr(feature = "serde", serde(default))]
    pub drag_source_status: Option<GateStatus>,
    /// UC-29-E1 — per-recipient drop-target permission. `None` ⇔ entity
    /// has no `Container` and is not a drop target. Same Pass/Fail
    /// semantics as `drag_source_status`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub drop_target_status: Option<GateStatus>,
}

impl Default for EntityView {
    fn default() -> Self {
        Self {
            id: EntityId::from_raw(0),
            type_id: None,
            named: None,
            sustained: None,
            temporal: None,
            ranged: None,
            areal: None,
            featured: None,
            tangible: None,
            connector: None,
            observable: None,
            ownable: None,
            attached: None,
            movable: None,
            quantified: None,
            activatable: None,
            obscurable: None,
            morphable: None,
            container: None,
            gated: None,
            tags: TagSet::new(),
            gate_status: None,
            drag_source_status: None,
            drop_target_status: None,
        }
    }
}

// 0229-fu1 — `Cell<u64>` serde via plain `u64`. Required because
// `EntityState.next_id` is a `Cell<u64>` (interior mutability for
// the id allocator) but `Cell` doesn't derive serde.
#[cfg(feature = "serde")]
mod cell_u64_serde {
    use std::cell::Cell;
    use serde::{Deserialize, Deserializer, Serializer};

    pub fn serialize<S: Serializer>(c: &Cell<u64>, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_u64(c.get())
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Cell<u64>, D::Error> {
        Ok(Cell::new(u64::deserialize(d)?))
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// R03 regression tests — EntityState invariant verification
// ─────────────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verify_invariant_passes_for_empty_store() {
        let store = EntityState::new();
        store.verify_invariant(); // must not panic
    }

    // -------------------------------------------------------------------
    // Task #224 - encapsulation of `Entity.cached_traits`.
    // The setters are the only public path that mutates the bitfield
    // alongside the data; they MUST keep them in lockstep.
    // -------------------------------------------------------------------

    #[test]
    fn setter_atomically_sets_bit_and_data() {
        let mut e = Entity::new(EntityId::from_raw(1), EntityTypeId::from_raw(2));
        e.set_named(NamedData::new("foo"));
        assert!(e.has_trait(TraitSet::NAMED));
        assert_eq!(e.named().map(|n| n.display_name.as_str()), Some("foo"));
        // Mini-store wrapper just to call verify_invariant.
        let mut store = EntityState::new();
        store.alive.insert(e.id);
        store.entities.insert(e.id, e);
        store.verify_invariant(); // confirms parity
    }

    #[test]
    fn clearer_atomically_clears_bit_and_data() {
        let mut e = Entity::new(EntityId::from_raw(1), EntityTypeId::from_raw(2));
        e.set_named(NamedData::new("foo"));
        e.clear_named();
        assert!(!e.has_trait(TraitSet::NAMED));
        assert!(e.named().is_none());
        let mut store = EntityState::new();
        store.alive.insert(e.id);
        store.entities.insert(e.id, e);
        store.verify_invariant();
    }

    #[test]
    fn cached_traits_is_not_publicly_writable() {
        // Compile-time assertion: this test exists to document that direct
        // write is now impossible. We don't try to write -- that wouldn't
        // compile.  Reading by value is fine.
        let e = Entity::new(EntityId::from_raw(1), EntityTypeId::from_raw(2));
        let _read_ok: TraitSet = e.traits();
        // e.cached_traits = TraitSet::NAMED;   // compile error if uncommented
        // e.named = Some(NamedData::new("x")); // compile error if uncommented
    }

    #[test]
    fn traits_returns_cached_bitfield_in_lockstep() {
        let mut e = Entity::new(EntityId::from_raw(1), EntityTypeId::from_raw(2));
        assert!(e.traits().is_empty());
        e.set_named(NamedData::new("hero"));
        assert_eq!(e.traits(), TraitSet::NAMED);
        e.set_tangible(TangibleData::at(Position::Face(FaceId(0))));
        assert_eq!(e.traits(), TraitSet::NAMED | TraitSet::TANGIBLE);
        e.clear_named();
        assert_eq!(e.traits(), TraitSet::TANGIBLE);
    }

    #[test]
    fn mut_accessor_does_not_change_trait_presence() {
        let mut e = Entity::new(EntityId::from_raw(1), EntityTypeId::from_raw(2));
        e.set_named(NamedData::new("foo"));
        let before = e.traits();
        if let Some(n) = e.named_mut() {
            n.display_name = "bar".into();
        }
        assert_eq!(e.traits(), before);
        assert_eq!(e.named().map(|n| n.display_name.as_str()), Some("bar"));
    }

    #[test]
    fn entity_tags_are_authoritative() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(99)).build();
        store.add_tag(id, "marked");
        // Authoritative store: Entity.tags
        assert!(store.entity(id).unwrap().tags.has("marked"));
        // Convenience helper agrees
        assert!(store.entity_has_tag(id, "marked"));
        // Removal clears it from the authoritative location
        store.remove_tag(id, "marked");
        assert!(!store.entity(id).unwrap().tags.has("marked"));
        assert!(!store.entity_has_tag(id, "marked"));
    }

    #[test]
    fn entity_destroy_drops_tags() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7)).build();
        store.add_tag(id, "doomed");
        assert!(store.entity_has_tag(id, "doomed"));
        store.destroy(id);
        // After destroy the entity is gone, so the tag query returns false.
        assert!(!store.entity_has_tag(id, "doomed"));
        assert!(store.entity(id).is_none());
    }

    #[test]
    fn entity_has_tag_returns_false_for_unknown_entity() {
        let store = EntityState::new();
        assert!(!store.entity_has_tag(EntityId::from_raw(9999), "anything"));
    }

    #[test]
    fn verify_invariant_passes_for_entity_built_via_builder() {
        let mut store = EntityState::new();
        store.spawn(EntityTypeId::from_raw(1))
            .tangible(TangibleData::at(Position::Face(FaceId(0))))
            .build();
        store.verify_invariant();
    }

    #[test]
    fn verify_invariant_passes_for_multi_trait_entity() {
        let mut store = EntityState::new();
        store.spawn(EntityTypeId::from_raw(2))
            .tangible(TangibleData::at(Position::Face(FaceId(27))))
            .movable(MovableData::new(MovementType::Step))
            .ownable(OwnableData::owned_by(SeatId::from_raw(1)))
            .build();
        store.verify_invariant();
    }

    // Bypass setters by mutating the Entity field directly without updating
    // cached_traits. verify_invariant must detect: cached=empty, derive=TANGIBLE.
    #[cfg(debug_assertions)]
    #[test]
    #[should_panic(expected = "cached_traits")]
    fn verify_invariant_detects_data_inserted_without_bit_set() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(99)).build();
        // Bypass the setter — directly assign the field through the
        // test-only escape hatch, leaving cached_traits stale.
        let e = store.entities.get_mut(&id).unwrap();
        e.debug_force_set_field_no_bit_tangible(TangibleData::at(Position::Face(FaceId(0))));
        // cached_traits was NOT updated.
        store.verify_invariant();
    }

    // Set a bit in cached_traits without inserting the corresponding trait data.
    // verify_invariant must detect the mismatch.
    #[cfg(debug_assertions)]
    #[test]
    #[should_panic(expected = "cached_traits")]
    fn verify_invariant_detects_stale_bit_without_data() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(99)).build();
        // Force a bit in cached_traits without inserting data.
        store.entities.get_mut(&id).unwrap().debug_force_set_bit_no_field(TraitSet::TANGIBLE);
        store.verify_invariant();
    }

    // ─────────────────────────────────────────────────────────────────────
    // S01: Named trait — display data without sidebar status
    // ─────────────────────────────────────────────────────────────────────

    #[test]
    fn named_without_featured_carries_display_data_but_not_in_sidebar_query() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7000))
            .named(NamedData::new("Fireball").with_description("3rd-level evocation"))
            .build();
        store.verify_invariant();

        // Display data is reachable.
        let e = store.entity(id).unwrap();
        assert_eq!(e.named().unwrap().display_name, "Fireball");
        assert_eq!(e.named().unwrap().description, "3rd-level evocation");

        // Sidebar query (FEATURED) does NOT include this entity.
        let featured: Vec<EntityId> = store.query_ids(TraitSet::FEATURED).collect();
        assert!(!featured.contains(&id), "spell should not be featured");

        // But cross-system query for "anything renderable" (NAMED) does.
        let renderable: Vec<EntityId> = store.query_ids(TraitSet::NAMED).collect();
        assert!(renderable.contains(&id), "named entities are renderable");
    }

    #[test]
    fn featured_plus_named_appears_in_sidebar_and_carries_display_data() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7001))
            .named(NamedData::new("Hero"))
            .featured(FeaturedData::new())
            .build();
        store.verify_invariant();

        assert_eq!(store.entity(id).unwrap().named().unwrap().display_name, "Hero");
        let featured: Vec<EntityId> = store.query_ids(TraitSet::FEATURED).collect();
        assert!(featured.contains(&id), "hero is a sidebar entry");
    }

    #[cfg(debug_assertions)]
    #[test]
    #[should_panic(expected = "FEATURED-implies-NAMED invariant violation")]
    fn featured_without_named_panics_in_debug_invariant() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7002)).build();
        // Bypass the builder — directly install Featured without Named.
        store.entities.get_mut(&id).unwrap().set_featured(FeaturedData::new());
        store.verify_invariant(); // must panic
    }

    #[test]
    fn named_and_attached_query_finds_conditions_with_display_data() {
        let mut store = EntityState::new();
        // The "host" entity that conditions attach to.
        let host = store.spawn(EntityTypeId::from_raw(7100)).build();
        // A condition: Named + Attached (display data + relationship). Not Featured.
        let cond = store.spawn(EntityTypeId::from_raw(7101))
            .named(NamedData::new("Poisoned").with_icon("status:poison"))
            .attached(AttachedData { target: host, stacks: false })
            .build();
        store.verify_invariant();

        // Cross-system query: "every entity that is both Named and Attached"
        // — i.e. every condition-shaped thing with display data.
        let conditions: Vec<EntityId> = store
            .query_ids(TraitSet::NAMED | TraitSet::ATTACHED)
            .collect();
        assert!(conditions.contains(&cond));
        assert!(!conditions.contains(&host), "host has neither trait");
    }

    #[test]
    fn entity_view_default_has_no_named_or_featured() {
        let v = EntityView::default();
        assert!(v.named.is_none());
        assert!(v.featured.is_none());
    }

    #[test]
    fn add_named_remove_named_round_trip_keeps_invariant() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7200)).build();

        store.add_named(id, NamedData::new("Sword"));
        store.verify_invariant();
        assert!(store.has_traits(id, TraitSet::NAMED));

        store.remove_named(id);
        store.verify_invariant();
        assert!(!store.has_traits(id, TraitSet::NAMED));
    }

    // ─────────────────────────────────────────────────────────────────────
    // S06: Sustained trait — store-level round-trip and bitfield sync.
    // The `(group, owner)` exclusivity invariant is enforced by the engine
    // pipeline; see core/engine tests for those scenarios.
    // ─────────────────────────────────────────────────────────────────────

    #[test]
    fn add_sustained_remove_sustained_round_trip_keeps_invariant() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7300)).build();

        store.add_sustained(id, SustainedData::new(CONCENTRATION));
        store.verify_invariant();
        assert!(store.has_traits(id, TraitSet::SUSTAINED));
        assert_eq!(store.entity(id).unwrap().sustained().unwrap().group, CONCENTRATION);

        store.remove_sustained(id);
        store.verify_invariant();
        assert!(!store.has_traits(id, TraitSet::SUSTAINED));
        assert!(store.entity(id).unwrap().sustained().is_none());
    }

    #[test]
    fn sustained_builder_sets_bit_and_data() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(7301))
            .named(NamedData::new("Hold Person"))
            .sustained(SustainedData::new(CONCENTRATION))
            .build();
        store.verify_invariant();
        assert!(store.has_traits(id, TraitSet::SUSTAINED));
        assert!(store.has_traits(id, TraitSet::NAMED));
        assert_eq!(store.entity(id).unwrap().sustained().unwrap().group, CONCENTRATION);
    }

    // ─────────────────────────────────────────────────────────────────────
    // Phase-4 TraitSet cache removal: Entity struct is the single source of
    // truth — no per-trait HashMaps remain.
    // ─────────────────────────────────────────────────────────────────────

    #[test]
    fn entity_record_holds_trait_data_after_builder() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(8000))
            .named(NamedData::new("Pawn"))
            .featured(FeaturedData::new())
            .ownable(OwnableData::owned_by(SeatId::from_raw(1)))
            .tangible(TangibleData::at(Position::Face(FaceId(0))))
            .build();

        let e = store.entity(id).expect("entity exists");
        assert_eq!(e.id, id);
        assert_eq!(e.type_id, EntityTypeId::from_raw(8000));
        assert!(e.named().is_some());
        assert!(e.featured().is_some());
        assert!(e.ownable().is_some());
        assert!(e.tangible().is_some());
        // Cached bitfield matches `derive_traits()`.
        let derived = e.derive_traits();
        assert_eq!(e.trait_set(), derived);
        assert!(e.has_traits(TraitSet::NAMED | TraitSet::TANGIBLE | TraitSet::OWNABLE));
        store.verify_invariant();
    }

    #[test]
    fn entity_setter_keeps_cached_traits_in_lockstep() {
        let mut e = Entity::new(EntityId::from_raw(42), EntityTypeId::from_raw(1));
        assert_eq!(e.trait_set(), TraitSet::empty());
        e.set_named(NamedData::new("X"));
        assert!(e.has_traits(TraitSet::NAMED));
        assert_eq!(e.trait_set(), e.derive_traits());
        e.clear_named();
        assert!(!e.has_traits(TraitSet::NAMED));
        assert_eq!(e.trait_set(), TraitSet::empty());
    }

    #[test]
    fn entity_record_removed_on_destroy() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(8001)).build();
        assert!(store.entity(id).is_some());
        store.destroy(id);
        assert!(store.entity(id).is_none());
    }

    #[test]
    fn add_remove_through_helper_keeps_invariant() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(8002)).build();
        store.add_named(id, NamedData::new("Bat"));
        store.add_quantified(id, QuantifiedData::new());
        store.verify_invariant();
        assert!(store.entity(id).unwrap().named().is_some());
        assert!(store.entity(id).unwrap().quantified().is_some());

        store.remove_quantified(id);
        store.verify_invariant();
        assert!(store.entity(id).unwrap().quantified().is_none());
    }

    #[test]
    fn sustain_groups_compare_by_slug() {
        assert_eq!(CONCENTRATION, SustainGroup("concentration"));
        assert_ne!(CONCENTRATION, PERFORMANCE);
        // Distinct slugs hash distinctly.
        let a = std::collections::HashSet::from([CONCENTRATION, PERFORMANCE]);
        assert_eq!(a.len(), 2);
    }

    #[test]
    fn store_spawn_with_gated_sets_bit_and_stores_data() {
        use crate::predicate::Predicate;
        let mut store = EntityState::new();
        let id = store
            .spawn(EntityTypeId::from_raw(9001))
            .gated(GatedData::when(Predicate::All(vec![])))
            .build();
        let e = store.entity(id).unwrap();
        assert!(e.gated().is_some());
        assert!(e.has_traits(TraitSet::GATED));
        assert_eq!(e.trait_set(), e.derive_traits());
        store.verify_invariant();
    }

    // ─────────────────────────────────────────────────────────────────────
    // Reverse-index coverage: contained_by + attached_to/attachments stay in
    // lockstep regardless of which mutation path produced the change.
    // ─────────────────────────────────────────────────────────────────────

    #[test]
    fn attached_children_populates_via_add_attached() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(10_001)).build();
        let child = store.spawn(EntityTypeId::from_raw(10_002)).build();
        store.add_attached(child, AttachedData { target: parent, stacks: false });
        assert_eq!(store.attached_children(parent), &[child]);
        assert_eq!(store.attached_to.get(&child), Some(&parent));
    }

    #[test]
    fn attached_children_populates_via_builder() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(10_010)).build();
        let child = store
            .spawn(EntityTypeId::from_raw(10_011))
            .attached(AttachedData { target: parent, stacks: false })
            .build();
        assert_eq!(store.attached_children(parent), &[child]);
    }

    #[test]
    fn attached_children_clears_on_destroy() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(10_020)).build();
        let child = store.spawn(EntityTypeId::from_raw(10_021)).build();
        store.add_attached(child, AttachedData { target: parent, stacks: false });
        assert_eq!(store.attached_children(parent), &[child]);
        store.destroy(child);
        assert!(store.attached_children(parent).is_empty());
        assert!(!store.attached_to.contains_key(&child));
    }

    #[test]
    fn attached_children_clears_on_remove_attached() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(10_030)).build();
        let child = store.spawn(EntityTypeId::from_raw(10_031)).build();
        store.add_attached(child, AttachedData { target: parent, stacks: false });
        store.remove_attached(child);
        assert!(store.attached_children(parent).is_empty());
        assert!(!store.attached_to.contains_key(&child));
    }

    #[test]
    fn add_attached_retargets_updates_both_lists() {
        let mut store = EntityState::new();
        let p1 = store.spawn(EntityTypeId::from_raw(10_040)).build();
        let p2 = store.spawn(EntityTypeId::from_raw(10_041)).build();
        let child = store.spawn(EntityTypeId::from_raw(10_042)).build();
        store.add_attached(child, AttachedData { target: p1, stacks: false });
        // Retarget to p2 — the old parent's list should be cleared.
        store.add_attached(child, AttachedData { target: p2, stacks: false });
        assert!(store.attached_children(p1).is_empty());
        assert_eq!(store.attached_children(p2), &[child]);
        assert_eq!(store.attached_to.get(&child), Some(&p2));
    }

    #[test]
    fn multiple_attached_children_keep_insertion_order() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(10_050)).build();
        let c1 = store.spawn(EntityTypeId::from_raw(10_051)).build();
        let c2 = store.spawn(EntityTypeId::from_raw(10_052)).build();
        let c3 = store.spawn(EntityTypeId::from_raw(10_053)).build();
        store.add_attached(c1, AttachedData { target: parent, stacks: false });
        store.add_attached(c2, AttachedData { target: parent, stacks: false });
        store.add_attached(c3, AttachedData { target: parent, stacks: false });
        assert_eq!(store.attached_children(parent), &[c1, c2, c3]);
    }

    #[test]
    fn container_of_populates_via_add_container() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(20_001)).build();
        let child = store.spawn(EntityTypeId::from_raw(20_002)).build();
        let mut data = ContainerData::new(ContainerType::Bag);
        data.contents.push(child);
        store.add_container(parent, data);
        assert_eq!(store.container_of(child), Some(parent));
    }

    #[test]
    fn container_of_populates_via_builder() {
        let mut store = EntityState::new();
        // Pre-spawn the child so we can pass its id into the builder.
        let child = store.spawn(EntityTypeId::from_raw(20_010)).build();
        let mut data = ContainerData::new(ContainerType::Bag);
        data.contents.push(child);
        let parent = store
            .spawn(EntityTypeId::from_raw(20_011))
            .container(data)
            .build();
        assert_eq!(store.container_of(child), Some(parent));
    }

    #[test]
    fn container_of_clears_when_child_removed_from_contents() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(20_020)).build();
        let child = store.spawn(EntityTypeId::from_raw(20_021)).build();
        let mut data = ContainerData::new(ContainerType::Bag);
        data.contents.push(child);
        store.add_container(parent, data);
        assert_eq!(store.container_of(child), Some(parent));
        // Now mutate: empty the container via add_container with no contents.
        store.add_container(parent, ContainerData::new(ContainerType::Bag));
        assert!(store.container_of(child).is_none());
    }

    #[test]
    fn container_of_clears_on_remove_container() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(20_030)).build();
        let child = store.spawn(EntityTypeId::from_raw(20_031)).build();
        let mut data = ContainerData::new(ContainerType::Bag);
        data.contents.push(child);
        store.add_container(parent, data);
        store.remove_container(parent);
        assert!(store.container_of(child).is_none());
    }

    #[test]
    fn container_of_clears_on_child_destroy() {
        let mut store = EntityState::new();
        let parent = store.spawn(EntityTypeId::from_raw(20_040)).build();
        let child = store.spawn(EntityTypeId::from_raw(20_041)).build();
        let mut data = ContainerData::new(ContainerType::Bag);
        data.contents.push(child);
        store.add_container(parent, data);
        store.destroy(child);
        assert!(store.container_of(child).is_none());
        // The parent's contents Vec also drops the child via destroy().
        assert!(store.contents_of(parent).is_empty());
    }

    #[test]
    fn add_remove_gated_through_helper_keeps_invariant() {
        use crate::predicate::Predicate;
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(9002)).build();
        store.add_gated(id, GatedData::when(Predicate::All(vec![])));
        store.verify_invariant();
        assert!(store.entity(id).unwrap().has_traits(TraitSet::GATED));
        store.remove_gated(id);
        store.verify_invariant();
        assert!(!store.entity(id).unwrap().has_traits(TraitSet::GATED));
    }
}
