//! Simulation event bus and typed event channels.

use crate::components::CallDirection;
use crate::entity::EntityId;
use crate::error::{RejectionContext, RejectionReason};
use crate::ids::GroupId;
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};

/// Events emitted by the simulation during ticks.
///
/// All entity references use `EntityId`. Games can look up additional
/// component data on the referenced entity if needed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Event {
    // -- Elevator events --
    /// An elevator departed from a stop.
    ElevatorDeparted {
        /// The elevator that departed.
        elevator: EntityId,
        /// The stop it departed from.
        from_stop: EntityId,
        /// The tick when departure occurred.
        tick: u64,
    },
    /// An elevator arrived at a stop.
    ElevatorArrived {
        /// The elevator that arrived.
        elevator: EntityId,
        /// The stop it arrived at.
        at_stop: EntityId,
        /// The tick when arrival occurred.
        tick: u64,
    },
    /// An elevator's doors finished opening.
    DoorOpened {
        /// The elevator whose doors opened.
        elevator: EntityId,
        /// The tick when the doors opened.
        tick: u64,
    },
    /// An elevator's doors finished closing.
    DoorClosed {
        /// The elevator whose doors closed.
        elevator: EntityId,
        /// The tick when the doors closed.
        tick: u64,
    },
    /// Emitted when an elevator passes a stop without stopping.
    /// Games/dispatch can use this to decide whether to add stops mid-travel.
    PassingFloor {
        /// The elevator passing by.
        elevator: EntityId,
        /// The stop being passed.
        stop: EntityId,
        /// Direction: true = moving up, false = moving down.
        moving_up: bool,
        /// The tick when the pass occurred.
        tick: u64,
    },
    /// An in-flight movement was aborted by
    /// [`Simulation::abort_movement`](crate::sim::Simulation::abort_movement).
    ///
    /// The car brakes along its normal deceleration profile, re-targets to
    /// the nearest reachable stop, and arrives there without opening doors
    /// (onboard riders stay aboard). The pending destination queue is also
    /// cleared as part of the abort.
    ///
    /// Emitted at abort time — the car is still in flight, decelerating
    /// toward `brake_target`. A normal [`Event::ElevatorArrived`] fires
    /// once the car actually reaches the stop.
    MovementAborted {
        /// The elevator whose trip was aborted.
        elevator: EntityId,
        /// The stop the car will brake to and park at.
        brake_target: EntityId,
        /// The tick when the abort was requested.
        tick: u64,
    },

    // -- Rider events (unified: passengers, cargo, any rideable entity) --
    /// A new rider appeared at a stop and wants to travel.
    RiderSpawned {
        /// The spawned rider entity.
        rider: EntityId,
        /// The stop where the rider appeared.
        origin: EntityId,
        /// The stop the rider wants to reach.
        destination: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged. See [`Simulation::set_rider_tag`]
        /// for the back-pointer pattern this enables.
        ///
        /// [`Simulation::set_rider_tag`]: crate::sim::Simulation::set_rider_tag
        #[serde(default)]
        tag: u64,
        /// The tick when the rider spawned.
        tick: u64,
    },
    /// A rider boarded an elevator.
    RiderBoarded {
        /// The rider that boarded.
        rider: EntityId,
        /// The elevator the rider boarded.
        elevator: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when boarding occurred.
        tick: u64,
    },
    /// A rider exited an elevator at a stop.
    #[serde(alias = "RiderAlighted")]
    RiderExited {
        /// The rider that exited.
        rider: EntityId,
        /// The elevator the rider exited.
        elevator: EntityId,
        /// The stop where the rider exited.
        stop: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged. Sampled before the rider is freed
        /// so consumers can correlate the exit with external state even
        /// after the [`RiderId`](crate::entity::RiderId) becomes stale.
        #[serde(default)]
        tag: u64,
        /// The tick when exiting occurred.
        tick: u64,
    },
    /// A rider was rejected from boarding (e.g., over capacity).
    RiderRejected {
        /// The rider that was rejected.
        rider: EntityId,
        /// The elevator that rejected the rider.
        elevator: EntityId,
        /// The reason for rejection.
        reason: RejectionReason,
        /// Additional numeric context for the rejection.
        context: Option<RejectionContext>,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when rejection occurred.
        tick: u64,
    },
    /// A rider gave up waiting and left the stop.
    RiderAbandoned {
        /// The rider that abandoned.
        rider: EntityId,
        /// The stop the rider left.
        stop: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged. Sampled before the rider is freed.
        #[serde(default)]
        tag: u64,
        /// The tick when abandonment occurred.
        tick: u64,
    },

    /// A rider was ejected from an elevator (due to disable or despawn).
    ///
    /// The rider is moved to `Waiting` phase at the nearest stop.
    RiderEjected {
        /// The rider that was ejected.
        rider: EntityId,
        /// The elevator the rider was ejected from.
        elevator: EntityId,
        /// The stop the rider was placed at.
        stop: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when ejection occurred.
        tick: u64,
    },

    // -- Dispatch events --
    /// An elevator was assigned to serve a stop by the dispatcher.
    ElevatorAssigned {
        /// The elevator that was assigned.
        elevator: EntityId,
        /// The stop it was assigned to serve.
        stop: EntityId,
        /// The tick when the assignment occurred.
        tick: u64,
    },

    // -- Topology lifecycle events --
    /// A new stop was added to the simulation.
    StopAdded {
        /// The new stop entity.
        stop: EntityId,
        /// The line the stop was added to.
        line: EntityId,
        /// The group the stop was added to.
        group: GroupId,
        /// The tick when the stop was added.
        tick: u64,
    },
    /// A new elevator was added to the simulation.
    ElevatorAdded {
        /// The new elevator entity.
        elevator: EntityId,
        /// The line the elevator was added to.
        line: EntityId,
        /// The group the elevator was added to.
        group: GroupId,
        /// The tick when the elevator was added.
        tick: u64,
    },
    /// An entity was disabled.
    EntityDisabled {
        /// The entity that was disabled.
        entity: EntityId,
        /// The tick when it was disabled.
        tick: u64,
    },
    /// An entity was re-enabled.
    EntityEnabled {
        /// The entity that was re-enabled.
        entity: EntityId,
        /// The tick when it was enabled.
        tick: u64,
    },
    /// A rider's route was invalidated due to topology change.
    ///
    /// Emitted when a stop on a rider's route is disabled or removed.
    /// If no alternative is found, the rider will abandon after a grace period.
    RouteInvalidated {
        /// The affected rider.
        rider: EntityId,
        /// The stop that caused the invalidation.
        affected_stop: EntityId,
        /// Why the route was invalidated.
        reason: RouteInvalidReason,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when invalidation occurred.
        tick: u64,
    },
    /// A rider was manually rerouted via `sim.reroute()`.
    RiderRerouted {
        /// The rerouted rider.
        rider: EntityId,
        /// The new destination stop.
        new_destination: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when rerouting occurred.
        tick: u64,
    },

    /// A rider settled at a stop, becoming a resident.
    RiderSettled {
        /// The rider that settled.
        rider: EntityId,
        /// The stop where the rider settled.
        stop: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// The tick when settlement occurred.
        tick: u64,
    },
    /// A rider was removed from the simulation.
    RiderDespawned {
        /// The rider that was removed.
        rider: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged. Sampled before the rider is freed.
        #[serde(default)]
        tag: u64,
        /// The tick when despawn occurred.
        tick: u64,
    },

    // -- Line lifecycle events --
    /// A line was added to the simulation.
    LineAdded {
        /// The new line entity.
        line: EntityId,
        /// The group the line was added to.
        group: GroupId,
        /// The tick when the line was added.
        tick: u64,
    },
    /// A line was removed from the simulation.
    LineRemoved {
        /// The removed line entity.
        line: EntityId,
        /// The group the line belonged to.
        group: GroupId,
        /// The tick when the line was removed.
        tick: u64,
    },
    /// A line was reassigned to a different group.
    LineReassigned {
        /// The line entity that was reassigned.
        line: EntityId,
        /// The group the line was previously in.
        old_group: GroupId,
        /// The group the line was moved to.
        new_group: GroupId,
        /// The tick when reassignment occurred.
        tick: u64,
    },
    /// An elevator was reassigned to a different line.
    ElevatorReassigned {
        /// The elevator that was reassigned.
        elevator: EntityId,
        /// The line the elevator was previously on.
        old_line: EntityId,
        /// The line the elevator was moved to.
        new_line: EntityId,
        /// The tick when reassignment occurred.
        tick: u64,
    },

    // -- Repositioning events --
    /// An elevator is being repositioned to improve coverage.
    ///
    /// Emitted when an idle elevator begins moving to a new position
    /// as decided by the [`RepositionStrategy`](crate::dispatch::RepositionStrategy).
    ElevatorRepositioning {
        /// The elevator being repositioned.
        elevator: EntityId,
        /// The stop it is being sent to.
        to_stop: EntityId,
        /// The tick when repositioning began.
        tick: u64,
    },
    /// An elevator completed repositioning and arrived at its target.
    ///
    /// Note: this is detected by the movement system — the elevator
    /// arrives just like any other movement. Games can distinguish
    /// repositioning arrivals from dispatch arrivals by tracking
    /// which elevators received `ElevatorRepositioning` events.
    ElevatorRepositioned {
        /// The elevator that completed repositioning.
        elevator: EntityId,
        /// The stop it arrived at.
        at_stop: EntityId,
        /// The tick when it arrived.
        tick: u64,
    },

    /// An elevator was recalled to a specific stop via
    /// [`Simulation::recall_to`](crate::sim::Simulation::recall_to).
    ///
    /// The car's destination queue has been replaced with the recall
    /// target. If the car was mid-flight, it continues to its current
    /// target, then proceeds to the recall stop. If idle, it departs
    /// on the next tick.
    ElevatorRecalled {
        /// The elevator that was recalled.
        elevator: EntityId,
        /// The stop the elevator is being sent to.
        to_stop: EntityId,
        /// The tick when the recall was issued.
        tick: u64,
    },

    /// An elevator's service mode was changed.
    ServiceModeChanged {
        /// The elevator whose mode changed.
        elevator: EntityId,
        /// The previous service mode.
        from: crate::components::ServiceMode,
        /// The new service mode.
        to: crate::components::ServiceMode,
        /// The tick when the change occurred.
        tick: u64,
    },

    // -- Observability events --
    /// Energy consumed/regenerated by an elevator this tick.
    ///
    /// Requires the `energy` feature.
    #[cfg(feature = "energy")]
    EnergyConsumed {
        /// The elevator that consumed energy.
        elevator: EntityId,
        /// Energy consumed this tick.
        consumed: OrderedFloat<f64>,
        /// Energy regenerated this tick.
        regenerated: OrderedFloat<f64>,
        /// The tick when energy was recorded.
        tick: u64,
    },

    /// An elevator's load changed (rider boarded or exited).
    ///
    /// Emitted immediately after [`RiderBoarded`](Self::RiderBoarded) or
    /// [`RiderExited`](Self::RiderExited). Useful for real-time capacity
    /// bar displays in game UIs.
    ///
    /// Load values use [`OrderedFloat`] for
    /// `Eq` compatibility. Dereference to get the inner `f64`:
    ///
    /// ```rust,no_run
    /// use elevator_core::events::Event;
    /// # fn handle(event: Event) {
    /// if let Event::CapacityChanged { current_load, capacity, .. } = event {
    ///     let pct = *current_load / *capacity * 100.0;
    ///     println!("Elevator at {pct:.0}% capacity");
    /// }
    /// # }
    /// ```
    CapacityChanged {
        /// The elevator whose load changed.
        elevator: EntityId,
        /// Current total weight aboard after the change.
        current_load: OrderedFloat<f64>,
        /// Maximum weight capacity of the elevator.
        capacity: OrderedFloat<f64>,
        /// The tick when the change occurred.
        tick: u64,
    },

    /// An elevator became idle (no more assignments or repositioning).
    ElevatorIdle {
        /// The elevator that became idle.
        elevator: EntityId,
        /// The stop where it became idle (if at a stop).
        at_stop: Option<EntityId>,
        /// The tick when it became idle.
        tick: u64,
    },

    /// An elevator's direction indicator lamps changed.
    ///
    /// Emitted by the dispatch phase when the pair
    /// `(going_up, going_down)` transitions to a new value — e.g. when
    /// a car is assigned a target above it (up-only), below it (down-only),
    /// or returns to idle (both lamps lit).
    ///
    /// Games can use this for UI (lighting up-arrow / down-arrow indicators
    /// on a car) without polling the elevator each tick.
    DirectionIndicatorChanged {
        /// The elevator whose indicator lamps changed.
        elevator: EntityId,
        /// New state of the up-direction lamp.
        going_up: bool,
        /// New state of the down-direction lamp.
        going_down: bool,
        /// The tick when the change occurred.
        tick: u64,
    },

    /// An elevator was permanently removed from the simulation.
    ///
    /// Distinct from [`Event::EntityDisabled`] — a disabled elevator can be
    /// re-enabled, but a removed elevator is despawned.
    ElevatorRemoved {
        /// The elevator that was removed.
        elevator: EntityId,
        /// The line it belonged to.
        line: EntityId,
        /// The group it belonged to.
        group: GroupId,
        /// The tick when removal occurred.
        tick: u64,
    },

    /// A stop was queued as a destination for an elevator.
    ///
    /// Emitted by [`Simulation::push_destination`](crate::sim::Simulation::push_destination),
    /// [`Simulation::push_destination_front`](crate::sim::Simulation::push_destination_front),
    /// and the built-in dispatch phase whenever it actually appends a stop
    /// to an elevator's [`DestinationQueue`](crate::components::DestinationQueue).
    /// Adjacent-duplicate pushes (that are deduplicated) do not emit.
    DestinationQueued {
        /// The elevator whose queue was updated.
        elevator: EntityId,
        /// The stop that was queued.
        stop: EntityId,
        /// The tick when the push occurred.
        tick: u64,
    },

    /// A stop was permanently removed from the simulation.
    ///
    /// Distinct from [`Event::EntityDisabled`] — a disabled stop can be
    /// re-enabled, but a removed stop is despawned.
    StopRemoved {
        /// The stop that was removed.
        stop: EntityId,
        /// The tick when removal occurred.
        tick: u64,
    },

    /// A manual door-control command was received and either applied
    /// immediately or stored for later.
    ///
    /// Emitted by
    /// [`Simulation::open_door`](crate::sim::Simulation::open_door),
    /// [`Simulation::close_door`](crate::sim::Simulation::close_door),
    /// [`Simulation::hold_door`](crate::sim::Simulation::hold_door),
    /// and [`Simulation::cancel_door_hold`](crate::sim::Simulation::cancel_door_hold)
    /// when the command is accepted. Paired with
    /// [`Event::DoorCommandApplied`] when the command eventually takes effect.
    DoorCommandQueued {
        /// The elevator targeted by the command.
        elevator: EntityId,
        /// The command that was queued.
        command: crate::door::DoorCommand,
        /// The tick when the command was submitted.
        tick: u64,
    },
    /// A queued door-control command actually took effect — doors began
    /// opening/closing or a hold was applied.
    DoorCommandApplied {
        /// The elevator the command applied to.
        elevator: EntityId,
        /// The command that was applied.
        command: crate::door::DoorCommand,
        /// The tick when the command was applied.
        tick: u64,
    },

    /// An elevator parameter was mutated at runtime via one of the
    /// `Simulation::set_*` upgrade setters (e.g. buying a speed upgrade
    /// in an RPG, or a scripted event changing capacity mid-game).
    ///
    /// Emitted immediately when the setter succeeds. Games can use this
    /// to trigger score popups, SFX, or UI updates.
    ElevatorUpgraded {
        /// The elevator whose parameter changed.
        elevator: EntityId,
        /// Which field was changed.
        field: UpgradeField,
        /// Previous value of the field.
        old: UpgradeValue,
        /// New value of the field.
        new: UpgradeValue,
        /// The tick when the upgrade was applied.
        tick: u64,
    },

    /// A velocity command was submitted to an elevator running in
    /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
    ///
    /// Emitted by
    /// [`Simulation::set_target_velocity`](crate::sim::Simulation::set_target_velocity)
    /// and [`Simulation::emergency_stop`](crate::sim::Simulation::emergency_stop).
    ManualVelocityCommanded {
        /// The elevator targeted by the command.
        elevator: EntityId,
        /// The new target velocity (clamped to `[-max_speed, max_speed]`),
        /// or `None` when the command clears the target.
        target_velocity: Option<ordered_float::OrderedFloat<f64>>,
        /// The tick when the command was submitted.
        tick: u64,
    },

    // -- Hall & car call events --
    /// A hall button was pressed. Fires immediately, before any ack
    /// latency delay. Games use this for button-light animations or SFX.
    HallButtonPressed {
        /// Stop where the press occurred.
        stop: EntityId,
        /// Direction the button requests service in.
        direction: CallDirection,
        /// Tick of the press.
        tick: u64,
    },
    /// A hall call has elapsed its ack latency and is now visible to
    /// dispatch. Useful for UI confirmations ("your call has been
    /// received"). In groups with `ack_latency_ticks = 0` this fires on
    /// the same tick as `HallButtonPressed`.
    HallCallAcknowledged {
        /// Stop the call was placed at.
        stop: EntityId,
        /// Direction of the call.
        direction: CallDirection,
        /// Tick acknowledgement completed.
        tick: u64,
    },
    /// A hall call was cleared when an assigned car arrived at the stop
    /// with matching direction indicators. Corresponds to the real-world
    /// button-light turning off.
    HallCallCleared {
        /// Stop whose call was cleared.
        stop: EntityId,
        /// Direction of the cleared call.
        direction: CallDirection,
        /// Car that cleared the call by arriving.
        car: EntityId,
        /// Tick the call was cleared.
        tick: u64,
    },
    /// A rider inside a car pressed a floor button (Classic mode only).
    /// In Destination mode riders reveal destinations at the hall
    /// kiosk, so no car buttons are pressed.
    CarButtonPressed {
        /// Elevator the button was pressed inside.
        car: EntityId,
        /// Floor the rider requested.
        floor: EntityId,
        /// Rider who pressed the button. `None` when the press is
        /// synthetic — e.g. issued via
        /// [`Simulation::press_car_button`](crate::sim::Simulation::press_car_button)
        /// for scripted events, player input, or cutscene cues with no
        /// associated rider entity.
        rider: Option<EntityId>,
        /// Opaque consumer tag attached to the pressing rider, mirroring
        /// the optionality of [`rider`](Self::CarButtonPressed::rider).
        /// `Some(0)` for a present-but-untagged rider; `None` for a
        /// synthetic press with no rider entity.
        #[serde(default)]
        tag: Option<u64>,
        /// Tick of the press.
        tick: u64,
    },
    /// A rider skipped boarding a car they considered too crowded
    /// (their preferences filtered the car out). The rider remains
    /// Waiting and may board a later car.
    #[serde(alias = "RiderBalked")]
    RiderSkipped {
        /// Rider who skipped.
        rider: EntityId,
        /// Elevator they declined to board.
        elevator: EntityId,
        /// Stop where the skip happened.
        at_stop: EntityId,
        /// Opaque consumer tag attached to the rider at emit time. `0`
        /// when the rider is untagged.
        #[serde(default)]
        tag: u64,
        /// Tick of the skip.
        tick: u64,
    },
    /// A snapshot restore encountered an entity reference that could not
    /// be remapped. The original (now-invalid) ID was kept. This signals
    /// a corrupted or hand-edited snapshot.
    SnapshotDanglingReference {
        /// The entity ID from the snapshot that had no mapping.
        stale_id: EntityId,
        /// Tick from the snapshot at restore time.
        tick: u64,
    },
    /// A snapshot restore could not re-instantiate the reposition strategy
    /// for a group (e.g. custom strategy not registered). Idle elevator
    /// positioning will not work for this group until the game calls
    /// [`Simulation::set_reposition`](crate::sim::Simulation::set_reposition).
    RepositionStrategyNotRestored {
        /// The group whose strategy was lost.
        group: GroupId,
    },
    /// A snapshot restore instantiated the dispatch strategy for a
    /// group but couldn't replay its tunable configuration (the
    /// serialized form in the snapshot didn't parse). The strategy
    /// runs with its default weights — identical to a legacy snapshot
    /// that never carried dispatch config at all.
    DispatchConfigNotRestored {
        /// The group whose dispatcher ran with defaults.
        group: GroupId,
        /// The parse error from
        /// [`DispatchStrategy::restore_config`](crate::dispatch::DispatchStrategy::restore_config).
        reason: String,
    },
    /// A stop was removed while resident riders were present.
    /// The game must relocate or despawn these riders.
    ResidentsAtRemovedStop {
        /// The removed stop.
        stop: EntityId,
        /// Riders that were resident at the stop.
        residents: Vec<EntityId>,
    },
}

