//! `GroupPubsubService` — the §11.6 D2 shared group-TOPIC publish contract.
//!
//! The pub/sub counterpart to [`crate::GroupKvService`]. A script reaches it via
//! the explicit `pubsub::shared_topic("name")` handle (distinct from the app-
//! private `pubsub::publish_durable`). A shared topic is **storeless**: there is
//! no message log — a publish fans out to the `shared = true` pubsub triggers on
//! the OWNING GROUP (resolved by walking the calling app's ancestor chain for
//! the nearest group that declares the topic shared). That ancestry walk is the
//! isolation boundary — a foreign app's chain never contains the owning group,
//! so the topic does not resolve. The trait takes **no** group id (owner
//! derivation stays inside the impl), matching `GroupKvService`.
//!
//! Trust model (§11.6): a publish is a WRITE to shared state, so it requires an
//! authenticated principal with editor+ on the owning group (fails closed for
//! an anonymous caller), matching `GroupKvService` writes.

use async_trait::async_trait;
use thiserror::Error;

use crate::SdkCallCx;

#[async_trait]
pub trait GroupPubsubService: Send + Sync {
    /// Publish `message` to the group shared topic `namespace` (optionally under
    /// `subtopic`, so `("events", "created")` publishes `events.created`).
    /// Resolves the owning group (kind `topic`) from `cx.app_id`'s chain,
    /// requires editor+, and fans out to `shared = true` pubsub triggers on that
    /// group. Returns the number of delivery rows written (0 when no trigger
    /// matched — the publish still succeeds).
    async fn publish(
        &self,
        cx: &SdkCallCx,
        namespace: &str,
        subtopic: &str,
        message: serde_json::Value,
    ) -> Result<u32, GroupPubsubError>;
}

/// Stub for test bundles that don't exercise shared topics. Every call errors so
/// accidental use surfaces clearly.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopGroupPubsubService;

#[async_trait]
impl GroupPubsubService for NoopGroupPubsubService {
    async fn publish(
        &self,
        _cx: &SdkCallCx,
        _namespace: &str,
        _subtopic: &str,
        _message: serde_json::Value,
    ) -> Result<u32, GroupPubsubError> {
        Err(GroupPubsubError::Backend(
            "group pubsub is not wired in".into(),
        ))
    }
}

/// Failure modes surfaced to the Rhai bridge.
#[derive(Debug, Error)]
pub enum GroupPubsubError {
    /// Empty topic namespace; rejected at the SDK boundary.
    #[error("shared topic name must not be empty")]
    InvalidTopic,

    /// No group on the calling app's ancestor chain declares this topic shared —
    /// the structural "not shared with you" boundary. Also the outcome for a
    /// foreign app naming another subtree's topic.
    #[error("topic {0:?} is not a shared group topic for this app")]
    CollectionNotShared(String),

    /// Caller lacked the required capability. A publish always needs an
    /// authenticated editor+ on the owning group (fails closed for anon).
    #[error("forbidden")]
    Forbidden,

    /// Message exceeds the per-message JSON-encoded size cap.
    #[error("message too large: {actual} bytes exceeds the {limit}-byte cap")]
    MessageTooLarge { limit: usize, actual: usize },

    /// Storage / backend failure.
    #[error("backend error: {0}")]
    Backend(String),
}
