//! `RealtimeBroadcaster` — the in-process fan-out seam for SSE realtime
//! delivery (v1.1.6).
//!
//! Structurally a sibling of [`crate::inbox::InboxResolver`]: the trait
//! lives here in `picloud-shared` because the publish side
//! (`PubsubServiceImpl` in manager-core) and the subscribe side (the SSE
//! handler in orchestrator-core) live in different crates and both need
//! one shared instance. The in-process impl lives in orchestrator-core
//! (`Mutex<HashMap<(AppId, topic), broadcast::Sender>>`); cluster mode
//! (v1.3+) swaps it for a Postgres `LISTEN/NOTIFY`-backed resolver behind
//! the same trait without touching either caller.
//!
//! Delivery is **best-effort, at-most-once**: this is the realtime path,
//! NOT the durable one. Durable trigger fan-out (retry / dead-letter)
//! goes through the outbox and is the publish caller's separate concern.
//! A slow SSE consumer loses the oldest events (bounded broadcast
//! buffer); SSE's own transport-layer auto-reconnect is the recovery
//! mechanism (no server-side replay in v1.1.6).

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use thiserror::Error;
use tokio::sync::broadcast;

use crate::{AppId, GroupId};

/// A single realtime event delivered to in-process SSE subscribers. The
/// SSE handler serializes this to `data: {...}\n\n` on the wire.
#[derive(Debug, Clone)]
pub struct RealtimeEvent {
    pub topic: String,
    pub message: serde_json::Value,
    pub published_at: DateTime<Utc>,
}

#[derive(Debug, Error)]
pub enum BroadcasterError {
    /// Reserved for backends that can fail to register a subscriber
    /// (e.g. the cluster-mode `LISTEN/NOTIFY` resolver). The in-process
    /// impl never returns this.
    #[error("realtime broadcaster unavailable: {0}")]
    Unavailable(String),
}

#[async_trait]
pub trait RealtimeBroadcaster: Send + Sync {
    /// Subscribe to events on `(app_id, topic)`. Returns a receiver that
    /// yields events until dropped. Channels are created lazily on first
    /// subscribe.
    async fn subscribe(
        &self,
        app_id: AppId,
        topic: &str,
    ) -> Result<broadcast::Receiver<RealtimeEvent>, BroadcasterError>;

    /// Publish an event to in-process subscribers. NOT durable — the
    /// outbox-backed durable fan-out is the publish caller's separate
    /// concern. A publish with no live subscribers is a silent no-op.
    async fn publish(&self, app_id: AppId, topic: &str, event: RealtimeEvent);

    /// Drop every subscriber for a topic (called on topic DELETE). Live
    /// receivers observe a closed channel and disconnect cleanly.
    async fn drop_topic(&self, app_id: AppId, topic: &str);

    // ---- §11.6 D2: group SHARED topics -------------------------------------
    // A shared topic fans out to every subtree app that subscribes, so it is
    // keyed by the OWNING GROUP (not a single app) — one channel per
    // `(group_id, topic)`. Default impls are no-ops so non-realtime bundles
    // (NoopRealtimeBroadcaster, test doubles) need no change.

    /// Subscribe to a group shared topic's realtime stream.
    async fn subscribe_group(
        &self,
        group_id: GroupId,
        topic: &str,
    ) -> Result<broadcast::Receiver<RealtimeEvent>, BroadcasterError> {
        let _ = (group_id, topic);
        let (_tx, rx) = broadcast::channel(1);
        Ok(rx)
    }

    /// Publish to a group shared topic's in-process subscribers (best-effort;
    /// no live subscriber is a silent no-op).
    async fn publish_group(&self, group_id: GroupId, topic: &str, event: RealtimeEvent) {
        let _ = (group_id, topic, event);
    }

    /// Drop every subscriber for a group shared topic.
    async fn drop_group_topic(&self, group_id: GroupId, topic: &str) {
        let _ = (group_id, topic);
    }
}

/// Bootstrap / test impl: subscribe yields a receiver on a throwaway
/// channel, publish is a no-op. Lets a `Services`-style bundle build
/// without the real registry wired in.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopRealtimeBroadcaster;

#[async_trait]
impl RealtimeBroadcaster for NoopRealtimeBroadcaster {
    async fn subscribe(
        &self,
        _app_id: AppId,
        _topic: &str,
    ) -> Result<broadcast::Receiver<RealtimeEvent>, BroadcasterError> {
        let (_tx, rx) = broadcast::channel(1);
        Ok(rx)
    }

    async fn publish(&self, _app_id: AppId, _topic: &str, _event: RealtimeEvent) {}

    async fn drop_topic(&self, _app_id: AppId, _topic: &str) {}
}