/// Identifies which elevator parameter was changed in an
/// [`Event::ElevatorUpgraded`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UpgradeField {
    /// Maximum travel speed (distance/tick).
    MaxSpeed,
    /// Acceleration rate (distance/tick^2).
    Acceleration,
    /// Deceleration rate (distance/tick^2).
    Deceleration,
    /// Maximum weight the car can carry.
    WeightCapacity,
    /// Ticks for a door open/close transition.
    DoorTransitionTicks,
    /// Ticks the door stays fully open.
    DoorOpenTicks,
}

impl std::fmt::Display for UpgradeField {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MaxSpeed => write!(f, "max_speed"),
            Self::Acceleration => write!(f, "acceleration"),
            Self::Deceleration => write!(f, "deceleration"),
            Self::WeightCapacity => write!(f, "weight_capacity"),
            Self::DoorTransitionTicks => write!(f, "door_transition_ticks"),
            Self::DoorOpenTicks => write!(f, "door_open_ticks"),
        }
    }
}

/// Old-or-new value carried by [`Event::ElevatorUpgraded`].
///
/// Uses [`OrderedFloat`] for the float variant so the event enum
/// remains `Eq`-comparable alongside the other observability events.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UpgradeValue {
    /// A floating-point parameter value (speed, accel, decel, capacity).
    Float(OrderedFloat<f64>),
    /// An integral tick-count parameter value (door timings).
    Ticks(u32),
}

