//! Real-time pipeline event bus — broadcasts `TransitionFired` events to
//! per-persona WebSocket subscribers as they happen.
//!
//! Turn-time [`crate::llm_session::assemble_prompt_context`] still works as
//! a fallback catch-up mechanism for any events that accumulated while no
//! subscriber was connected.

use std::sync::OnceLock;
use tokio::sync::broadcast;

/// Capacity of the per-persona event bus broadcast channel.
const BUS_CAPACITY: usize = 256;

/// A raw pipeline-transition event forwarded from `log_transition_event`.
#[derive(Clone, Debug)]
pub struct BusEvent {
    /// Hex-encoded sled ID that fired the transition.
    pub sled_id: String,
    /// Story identifier (e.g. `"42_story_foo"`).
    pub story_id: String,
    /// Human-readable label of the stage before the transition.
    pub from_stage: String,
    /// Human-readable label of the stage after the transition.
    pub to_stage: String,
    /// String label of the `PipelineEvent` variant.
    pub pipeline_event: String,
}

static BUS_TX: OnceLock<broadcast::Sender<BusEvent>> = OnceLock::new();

/// Initialise the pipeline event bus.  No-op on subsequent calls.
pub fn init() {
    let _ = BUS_TX.get_or_init(|| broadcast::channel::<BusEvent>(BUS_CAPACITY).0);
}

/// Broadcast a pipeline transition event to all active subscribers.
///
/// No-op if the bus has not been initialised or there are no subscribers.
pub fn broadcast(event: BusEvent) {
    if let Some(tx) = BUS_TX.get() {
        let _ = tx.send(event);
    }
}

/// Subscribe to the pipeline event bus.
///
/// Returns `None` if the bus has not been initialised yet.
pub fn subscribe() -> Option<broadcast::Receiver<BusEvent>> {
    BUS_TX.get().map(|tx| tx.subscribe())
}

/// Render a [`BusEvent`] as the same compact audit line used in
/// `assemble_and_advance_session`.
pub fn render_event(event: &BusEvent) -> String {
    if event.pipeline_event == crate::crdt_state::GAP_PIPELINE_EVENT {
        format!(
            "events between {} and {} were dropped",
            event.from_stage, event.to_stage
        )
    } else {
        format!(
            "pipeline_event sled_id=\"{}\" story_id=\"{}\" from=\"{}\" to=\"{}\" event=\"{}\"",
            event.sled_id, event.story_id, event.from_stage, event.to_stage, event.pipeline_event
        )
    }
}

/// Returns `true` if `event` should be delivered to `persona` based on the
/// persona's stored scope filter in the CRDT.
///
/// Falls back to local-sled-only if no session entry exists for `persona`.
pub fn event_matches_persona(event: &BusEvent, persona: &str) -> bool {
    use crate::crdt_state::ScopeFilter;
    match crate::crdt_state::read_llm_session(persona) {
        Some(session) => match &session.scope_filter {
            ScopeFilter::All => true,
            ScopeFilter::Sleds(ids) => ids.contains(&event.sled_id),
        },
        None => {
            let local = crate::crdt_state::our_node_id().unwrap_or_default();
            !local.is_empty() && event.sled_id == local
        }
    }
}
