//! `GroupDocsService` — the §11.6 shared group-collection DOCS contract.
//!
//! The cross-app-sharing counterpart to [`crate::DocsService`], analogous to
//! [`crate::GroupKvService`]. A script reaches it via the explicit
//! `docs::shared_collection("name")` handle. The owner of the data is not
//! `cx.app_id`: the impl resolves the **owning group** by walking the calling
//! app's ancestor chain for the nearest group that declares the collection
//! shared with `kind='docs'`. That ancestry walk is the isolation boundary —
//! the trait surface takes **no** group id (owner derivation stays in the impl).
//!
//! Trust model (§11.6): **reads are open** to any subtree script (anonymous
//! public HTTP included); **writes require an authenticated principal** with
//! editor+ on the owning group.

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

use crate::{DocId, DocRow, DocsListPage, SdkCallCx};

#[async_trait]
pub trait GroupDocsService: Send + Sync {
    async fn create(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        data: serde_json::Value,
    ) -> Result<DocId, GroupDocsError>;

    async fn get(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        id: DocId,
    ) -> Result<Option<DocRow>, GroupDocsError>;

    async fn find(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        filter: serde_json::Value,
    ) -> Result<Vec<DocRow>, GroupDocsError>;

    async fn find_one(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        filter: serde_json::Value,
    ) -> Result<Option<DocRow>, GroupDocsError>;

    async fn update(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        id: DocId,
        data: serde_json::Value,
    ) -> Result<(), GroupDocsError>;

    async fn delete(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        id: DocId,
    ) -> Result<bool, GroupDocsError>;

    async fn list(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<DocsListPage, GroupDocsError>;
}

/// Stub for test bundles that don't exercise shared docs collections.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopGroupDocsService;

#[async_trait]
impl GroupDocsService for NoopGroupDocsService {
    async fn create(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _data: serde_json::Value,
    ) -> Result<DocId, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn get(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _id: DocId,
    ) -> Result<Option<DocRow>, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn find(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _filter: serde_json::Value,
    ) -> Result<Vec<DocRow>, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn find_one(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _filter: serde_json::Value,
    ) -> Result<Option<DocRow>, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn update(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _id: DocId,
        _data: serde_json::Value,
    ) -> Result<(), GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn delete(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _id: DocId,
    ) -> Result<bool, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }

    async fn list(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _cursor: Option<&str>,
        _limit: u32,
    ) -> Result<DocsListPage, GroupDocsError> {
        Err(GroupDocsError::Backend("group docs is not wired in".into()))
    }
}

/// Failure modes surfaced to the Rhai bridge. Mirrors [`crate::DocsError`] plus
/// the §11.6 `CollectionNotShared` boundary.
#[derive(Debug, Error)]
pub enum GroupDocsError {
    #[error("collection name must not be empty")]
    InvalidCollection,

    /// No group on the calling app's ancestor chain declares this collection
    /// shared with `kind='docs'` — the structural "not shared with you"
    /// boundary (also the outcome for a foreign app).
    #[error("collection {0:?} is not a shared group docs collection for this app")]
    CollectionNotShared(String),

    #[error("document data must be a JSON object")]
    InvalidData,

    #[error("invalid filter: {0}")]
    InvalidFilter(String),

    #[error("unsupported operator: {0}")]
    UnsupportedOperator(String),

    #[error("document not found")]
    NotFound,

    /// For reads this is only raised when `cx.principal.is_some()`; for WRITES
    /// it is also raised when the principal is absent (writes fail closed).
    #[error("forbidden")]
    Forbidden,

    #[error("group docs: data too large ({actual} bytes; limit {limit})")]
    ValueTooLarge { limit: usize, actual: usize },

    /// The owning group's shared-docs store is at its row quota
    /// (`PICLOUD_GROUP_DOCS_MAX_ROWS`); a new doc is rejected.
    #[error("group docs: quota exceeded ({actual} rows; limit {limit})")]
    QuotaExceeded { limit: usize, actual: usize },

    /// The owning group's shared-docs store would exceed its total-bytes quota
    /// (`PICLOUD_GROUP_DOCS_MAX_TOTAL_BYTES`). `actual` is the projected total.
    #[error("group docs: total-bytes quota exceeded ({actual} bytes; limit {limit})")]
    TotalBytesQuotaExceeded { limit: u64, actual: u64 },

    #[error("group docs backend error: {0}")]
    Backend(String),
}
