//! Cross-node notification bus for message delivery fan-out.
//!
//! When a message is enqueued, the bus publishes a notification so that
//! any node running a `fetch_wait` long-poll for that recipient can
//! wake up — even if the enqueue happened on a different node.
//!
//! Two backends:
//! - `InMemoryNotificationBus`: single-node, tokio::sync::Notify (default)
//! - Redis pub/sub (feature-gated `redis-pubsub`, implemented externally)

use std::sync::Arc;
use dashmap::DashMap;
use tokio::sync::Notify;

// ── Trait ────────────────────────────────────────────────────────────────────

/// Cross-node notification bus.
///
/// Publishers call `publish` when a message is enqueued.
/// Subscribers call `subscribe` to get a future that resolves when
/// a notification arrives for the given topic.
pub trait NotificationBus: Send + Sync {
    /// Notify all waiters for `topic` that new data is available.
    fn publish(&self, topic: &[u8]);

    /// Return a future that completes when `topic` receives a notification.
    /// The returned `Notify` can be `.notified().await`'d.
    fn subscribe(&self, topic: &[u8]) -> Arc<Notify>;
}

// ── In-memory implementation ────────────────────────────────────────────────

/// Single-node notification bus backed by `tokio::sync::Notify`.
///
/// This is the default for single-node deployments. For multi-node,
/// replace with a Redis pub/sub or NATS implementation.
pub struct InMemoryNotificationBus {
    waiters: DashMap<Vec<u8>, Arc<Notify>>,
}

impl InMemoryNotificationBus {
    pub fn new() -> Self {
        Self {
            waiters: DashMap::new(),
        }
    }
}

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

impl NotificationBus for InMemoryNotificationBus {
    fn publish(&self, topic: &[u8]) {
        if let Some(notify) = self.waiters.get(topic) {
            notify.notify_waiters();
        }
    }

    fn subscribe(&self, topic: &[u8]) -> Arc<Notify> {
        self.waiters
            .entry(topic.to_vec())
            .or_insert_with(|| Arc::new(Notify::new()))
            .clone()
    }
}

/// Create the default notification bus (in-memory, single-node).
pub fn default_notification_bus() -> Arc<dyn NotificationBus> {
    Arc::new(InMemoryNotificationBus::new())
}

// ── Tests ───────────────────────────────────────────────────────────────────

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

    #[tokio::test]
    async fn publish_wakes_subscriber() {
        let bus = InMemoryNotificationBus::new();
        let topic = b"user:alice";

        let notify = bus.subscribe(topic);
        let notified = notify.notified();

        // Publish from another "node" (same process in this case).
        bus.publish(topic);

        // Should resolve immediately since we published.
        tokio::time::timeout(Duration::from_millis(100), notified)
            .await
            .expect("notification should arrive");
    }

    #[tokio::test]
    async fn no_publish_times_out() {
        let bus = InMemoryNotificationBus::new();
        let topic = b"user:bob";

        let notify = bus.subscribe(topic);
        let notified = notify.notified();

        let result = tokio::time::timeout(Duration::from_millis(50), notified).await;
        assert!(result.is_err(), "should time out without publish");
    }

    #[tokio::test]
    async fn independent_topics() {
        let bus = InMemoryNotificationBus::new();

        let notify_a = bus.subscribe(b"topic-a");
        let notified_a = notify_a.notified();

        let notify_b = bus.subscribe(b"topic-b");
        let notified_b = notify_b.notified();

        // Only publish to topic-a.
        bus.publish(b"topic-a");

        tokio::time::timeout(Duration::from_millis(100), notified_a)
            .await
            .expect("topic-a should wake");

        let result = tokio::time::timeout(Duration::from_millis(50), notified_b).await;
        assert!(result.is_err(), "topic-b should not wake");
    }
}
