//! Predicate evaluator.
//!
//! Pure, read-only traversal of a [`Predicate`] against an [`EvalContext`].
//! Engine-side and plugin-test contexts both implement the same trait.
//!
//! See `core/sdk/src/predicate/mod.rs` for module-level docs.

use super::types::{PoolMode, SettingKey, SourceKey, MetricKind, Predicate, EntityRef, EntitySet, Value, Op};
use crate::{EntityId, EntityTypeId, PoolId};

// `MetricKind` re-exported via `super::types::*` glob above.

/// Read-only context for predicate evaluation. Implemented both by the engine
/// (against the live store) and by tests (mock data).
pub trait EvalContext {
    /// The entity carrying the predicate (`EntityRef::Self_`).
    fn resolve_self(&self) -> Option<EntityId>;
    /// `OwnableData.owner`-style lookup. Resolves [`EntityRef::Owner`] when
    /// applied to the current `Self_`.
    fn resolve_owner(&self, of: EntityId) -> Option<EntityId>;
    /// Viewing player's entity (for visibility-aware predicates).
    fn resolve_observer(&self) -> Option<EntityId>;
    /// Activation target — `None` outside an activation context (e.g. during
    /// option enumeration before a target has been chosen).
    fn resolve_target(&self) -> Option<EntityId>;

    /// Type identifier of `e`, if known.
    fn type_of(&self, e: EntityId) -> Option<EntityTypeId>;
    /// Pool reading. Engine-side: base or effective per `mode`.
    fn pool(&self, e: EntityId, key: &PoolId, mode: PoolMode) -> Option<i64>;
    /// Entities `Attached` to `parent`. Empty if none.
    fn attached_to(&self, parent: EntityId) -> Vec<EntityId>;
    /// Entities in `ContainerData.contents` of `container`. Empty if none.
    fn contained_in(&self, container: EntityId) -> Vec<EntityId>;

    /// Read a session setting by typed key. Returns the setting's `i64`
    /// representation (booleans stored as 0/1).
    ///
    /// Default returns `None` so existing implementors compile unchanged
    /// during migration. The engine-side `StoreEvalContext` overrides this
    /// to read `SessionConfig.public_settings`. Mock contexts that need to
    /// test `Value::Setting` should override.
    fn setting(&self, _key: &SettingKey) -> Option<i64> {
        None
    }

    /// Returns true iff the named source/sourcebook/plugin pack is active
    /// in the current campaign.
    ///
    /// Default returns `false`. The engine-side `StoreEvalContext` overrides
    /// this to consult `SessionConfig.active_sources`. Mock contexts that
    /// need to test `Value::SourceLoaded` should override.
    fn source_loaded(&self, _key: &SourceKey) -> bool {
        false
    }

    /// Distance between two entities, in cells, under the given [`MetricKind`].
    ///
    /// Default returns `None` so existing implementors compile unchanged
    /// during migration. UC-04 Phase E wires this through to the engine's
    /// reachability presenter on the server-side `StoreEvalContext` and to
    /// the snapshot's `ReachabilityFrame` on the client-side
    /// `ClientEvalContext`. Mock contexts that need to test
    /// `Value::Distance` should override.
    fn distance(
        &self,
        _from: EntityId,
        _to: EntityId,
        _kind: MetricKind,
    ) -> Option<i64> {
        None
    }

    /// WI1-FU3: `true` iff `target` is visible to the seat that owns
    /// `observer`. Used by [`Predicate::VisibleTo`] and the
    /// `gate! { visible_to(..) }` sugar.
    ///
    /// 0800: no default impl. Mock / test contexts that don't care
    /// about visibility must explicitly return `false` (or whatever
    /// the test fixture needs); engine-side `EngineContext` derives
    /// the seat from `observer.ownable.owner` and delegates to
    /// `VisionQuery::can_see`. Forcing an explicit decision per
    /// Governing Principle #1 ("Automated or Narrated — no silent
    /// failures"): the previous default-`false` silently produced
    /// wrong predicate answers in test contexts that exercised
    /// `Predicate::VisibleTo` without overriding.
    fn visible(&self, observer: EntityId, target: EntityId) -> bool;

