//! `KvService` — the v1.1.1 key-value store contract.
//!
//! Lives in `picloud-shared` (not `executor-core`) so the Rhai bridge,
//! the manager-core Postgres impl, and any future in-memory test impl
//! can all depend on the same trait without dragging
//! `executor-core` into `manager-core`'s dep graph.
//!
//! Implementations MUST derive every storage `app_id` from `cx.app_id`
//! — never from a script-passed argument. That is the cross-app
//! isolation boundary; see `docs/sdk-shape.md`.

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

use crate::SdkCallCx;

/// `KvService` is collection-scoped. Scripts get a handle via
/// `kv::collection(name)` and call `get`/`set`/`has`/`delete`/`list`
/// on it. The trait surface accepts the collection by name so the
/// Postgres impl can avoid an extra round-trip to materialize the
/// collection (collections are namespaces, not first-class rows).
#[async_trait]
pub trait KvService: Send + Sync {
    async fn get(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, KvError>;

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

    /// Compare-and-swap. Atomically writes `new` only if the current value
    /// equals `expected` — or, when `expected` is `None`, only if the key is
    /// ABSENT (insert-if-absent). Returns `true` if the swap happened, `false`
    /// if the precondition failed (value differed / key already present).
    /// Default: unsupported, so only storage-backed impls opt in.
    async fn set_if(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, KvError> {
        let _ = (cx, collection, key, expected, new);
        Err(KvError::Backend(
            "set_if is not supported by this kv implementation".into(),
        ))
    }

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

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

    /// Cursor-style pagination. `cursor` is opaque to the caller;
    /// implementations encode the resume key inside. `None` cursor
    /// starts from the beginning. Implementations cap `limit` at a
    /// reasonable ceiling internally (script can't request an unbounded
    /// page).
    async fn list(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, KvError>;
}

/// One page of keys from `KvService::list`. `next_cursor` is `Some`
/// when more pages exist, `None` when exhausted. The cursor encoding
/// is implementation-defined (the Postgres impl base64-encodes the
/// last key).
#[derive(Debug, Clone)]
pub struct KvListPage {
    pub keys: Vec<String>,
    pub next_cursor: Option<String>,
}

/// Stub used by the test harness so executor-core integration tests
/// (which don't touch KV) can construct a `Services` bundle without
/// spinning up Postgres. Every call returns
/// `KvError::Backend("...")` so accidental KV use surfaces clearly.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopKvService;

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

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

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

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

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

/// Failure modes surfaced to the Rhai bridge. The bridge converts each
/// to a Rhai runtime error string; the discriminants exist so internal
/// callers (admin endpoints, tests, GC) can react more precisely.
#[derive(Debug, Error)]
pub enum KvError {
    /// Empty collection name; rejected at the SDK boundary per
    /// `docs/sdk-shape.md`.
    #[error("collection name must not be empty")]
    InvalidCollection,

    /// Caller principal lacked the required capability. Only raised
    /// when `cx.principal.is_some()` — scripts running with
    /// `principal: None` (public HTTP) operate under script-as-gate
    /// semantics and skip the capability check.
    #[error("forbidden")]
    Forbidden,

    /// The app is at its per-app row ceiling (`PICLOUD_APP_KV_MAX_ROWS`) and
    /// this write would add a key. An update to an existing key is exempt.
    #[error("kv: app key quota exceeded ({actual} keys; limit {limit})")]
    QuotaExceeded { limit: u64, actual: u64 },

    /// JSON-encoded value exceeded the per-app `max_value_bytes` cap.
    /// Defends Postgres JSONB columns from anonymous-public-script DoS;
    /// the limit is configurable via `PICLOUD_KV_MAX_VALUE_BYTES`.
    #[error("kv: value too large ({actual} bytes; limit {limit})")]
    ValueTooLarge { limit: usize, actual: usize },

    /// Anything else — Postgres unavailable, serialization failure,
    /// etc. The string is safe to surface to a script.
    #[error("kv backend error: {0}")]
    Backend(String),
}
