//! `RealtimeAuthority` — the SSE subscribe authorization seam (v1.1.6).
//!
//! The SSE endpoint (`GET /realtime/topics/{topic}`) is a data-plane
//! surface in orchestrator-core, but deciding whether a subscribe is
//! allowed needs a `topics` table read plus (for token-gated topics) an
//! HMAC verify against the app's signing key — both of which require DB
//! access and the signing-key material that must NOT leak into the
//! data-plane crate. This trait keeps all of that inside the manager-core
//! impl: orchestrator-core only ever sees the three-way verdict below.
//!
//! `NotFound` is deliberately returned for *both* "no such topic" and
//! "topic exists but isn't externally subscribable" so the endpoint
//! can't be used to probe which internal topics exist (design notes §5).

use async_trait::async_trait;

use crate::{AppId, GroupId};

/// Why a subscribe attempt was refused. The SSE handler maps these to
/// HTTP status codes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubscribeDenied {
    /// No externally-subscribable topic by that name in this app → 404.
    /// Used for genuinely-missing topics AND internal-only ones, so the
    /// endpoint doesn't leak which internal topics exist.
    NotFound,
    /// The topic is token-gated and the presented token was missing,
    /// malformed, badly signed, expired, or not scoped to this topic →
    /// 401 (generic; never says which check failed).
    Unauthorized,
    /// Backend failure (DB unavailable, etc.) → 500.
    Backend(String),
}

#[async_trait]
pub trait RealtimeAuthority: Send + Sync {
    /// Decide whether an external client may subscribe to
    /// `(app_id, topic)`. `token` is the bearer/query token if the
    /// client presented one (`None` otherwise).
    ///
    /// Returns `Ok(())` when the subscribe is permitted (public topic,
    /// or token-gated topic with a valid token scoped to it).
    ///
    /// # Errors
    ///
    /// [`SubscribeDenied`] — see the variants for the status mapping.
    async fn authorize_subscribe(
        &self,
        app_id: AppId,
        topic: &str,
        token: Option<&str>,
    ) -> Result<(), SubscribeDenied>;

    /// §11.6 D2: decide whether a subscriber reaching the platform via `app_id`'s
    /// host may subscribe to the group SHARED topic `topic`. Consistent with the
    /// shared-collection trust model, **reads are open**: the authorization IS the
    /// owning-group resolution — a group on `app_id`'s ancestor chain must declare
    /// `topic`'s root segment a shared `kind='topic'` collection. Returns that
    /// OWNING GROUP's id (so the caller subscribes to the group channel), or
    /// `NotFound` when no ancestor group declares it (the chain walk is the
    /// isolation boundary — a foreign-subtree app never resolves it). Default:
    /// `NotFound`.
    async fn authorize_subscribe_shared(
        &self,
        app_id: AppId,
        topic: &str,
    ) -> Result<GroupId, SubscribeDenied> {
        let _ = (app_id, topic);
        Err(SubscribeDenied::NotFound)
    }
}

/// Bootstrap impl: denies everything as `NotFound`. Replaced in
/// `build_app` with the manager-core DB-backed authority.
#[derive(Debug, Default, Clone, Copy)]
pub struct DenyAllRealtimeAuthority;

#[async_trait]
impl RealtimeAuthority for DenyAllRealtimeAuthority {
    async fn authorize_subscribe(
        &self,
        _app_id: AppId,
        _topic: &str,
        _token: Option<&str>,
    ) -> Result<(), SubscribeDenied> {
        Err(SubscribeDenied::NotFound)
    }
}