    /// UC-29-E1 — the recipient seat for this evaluation, if any.
    /// `None` outside per-recipient view construction (handler-side
    /// `GameContext` invocations have no observer seat). Engine-side
    /// `StoreEvalContext::with_observer_seat(seat)` sets it.
    fn observer_seat(&self) -> Option<crate::types::SeatId> {
        None
    }

    /// UC-29-E1 — role assigned to `seat` in the session. Default
    /// `Player` matches `SessionConfig::role_for`'s fallback.
    fn role_of(&self, _seat: crate::types::SeatId) -> crate::config::SeatRole {
        crate::config::SeatRole::default()
    }

    /// UC-29-E1 — owning seat of `e`, derived from
    /// `OwnableData.owner.as_seat()`. `None` for entities without
    /// Ownable, or with `Owner::Team` / `Owner::Neutral`.
    fn owner_seat_of(&self, _e: EntityId) -> Option<crate::types::SeatId> {
        None
    }
}

impl Predicate {
    /// Evaluate this predicate against `ctx`, returning true/false.
    ///
    /// Pure: no side effects, no mutation visible to the caller. Type-mismatched
    /// [`Predicate::Cmp`] (e.g. comparing an `EntityId` to an `i64`) is `false`,
    /// not a panic. NULL operands likewise produce `false`.
    pub fn evaluate<C: EvalContext>(&self, ctx: &C) -> bool {
        let mut frames = EvalFrames::default();
        eval(self, ctx, &mut frames)
    }
}

/// Internal state of one [`Predicate::evaluate`] call.
///
/// Holds the stack of `It` bindings introduced by enclosing `AnyOf`/`AllOf`
/// frames — innermost binder is at the top.
///
/// Visible to sibling submodules (`explain`) so the explain pass can mirror
/// the evaluator's binding semantics without duplicating the frame plumbing.
#[derive(Default)]
pub(super) struct EvalFrames {
    pub(super) it_stack: Vec<EntityId>,
}

fn eval<C: EvalContext>(p: &Predicate, ctx: &C, fr: &mut EvalFrames) -> bool {
    match p {
        Predicate::All(xs) => xs.iter().all(|q| eval(q, ctx, fr)),
        Predicate::Any(xs) => xs.iter().any(|q| eval(q, ctx, fr)),
        Predicate::Not(q) => !eval(q, ctx, fr),
        Predicate::Exists(r) => resolve_ref(r, ctx, fr).is_some(),
        Predicate::AnyOf(set, body) => {
            let ids = set_resolve(set, ctx, fr);
            ids.into_iter().any(|id| {
                fr.it_stack.push(id);
                let v = eval(body, ctx, fr);
                fr.it_stack.pop();
                v
            })
        }
        Predicate::AllOf(set, body) => {
            let ids = set_resolve(set, ctx, fr);
            ids.into_iter().all(|id| {
                fr.it_stack.push(id);
                let v = eval(body, ctx, fr);
                fr.it_stack.pop();
                v
            })
        }
        Predicate::Cmp(a, op, b) => match (read_value(a, ctx, fr), read_value(b, ctx, fr)) {
            (Some(av), Some(bv)) => apply_op(av, *op, bv),
            _ => false,
        },
        Predicate::RefEq(a, b) => match (resolve_ref(a, ctx, fr), resolve_ref(b, ctx, fr)) {
            (Some(av), Some(bv)) => av == bv,
            // Unresolved refs are NULL; NULL ≠ NULL (matches Cmp semantics).
            _ => false,
        },
        Predicate::VisibleTo { observer, target } => {
            match (resolve_ref(observer, ctx, fr), resolve_ref(target, ctx, fr)) {
                (Some(obs), Some(tgt)) => ctx.visible(obs, tgt),
                _ => false,
            }
        }
        Predicate::OwnerSeatMatchesObserver => {
            let Some(self_id) = ctx.resolve_self() else { return false; };
            let Some(observer) = ctx.observer_seat() else { return false; };
            let Some(owner) = ctx.owner_seat_of(self_id) else { return false; };
            owner == observer
        }
        Predicate::ObserverHasRole(role) => {
            let Some(observer) = ctx.observer_seat() else { return false; };
            ctx.role_of(observer) == *role
        }
    }
}

