//! Broadcast projection for durable Agent run/event updates.
//!
//! The hub follows the WorkManager plan-change broadcast pattern: the
//! Coordinator publishes a bounded view after each commit; the gateway
//! relays it to WebSocket clients.  A lost broadcast is never fatal — the
//! client recalibrates with `GetAgentRuns`, whose `revision` comes from
//! `agent_session_state`.

use crate::protocol::{AgentEventView, AgentRunView};

/// One projected change for a root session.  Exactly one of `run`/`event` is
/// present.
#[derive(Debug, Clone)]
pub struct AgentProjection {
    pub session_id: String,
    pub revision: i64,
    pub run: Option<AgentRunView>,
    pub event: Option<AgentEventView>,
}

#[derive(Debug, Clone)]
pub struct AgentProjectionHub {
    tx: tokio::sync::broadcast::Sender<AgentProjection>,
}

impl Default for AgentProjectionHub {
    fn default() -> Self {
        Self::new()
    }
}

impl AgentProjectionHub {
    pub fn new() -> Self {
        let (tx, _) = tokio::sync::broadcast::channel(256);
        Self { tx }
    }

    pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<AgentProjection> {
        self.tx.subscribe()
    }

    /// Publish a run update.  Best-effort: relays lag and drop the event,
    /// clients recalibrate with a full query.
    pub fn publish(&self, projection: AgentProjection) {
        let _ = self.tx.send(projection);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn publish_reaches_subscribers_and_drop_is_nonfatal() {
        let hub = AgentProjectionHub::new();
        let mut rx = hub.subscribe();
        hub.publish(AgentProjection {
            session_id: "cli:test:d".to_string(),
            revision: 1,
            run: None,
            event: None,
        });
        let received = rx.try_recv().unwrap();
        assert_eq!(received.session_id, "cli:test:d");
        // No subscribers after the first is dropped; publish must not panic.
        drop(rx);
        hub.publish(AgentProjection {
            session_id: "cli:test:d".to_string(),
            revision: 2,
            run: None,
            event: None,
        });
    }
}
