//! `GroupKvService` — the §11.6 shared group-collection KV contract.
//!
//! The cross-app-sharing counterpart to [`crate::KvService`]. A script reaches
//! it via the explicit `kv::shared_collection("name")` handle (distinct from the
//! app-private `kv::collection("name")`). Unlike `KvService`, 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 group-shared. That ancestry walk is the isolation boundary —
//! a foreign app's chain never contains the owning group, so the name does not
//! resolve. The trait surface therefore takes **no** group id (same discipline
//! as `KvService` omitting `app_id`): owner derivation stays inside the impl.
//!
//! Trust model (§11.6): **reads are open** to any script in the subtree
//! (anonymous public HTTP included — the operator opted in by declaring the
//! collection); **writes require an authenticated principal** with editor+ on
//! the owning group.

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

use crate::{KvListPage, SdkCallCx};

#[async_trait]
pub trait GroupKvService: Send + Sync {
    async fn get(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvError>;

    async fn set(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
        value: serde_json::Value,
    ) -> Result<(), GroupKvError>;

    /// Compare-and-swap on a shared-collection key. Atomically writes `new` only
    /// if the current value equals `expected` — or, when `expected` is `None`,
    /// only if the key is ABSENT. Returns `true` on swap, `false` if the
    /// precondition failed. Same reads-open / writes-authed + quota rules as
    /// [`Self::set`]. Default: unsupported.
    async fn set_if(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, GroupKvError> {
        let _ = (cx, collection, key, expected, new);
        Err(GroupKvError::Backend(
            "set_if is not supported by this group-kv implementation".into(),
        ))
    }

    async fn delete(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
    ) -> Result<bool, GroupKvError>;

    async fn has(&self, cx: &SdkCallCx, collection: &str, key: &str) -> Result<bool, GroupKvError>;

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

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

#[async_trait]
impl GroupKvService for NoopGroupKvService {
    async fn get(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvError> {
        Err(GroupKvError::Backend("group kv is not wired in".into()))
    }

    async fn set(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _key: &str,
        _value: serde_json::Value,
    ) -> Result<(), GroupKvError> {
        Err(GroupKvError::Backend("group kv is not wired in".into()))
    }

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

    async fn has(
        &self,
        _cx: &SdkCallCx,
        _collection: &str,
        _key: &str,
    ) -> Result<bool, GroupKvError> {
        Err(GroupKvError::Backend("group kv is not wired in".into()))
    }

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

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

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

    /// Caller lacked the required capability. 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 — shared mutation always needs
    /// an authenticated editor+ on the owning group).
    #[error("forbidden")]
    Forbidden,

    /// JSON-encoded value exceeded the per-value `max_value_bytes` cap
    /// (`PICLOUD_KV_MAX_VALUE_BYTES`).
    #[error("group kv: value too large ({actual} bytes; limit {limit})")]
    ValueTooLarge { limit: usize, actual: usize },

    /// The owning group's shared-KV store is at its row quota
    /// (`PICLOUD_GROUP_KV_MAX_ROWS`); a new key is rejected (an update of an
    /// existing key is exempt).
    #[error("group kv: quota exceeded ({actual} rows; limit {limit})")]
    QuotaExceeded { limit: usize, actual: usize },

    /// The owning group's shared-KV store would exceed its total-bytes quota
    /// (`PICLOUD_GROUP_KV_MAX_TOTAL_BYTES`). `actual` is the projected total
    /// (old value's bytes subtracted, new value's added).
    #[error("group kv: total-bytes quota exceeded ({actual} bytes; limit {limit})")]
    TotalBytesQuotaExceeded { limit: u64, actual: u64 },

    /// Anything else — Postgres unavailable, serialization failure, etc.
    #[error("group kv backend error: {0}")]
    Backend(String),
}