impl UpgradeValue {
    /// Construct a float-valued upgrade payload.
    #[must_use]
    pub const fn float(v: f64) -> Self {
        Self::Float(OrderedFloat(v))
    }

    /// Construct a tick-valued upgrade payload.
    #[must_use]
    pub const fn ticks(v: u32) -> Self {
        Self::Ticks(v)
    }
}

impl std::fmt::Display for UpgradeValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Float(v) => write!(f, "{}", **v),
            Self::Ticks(v) => write!(f, "{v}"),
        }
    }
}

/// Reason a rider's route was invalidated.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum RouteInvalidReason {
    /// A stop on the route was disabled (transient — the stop entity
    /// still exists).
    StopDisabled,
    /// Reserved. No code currently emits this — the "no alternative
    /// found" signal is conveyed by the accompanying `RiderAbandoned`
    /// event so this enum stays dedicated to *why* the route was
    /// invalidated rather than *what the rerouter could do about it*.
    NoAlternative,
    /// A stop on the route was permanently removed (despawned).
    /// Distinguishes the removal path from a transient disable.
    StopRemoved,
}

/// Coarse-grained classification of an [`Event`].
///
/// Exposes the same grouping that the `Event` variants are already
/// commented under, so consumers can filter a drained event stream with
/// one match arm per category rather than enumerating ~25 variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum EventCategory {
    /// Elevator motion, arrival/departure, and door state.
    Elevator,
    /// Rider lifecycle: spawn, board, exit, reject, abandon, despawn, settle, reroute.
    Rider,
    /// Dispatch decisions and bookkeeping.
    Dispatch,
    /// Runtime topology mutations (entities/lines/groups added, removed, reassigned).
    Topology,
    /// Idle-elevator repositioning activity.
    Reposition,
    /// Direction indicator lamp state changes.
    Direction,
    /// Observability and misc signals (capacity, service mode, energy, etc.).
    Observability,
}