pub(super) fn resolve_ref<C: EvalContext>(
    r: &EntityRef,
    ctx: &C,
    fr: &EvalFrames,
) -> Option<EntityId> {
    match r {
        EntityRef::Self_ => ctx.resolve_self(),
        EntityRef::Owner => ctx.resolve_self().and_then(|s| ctx.resolve_owner(s)),
        EntityRef::Observer => ctx.resolve_observer(),
        EntityRef::Target => ctx.resolve_target(),
        EntityRef::It => fr.it_stack.last().copied(),
        EntityRef::ById(id) => Some(*id),
    }
}

pub(super) fn set_resolve<C: EvalContext>(
    s: &EntitySet,
    ctx: &C,
    fr: &EvalFrames,
) -> Vec<EntityId> {
    match s {
        EntitySet::AttachedTo(r) => resolve_ref(r, ctx, fr)
            .map(|p| ctx.attached_to(p))
            .unwrap_or_default(),
        EntitySet::ContainedIn(r) => resolve_ref(r, ctx, fr)
            .map(|p| ctx.contained_in(p))
            .unwrap_or_default(),
    }
}

#[derive(Copy, Clone)]
pub(super) enum Read {
    Int(i64),
    Id(EntityId),
    Type(EntityTypeId),
}

fn read_value<C: EvalContext>(v: &Value, ctx: &C, fr: &EvalFrames) -> Option<Read> {
    match v {
        Value::Const(n) => Some(Read::Int(*n)),
        Value::ConstId(id) => Some(Read::Id(*id)),
        Value::ConstType(t) => Some(Read::Type(*t)),
        Value::TypeOf(r) => resolve_ref(r, ctx, fr)
            .and_then(|id| ctx.type_of(id))
            .map(Read::Type),
        Value::Pool { entity, key, mode } => {
            let id = resolve_ref(entity, ctx, fr)?;
            ctx.pool(id, key, *mode).map(Read::Int)
        }
        Value::Setting(key) => ctx.setting(key).map(Read::Int),
        Value::SourceLoaded(key) => Some(Read::Int(if ctx.source_loaded(key) { 1 } else { 0 })),
        Value::Distance { from, to, kind } => {
            // Both refs must resolve. NULL on either operand collapses the
            // value to NULL, which the enclosing `Cmp` treats as false —
            // same NULL-propagation rule as `Value::Pool` etc.
            let from_id = resolve_ref(from, ctx, fr)?;
            let to_id = resolve_ref(to, ctx, fr)?;
            ctx.distance(from_id, to_id, *kind).map(Read::Int)
        }
    }
}

pub(super) fn apply_op(a: Read, op: Op, b: Read) -> bool {
    match (a, b) {
        (Read::Int(x), Read::Int(y)) => match op {
            Op::Lt => x < y,
            Op::Le => x <= y,
            Op::Eq => x == y,
            Op::Ne => x != y,
            Op::Ge => x >= y,
            Op::Gt => x > y,
        },
        (Read::Id(x), Read::Id(y)) => match op {
            Op::Eq => x == y,
            Op::Ne => x != y,
            _ => false,
        },
        (Read::Type(x), Read::Type(y)) => match op {
            Op::Eq => x == y,
            Op::Ne => x != y,
            _ => false,
        },
        // Type-mismatched comparisons are false, not panic.
        _ => false,
    }
}
