//! Predicate IR types.
//!
//! See `core/sdk/src/predicate/mod.rs` for module-level docs.

use crate::{EntityId, EntityTypeId, PoolId};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Compositional boolean expression over the trait surface.
///
/// Engine primitives only — no system-specific (TTRPG/board-game) vocabulary
/// appears in any variant name. System-specific shorthand belongs in
/// plugin-side macros that lower to this IR.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Predicate {
    /// Conjunction. Empty vec → true.
    All(Vec<Predicate>),
    /// Disjunction. Empty vec → false.
    Any(Vec<Predicate>),
    /// Negation.
    Not(Box<Predicate>),
    /// True iff the [`EntityRef`] resolves to a concrete [`EntityId`].
    Exists(EntityRef),
    /// "There exists an entity in `set` such that `body` holds." Inside `body`,
    /// [`EntityRef::It`] refers to the iterator's current element.
    AnyOf(EntitySet, Box<Predicate>),
    /// "For every entity in `set`, `body` holds." Vacuously true on empty sets.
    AllOf(EntitySet, Box<Predicate>),
    /// Two-value comparison.
    Cmp(Value, Op, Value),
    /// Two-`EntityRef` identity comparison. True iff both refs resolve
    /// to the same concrete `EntityId`. False if either ref fails to
    /// resolve. Used by the `is_self()` / `is_observer()` sugar atoms
    /// in the `gate!` macro, and authorable directly for general
    /// "candidate is this actor" tests.
    RefEq(EntityRef, EntityRef),
    /// WI1-FU3: `true` iff `target` is visible to the seat owning
    /// `observer`. Resolved via [`super::eval::EvalContext::visible`];
    /// the engine implementation derives the observer's seat via
    /// `OwnableData.owner` and bridges into
    /// `crate::queries::VisionQuery::can_see`. Sugared by
    /// `gate! { visible_to(<observer_eref>) }` with the implicit
    /// target tracked lexically (It inside set quantifiers, Target
    /// outside).
    VisibleTo { observer: EntityRef, target: EntityRef },

    /// UC-29-E1: `true` iff the recipient seat (observer) is the
    /// owning seat of `EntityRef::Self_`. False when Self_ has no
    /// Ownable, has a non-Seat owner (Team/Neutral), or there is no
    /// observer seat. Generic ownership atom — used by the engine's
    /// default drag/drop gate predicates, and authorable directly
    /// for plugin-supplied permission checks.
    OwnerSeatMatchesObserver,

    /// UC-29-E1: `true` iff the recipient seat has the given
    /// `SeatRole` in the session config. False when there is no
    /// observer seat. Defaults via `SessionConfig::role_for` apply
    /// (unmapped seat → Player). Used by the default drag/drop
    /// predicates to grant GM / Sysadmin bypass.
    ObserverHasRole(crate::config::SeatRole),
}

// Manual `ToSchema` impl. We can't `#[derive(utoipa::ToSchema)]` on
// `Predicate` because:
//   1. The enum is genuinely recursive (`Vec<Predicate>` /
//      `Box<Predicate>`).
//   2. utoipa 5.4's `#[schema(no_recursion)]` is not supported on
//      tuple-variant enum fields, so we cannot suppress the recursion
//      with attributes.
//   3. Without recursion suppression utoipa's schema walker enters
//      unbounded recursion at spec-build time and overflows the stack —
//      empirically still overflows on a 256 MB Windows-debug thread —
//      so any route that transitively references `Predicate` (e.g.
//      `GatedCatalogEntry.gated: Option<Predicate>`) cannot serve
//      `/api/openapi.json` at all.
//
// We keep the wire shape identical (serde produces externally-tagged
// enum JSON: `{ "All": [..] }`, `{ "Cmp": [..,..,..] }`, etc.) and
// model the schema as a free-form object — terminating the recursion
// at this type. Clients that want a tighter contract can document the
// shape out-of-band; OpenAPI is aware that this is an opaque
// self-referential structure.
//
// Switch back to `#[derive(ToSchema)]` once utoipa supports
// `no_recursion` on tuple-variant enum fields (track utoipa-rs/utoipa).
#[cfg(feature = "schema")]
impl utoipa::PartialSchema for Predicate {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        use utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
        ObjectBuilder::new()
            .schema_type(SchemaType::Type(Type::Object))
            .description(Some(
                "Predicate IR (recursive). Externally tagged JSON: \
                 `{\"All\":[..]}`, `{\"Any\":[..]}`, `{\"Not\":<predicate>}`, \
                 `{\"Exists\":<EntityRef>}`, `{\"AnyOf\":[<EntitySet>,<predicate>]}`, \
                 `{\"AllOf\":[<EntitySet>,<predicate>]}`, \
                 `{\"Cmp\":[<Value>,<Op>,<Value>]}`, `{\"RefEq\":[<EntityRef>,<EntityRef>]}`, \
                 `{\"VisibleTo\":{\"observer\":<EntityRef>,\"target\":<EntityRef>}}`. \
                 Schema is opaque-object because utoipa 5.4 does not support \
                 `no_recursion` on tuple-variant enum fields and the auto- \
                 derived schema walker overflows on this recursive type."
                    .to_string(),
            ))
            .build()
            .into()
    }
}