impl Event {
    /// Classify this event into a coarse-grained [`EventCategory`].
    ///
    /// Useful when a consumer only cares about, say, rider activity and
    /// wants to skip elevator-motion and topology chatter without
    /// enumerating every variant. The exhaustive match inside guarantees
    /// the method stays in sync when new variants are added.
    #[must_use]
    pub const fn category(&self) -> EventCategory {
        match self {
            Self::ElevatorDeparted { .. }
            | Self::ElevatorArrived { .. }
            | Self::DoorOpened { .. }
            | Self::DoorClosed { .. }
            | Self::DoorCommandQueued { .. }
            | Self::DoorCommandApplied { .. }
            | Self::PassingFloor { .. }
            | Self::MovementAborted { .. }
            | Self::ElevatorIdle { .. } => EventCategory::Elevator,
            Self::RiderSpawned { .. }
            | Self::RiderBoarded { .. }
            | Self::RiderExited { .. }
            | Self::RiderRejected { .. }
            | Self::RiderAbandoned { .. }
            | Self::RiderEjected { .. }
            | Self::RouteInvalidated { .. }
            | Self::RiderRerouted { .. }
            | Self::RiderSettled { .. }
            | Self::RiderDespawned { .. } => EventCategory::Rider,
            Self::ElevatorAssigned { .. } | Self::DestinationQueued { .. } => {
                EventCategory::Dispatch
            }
            Self::StopAdded { .. }
            | Self::StopRemoved { .. }
            | Self::ElevatorAdded { .. }
            | Self::ElevatorRemoved { .. }
            | Self::EntityDisabled { .. }
            | Self::EntityEnabled { .. }
            | Self::LineAdded { .. }
            | Self::LineRemoved { .. }
            | Self::LineReassigned { .. }
            | Self::ElevatorReassigned { .. } => EventCategory::Topology,
            Self::ElevatorRepositioning { .. } | Self::ElevatorRepositioned { .. } => {
                EventCategory::Reposition
            }
            Self::ElevatorRecalled { .. } => EventCategory::Elevator,
            Self::DirectionIndicatorChanged { .. } => EventCategory::Direction,
            Self::ServiceModeChanged { .. }
            | Self::CapacityChanged { .. }
            | Self::ElevatorUpgraded { .. }
            | Self::ManualVelocityCommanded { .. } => EventCategory::Observability,
            #[cfg(feature = "energy")]
            Self::EnergyConsumed { .. } => EventCategory::Observability,
            Self::HallButtonPressed { .. }
            | Self::HallCallAcknowledged { .. }
            | Self::HallCallCleared { .. }
            | Self::CarButtonPressed { .. } => EventCategory::Dispatch,
            Self::RiderSkipped { .. } => EventCategory::Rider,
            Self::SnapshotDanglingReference { .. }
            | Self::RepositionStrategyNotRestored { .. }
            | Self::DispatchConfigNotRestored { .. } => EventCategory::Observability,
            Self::ResidentsAtRemovedStop { .. } => EventCategory::Topology,
        }
    }
}

