//! `GroupQueueService` — the §11.6 D3 shared group-QUEUE contract.
//!
//! The durable-queue counterpart to [`crate::GroupKvService`]. A script reaches
//! it via the explicit `queue::shared_collection("name")` handle (distinct from
//! the app-private `queue::enqueue`). Any subtree app ENQUEUES into one group-
//! owned store; CONSUMPTION is by competing per-descendant materialized
//! consumers (handled outside this trait, in the dispatcher). The impl resolves
//! the OWNING GROUP by walking the calling app's ancestor chain for the nearest
//! group that declares the queue shared — the isolation boundary.
//!
//! Trust model (§11.6): enqueue 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::{EnqueueOpts, QueueMessageId, SdkCallCx};

#[async_trait]
pub trait GroupQueueService: Send + Sync {
    /// Enqueue `payload` into the group shared queue `collection`. Resolves the
    /// owning group (kind `queue`) from `cx.app_id`'s chain and requires editor+.
    async fn enqueue(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        payload: serde_json::Value,
        opts: EnqueueOpts,
    ) -> Result<QueueMessageId, GroupQueueError>;

    /// Total rows in the shared queue.
    async fn depth(&self, cx: &SdkCallCx, collection: &str) -> Result<u64, GroupQueueError>;

    /// Currently-claimable rows in the shared queue.
    async fn depth_pending(&self, cx: &SdkCallCx, collection: &str)
        -> Result<u64, GroupQueueError>;
}

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

#[async_trait]
impl GroupQueueService for NoopGroupQueueService {
    async fn enqueue(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _payload: serde_json::Value,
        _opts: EnqueueOpts,
    ) -> Result<QueueMessageId, GroupQueueError> {
        Err(GroupQueueError::Backend(
            "group queue is not wired in".into(),
        ))
    }

    async fn depth(&self, _cx: &SdkCallCx, _collection: &str) -> Result<u64, GroupQueueError> {
        Err(GroupQueueError::Backend(
            "group queue is not wired in".into(),
        ))
    }

    async fn depth_pending(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
    ) -> Result<u64, GroupQueueError> {
        Err(GroupQueueError::Backend(
            "group queue is not wired in".into(),
        ))
    }
}

/// Failure modes surfaced to the Rhai bridge.
#[derive(Debug, Error)]
pub enum GroupQueueError {
    /// Empty collection name; rejected at the SDK boundary.
    #[error("queue name must not be empty")]
    InvalidCollection,

    /// No group on the calling app's ancestor chain declares this queue shared.
    #[error("queue {0:?} is not a shared group queue for this app")]
    CollectionNotShared(String),

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

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

    /// Invalid enqueue options (bad max_attempts / delay_ms).
    #[error("{0}")]
    InvalidOpts(String),

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