#[cfg(feature = "schema")]
impl utoipa::ToSchema for Predicate {
    fn name() -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed("Predicate")
    }
}

/// Values that flow into a [`Predicate::Cmp`].
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Value {
    /// Integer literal.
    Const(i64),
    /// EntityId literal (typically a plugin-declared constant).
    ConstId(EntityId),
    /// EntityTypeId literal.
    ConstType(EntityTypeId),
    /// EntityTypeId of the entity `entity` resolves to. NULL if unresolved
    /// (any [`Predicate::Cmp`] involving a NULL value is false).
    TypeOf(EntityRef),
    /// Pool reading, base or effective.
    Pool {
        entity: EntityRef,
        key: PoolId,
        mode: PoolMode,
    },
    /// Reads a session setting by typed key. Returns the setting's `i64`
    /// representation (booleans are stored as 0/1; missing settings → NULL,
    /// which makes the enclosing [`Predicate::Cmp`] false — same as any
    /// other unresolved value).
    Setting(SettingKey),
    /// Returns 1 if the named source/plugin/sourcebook is active in this
    /// evaluation context, 0 otherwise. Missing sources resolve to 0
    /// (i.e. always defined, never NULL — composes cleanly inside `All`/`Any`).
    SourceLoaded(SourceKey),
    /// Distance, in cells, between two entities, evaluated under the given
    /// [`MetricKind`]. Engine-side this consults a precomputed reachability
    /// cache via [`crate::predicate::eval::EvalContext::distance`]; client-side
    /// it consults the snapshot's `ReachabilityFrame`. Either ref unresolved
    /// → NULL → enclosing [`Predicate::Cmp`] is false.
    Distance {
        from: EntityRef,
        to: EntityRef,
        kind: MetricKind,
    },
}

/// How distance between two entities should be measured.
///
/// `Unlimited` ignores blockers entirely (raw graph distance). `MotionLimited`
/// respects movement-blocking edges (walls, closed doors). `VisionLimited`
/// respects vision-blocking edges (smoke, fog, opaque walls). The engine's
/// reachability presenter pre-computes one BFS per kind per source entity;
/// `EvalContext::distance` is an O(1) lookup against that cache.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum MetricKind {
    /// Raw graph distance, ignoring all blockers.
    Unlimited,
    /// Distance respecting `motion_blocked` edges.
    MotionLimited,
    /// Distance respecting `vision_blocked` edges.
    VisionLimited,
}

/// Typed key for session settings (rule toggles, sourcebook gating thresholds,
/// engine-wide policy flags). Plugin-namespaced; the convention is
/// `"{plugin}:{...}"` — e.g. `"myplugin:rule:my_setting"`,
/// `"core:simultaneous_play"`.
///
/// Equality is by full string content. Use [`SettingKey::new`] to construct.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SettingKey(pub String);

impl SettingKey {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
    pub fn key(&self) -> &str {
        &self.0
    }
}

/// Typed key for content sources / sourcebooks / plugin packs. Plugin-namespaced
/// — e.g. `"myplugin:source_a"`, `"myplugin:source_b"`, `"core"`. Used by
/// [`Value::SourceLoaded`] to gate predicates on whether a particular source
/// is active in the current campaign.
///
/// Equality is by full string content. Use [`SourceKey::new`] to construct.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SourceKey(pub String);

impl SourceKey {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
    pub fn key(&self) -> &str {
        &self.0
    }
}

/// Comparison operator for [`Predicate::Cmp`].
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Op {
    Lt,
    Le,
    Eq,
    Ne,
    Ge,
    Gt,
}

/// Whether a [`Value::Pool`] read returns the base value or the value after
/// modifiers have been applied.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PoolMode {
    Base,
    Effective,
}

/// A set of entities, identified relative to another [`EntityRef`].
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EntitySet {
    /// Entities `Attached` to the resolution of the inner ref.
    AttachedTo(EntityRef),
    /// Entities in `ContainerData.contents` of the inner ref's resolution.
    ContainedIn(EntityRef),
}

/// Indirect reference to an entity, resolved at evaluation time against the
/// active [`crate::predicate::eval::EvalContext`].
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EntityRef {
    /// The entity carrying the predicate.
    ///
    /// Spelled `Self_` because `Self` is a Rust keyword.
    Self_,
    /// Owner of `Self_` via `OwnableData` (NULL if not Ownable).
    Owner,
    /// Viewing player's entity (for visibility-aware predicates).
    Observer,
    /// Activation target (NULL outside activation contexts).
    Target,
    /// Element bound by the enclosing [`Predicate::AnyOf`] or
    /// [`Predicate::AllOf`] (NULL outside such a binder).
    It,
    /// Direct reference, typically a plugin-declared [`EntityId`] constant.
    ById(EntityId),
}