/// Collects simulation events for consumers to drain.
#[derive(Debug, Default)]
pub struct EventBus {
    /// The pending events not yet consumed.
    events: Vec<Event>,
}

impl EventBus {
    /// Pushes a new event onto the bus.
    pub fn emit(&mut self, event: Event) {
        self.events.push(event);
    }

    /// Returns and clears all pending events.
    pub fn drain(&mut self) -> Vec<Event> {
        std::mem::take(&mut self.events)
    }

    /// Returns a slice of all pending events without clearing them.
    #[must_use]
    pub fn peek(&self) -> &[Event] {
        &self.events
    }
}

/// A typed event channel for game-specific events.
///
/// Games insert this as a global resource on `World`:
///
/// ```
/// use elevator_core::world::World;
/// use elevator_core::events::EventChannel;
///
/// #[derive(Debug)]
/// enum MyGameEvent { Foo, Bar }
///
/// let mut world = World::new();
/// world.insert_resource(EventChannel::<MyGameEvent>::new());
/// // Later:
/// world.resource_mut::<EventChannel<MyGameEvent>>().unwrap().emit(MyGameEvent::Foo);
/// ```
#[derive(Debug)]
pub struct EventChannel<T> {
    /// Pending events not yet consumed.
    events: Vec<T>,
}

impl<T> EventChannel<T> {
    /// Create an empty event channel.
    #[must_use]
    pub const fn new() -> Self {
        Self { events: Vec::new() }
    }

    /// Emit an event into the channel.
    pub fn emit(&mut self, event: T) {
        self.events.push(event);
    }

    /// Drain and return all pending events.
    pub fn drain(&mut self) -> Vec<T> {
        std::mem::take(&mut self.events)
    }

    /// Peek at pending events without clearing.
    #[must_use]
    pub fn peek(&self) -> &[T] {
        &self.events
    }

    /// Check if the channel has no pending events.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.events.is_empty()
    }

    /// Number of pending events.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.events.len()
    }
}

impl<T> Default for EventChannel<T> {
    fn default() -> Self {
        Self::new()
    }
}

/// Helpers for surfacing simulation events as log-style records.
///
/// The FFI host exposes a "log drain" API ([`ev_drain_log_messages`])
/// that polling consumers (e.g. `GameMaker`) call to surface
/// debug-formatted strings for events emitted during the most recent
/// step. Other binding crates wrap the same primitive via this
/// module so every host produces the same message text and severity
/// for a given `Event`.
///
/// [`ev_drain_log_messages`]: https://docs.rs/elevator-ffi/latest/elevator_ffi/fn.ev_drain_log_messages.html
pub mod log_format {
    use super::Event;

    /// `trace`-level severity (`0`).
    ///
    /// The full set (`LEVEL_TRACE` … `LEVEL_ERROR`) is the shared
    /// vocabulary for cross-host log surfaces — see the repository's
    /// `docs/src/host-binding-parity.md` for the parity contract.
    pub const LEVEL_TRACE: u8 = 0;
    /// `debug`-level severity (`1`). The default for events surfaced
    /// via [`format_event`].
    pub const LEVEL_DEBUG: u8 = 1;
    /// `info`-level severity (`2`).
    pub const LEVEL_INFO: u8 = 2;
    /// `warn`-level severity (`3`).
    pub const LEVEL_WARN: u8 = 3;
    /// `error`-level severity (`4`).
    pub const LEVEL_ERROR: u8 = 4;

    /// Format an event for log-style consumption. Returns
    /// `(level, message)` where `message` is the `Debug` rendering
    /// of the event and `level` is currently always
    /// [`LEVEL_DEBUG`].
    ///
    /// Hosts wrap this in their idiomatic surface — e.g. a
    /// `Vec<JsValue>` for wasm, a Godot `Array` of dictionaries,
    /// or the FFI's borrowed `EvLogMessage` struct.
    #[must_use]
    pub fn format_event(event: &Event) -> (u8, String) {
        (LEVEL_DEBUG, format!("{event:?}"))
    }
}
