//! Low-level Postgres CRUD over `group_kv_entries` (§11.6). A near-clone of
//! [`crate::kv_repo`] keyed by the owning `group_id` instead of `app_id` —
//! authorization, group resolution, event policy, and empty-collection
//! validation live one layer up in `GroupKvServiceImpl`.

use async_trait::async_trait;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use picloud_shared::{GroupId, KvListPage};
use sqlx::PgPool;

#[derive(Debug, thiserror::Error)]
pub enum GroupKvRepoError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),

    #[error("invalid pagination cursor")]
    InvalidCursor,
}

/// Repo surface. The trait is exposed so tests can substitute an in-memory
/// backing without spinning up Postgres.
#[async_trait]
pub trait GroupKvRepo: Send + Sync {
    async fn get(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError>;

    /// Upserts the row. Returns the previous value (if any) so callers can
    /// determine whether this was an `insert` or an `update`.
    async fn set(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        value: serde_json::Value,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError>;

    /// Compare-and-swap. Atomically writes `new` iff the current value equals
    /// `expected` (`expected = None` → iff the key is ABSENT). Returns `true`
    /// when the write happened. One SQL statement — atomic without a tx.
    async fn set_if(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, GroupKvRepoError>;

    /// Returns the deleted value if present, `None` if the row didn't exist.
    async fn delete(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError>;

    async fn has(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<bool, GroupKvRepoError>;

    async fn list(
        &self,
        group_id: GroupId,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, GroupKvRepoError>;

    /// §11.6 quota: total key count across all of the group's shared-KV
    /// collections. Default `Ok(0)` so non-Postgres impls skip the quota check.
    async fn count_rows(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
        let _ = group_id;
        Ok(0)
    }

    /// §11.6 M4 quota: total stored JSON bytes across all of the group's shared-KV
    /// collections (`octet_length(value::text)`). Default `Ok(0)` so non-Postgres
    /// impls skip the byte-quota check.
    async fn total_bytes(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
        let _ = group_id;
        Ok(0)
    }

    /// §11.6 M4 quota: the PROJECTED total stored bytes for the group AFTER
    /// writing `new_value` at `(collection, key)` — i.e. current SUM minus the
    /// existing row's bytes (if any) plus the new value's bytes. Computed
    /// entirely in SQL so ALL three terms use the SAME canonical metric
    /// (`octet_length(value::text)`); the Rust-side subtract/add previously
    /// mixed the DB canonical SUM with a `serde_json` compact length, which
    /// drifted the ceiling permissive. Default `Ok(0)` (non-Postgres impls skip
    /// the byte quota).
    async fn projected_total_bytes(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        new_value: &serde_json::Value,
    ) -> Result<u64, GroupKvRepoError> {
        let _ = (group_id, collection, key, new_value);
        Ok(0)
    }
}

pub struct PostgresGroupKvRepo {
    pool: PgPool,
}

impl PostgresGroupKvRepo {
    #[must_use]
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

/// Hard ceiling on `list` page size (mirrors `kv_repo`).
const KV_LIST_MAX_LIMIT: u32 = 1_000;
const KV_LIST_DEFAULT_LIMIT: u32 = 100;

#[async_trait]
impl GroupKvRepo for PostgresGroupKvRepo {
    async fn get(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
        get_on(&self.pool, group_id, collection, key).await
    }

    async fn set(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        value: serde_json::Value,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
        set_on(&self.pool, group_id, collection, key, value).await
    }

    async fn set_if(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, GroupKvRepoError> {
        set_if_on(&self.pool, group_id, collection, key, expected, new).await
    }

    async fn delete(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
        delete_on(&self.pool, group_id, collection, key).await
    }

    async fn has(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
    ) -> Result<bool, GroupKvRepoError> {
        let row: Option<(i64,)> = sqlx::query_as(
            "SELECT 1 FROM group_kv_entries \
             WHERE group_id = $1 AND collection = $2 AND key = $3",
        )
        .bind(group_id.into_inner())
        .bind(collection)
        .bind(key)
        .fetch_optional(&self.pool)
        .await?;
        Ok(row.is_some())
    }

    async fn list(
        &self,
        group_id: GroupId,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, GroupKvRepoError> {
        let limit = if limit == 0 {
            KV_LIST_DEFAULT_LIMIT
        } else {
            limit.min(KV_LIST_MAX_LIMIT)
        };

        let last_key = match cursor {
            Some(c) => Some(decode_cursor(c)?),
            None => None,
        };

        let take = i64::from(limit) + 1;
        let rows: Vec<(String,)> = sqlx::query_as(
            "SELECT key FROM group_kv_entries \
             WHERE group_id = $1 AND collection = $2 \
               AND ($3::text IS NULL OR key > $3) \
             ORDER BY key ASC \
             LIMIT $4",
        )
        .bind(group_id.into_inner())
        .bind(collection)
        .bind(last_key.as_deref())
        .bind(take)
        .fetch_all(&self.pool)
        .await?;

        let mut keys: Vec<String> = rows.into_iter().map(|(k,)| k).collect();
        let next_cursor = if keys.len() > limit as usize {
            keys.truncate(limit as usize);
            keys.last().map(|k| encode_cursor(k))
        } else {
            None
        };

        Ok(KvListPage { keys, next_cursor })
    }

    async fn count_rows(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
        count_rows_on(&self.pool, group_id).await
    }

    async fn total_bytes(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
        let (n,): (i64,) = sqlx::query_as(
            "SELECT COALESCE(SUM(octet_length(value::text)), 0)::BIGINT \
             FROM group_kv_entries WHERE group_id = $1",
        )
        .bind(group_id.into_inner())
        .fetch_one(&self.pool)
        .await?;
        Ok(u64::try_from(n).unwrap_or(0))
    }

    async fn projected_total_bytes(
        &self,
        group_id: GroupId,
        collection: &str,
        key: &str,
        new_value: &serde_json::Value,
    ) -> Result<u64, GroupKvRepoError> {
        projected_total_bytes_on(&self.pool, group_id, collection, key, new_value).await
    }
}

// ----------------------------------------------------------------------------
// Connection-scoped operations
//
// Generic over the executor so the same SQL serves the pooled trait methods
// above and `crate::atomic_write`, which runs the per-group advisory lock, the
// quota reads, the write, and the shared-trigger fan-out on ONE connection
// inside ONE transaction. The quota reads in particular MUST run on the writing
// transaction's connection — that is what makes check-then-write atomic rather
// than a race (audit #8).
// ----------------------------------------------------------------------------

pub(crate) async fn get_on<'c, E>(
    exec: E,
    group_id: GroupId,
    collection: &str,
    key: &str,
) -> Result<Option<serde_json::Value>, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let row: Option<(serde_json::Value,)> = sqlx::query_as(
        "SELECT value FROM group_kv_entries \
         WHERE group_id = $1 AND collection = $2 AND key = $3",
    )
    .bind(group_id.into_inner())
    .bind(collection)
    .bind(key)
    .fetch_optional(exec)
    .await?;
    Ok(row.map(|(v,)| v))
}

/// Upsert. Returns the previous value (`None` = this was an insert).
pub(crate) async fn set_on<'c, E>(
    exec: E,
    group_id: GroupId,
    collection: &str,
    key: &str,
    value: serde_json::Value,
) -> Result<Option<serde_json::Value>, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let row: Option<(Option<serde_json::Value>,)> = sqlx::query_as(
        "WITH prev AS (\
            SELECT value FROM group_kv_entries \
            WHERE group_id = $1 AND collection = $2 AND key = $3\
         ), \
         upserted AS (\
            INSERT INTO group_kv_entries (group_id, collection, key, value) \
            VALUES ($1, $2, $3, $4) \
            ON CONFLICT (group_id, collection, key) DO UPDATE \
              SET value = EXCLUDED.value, updated_at = NOW() \
            RETURNING 1\
         ) \
         SELECT (SELECT value FROM prev) FROM upserted",
    )
    .bind(group_id.into_inner())
    .bind(collection)
    .bind(key)
    .bind(value)
    .fetch_optional(exec)
    .await?;
    Ok(row.and_then(|(v,)| v))
}

/// Compare-and-swap. Writes `new` iff the current value equals `expected`
/// (`expected = None` → iff the key is ABSENT). Returns whether it wrote.
pub(crate) async fn set_if_on<'c, E>(
    exec: E,
    group_id: GroupId,
    collection: &str,
    key: &str,
    expected: Option<serde_json::Value>,
    new: serde_json::Value,
) -> Result<bool, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let affected = match expected {
        Some(exp) => sqlx::query(
            "UPDATE group_kv_entries SET value = $4, updated_at = NOW() \
                 WHERE group_id = $1 AND collection = $2 AND key = $3 AND value = $5",
        )
        .bind(group_id.into_inner())
        .bind(collection)
        .bind(key)
        .bind(new)
        .bind(exp)
        .execute(exec)
        .await?
        .rows_affected(),
        None => sqlx::query(
            "INSERT INTO group_kv_entries (group_id, collection, key, value) \
                 VALUES ($1, $2, $3, $4) \
                 ON CONFLICT (group_id, collection, key) DO NOTHING",
        )
        .bind(group_id.into_inner())
        .bind(collection)
        .bind(key)
        .bind(new)
        .execute(exec)
        .await?
        .rows_affected(),
    };
    Ok(affected == 1)
}

/// Returns the deleted value if present, `None` if the row didn't exist.
pub(crate) async fn delete_on<'c, E>(
    exec: E,
    group_id: GroupId,
    collection: &str,
    key: &str,
) -> Result<Option<serde_json::Value>, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let row: Option<(serde_json::Value,)> = sqlx::query_as(
        "DELETE FROM group_kv_entries \
         WHERE group_id = $1 AND collection = $2 AND key = $3 \
         RETURNING value",
    )
    .bind(group_id.into_inner())
    .bind(collection)
    .bind(key)
    .fetch_optional(exec)
    .await?;
    Ok(row.map(|(v,)| v))
}

/// Total key count across the group's shared-KV collections (§11.6 row quota).
pub(crate) async fn count_rows_on<'c, E>(
    exec: E,
    group_id: GroupId,
) -> Result<u64, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let (n,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM group_kv_entries WHERE group_id = $1")
        .bind(group_id.into_inner())
        .fetch_one(exec)
        .await?;
    Ok(u64::try_from(n).unwrap_or(0))
}

/// §11.6 M4 quota: the PROJECTED total stored bytes for the group AFTER writing
/// `new_value` at `(collection, key)` — current SUM, minus the existing row's
/// bytes (0 if the key is new), plus the new value's bytes. Computed entirely in
/// SQL so all three terms use the SAME canonical metric
/// (`octet_length(value::text)`); mixing the DB's canonical SUM with a
/// `serde_json` compact length drifts the ceiling permissive.
pub(crate) async fn projected_total_bytes_on<'c, E>(
    exec: E,
    group_id: GroupId,
    collection: &str,
    key: &str,
    new_value: &serde_json::Value,
) -> Result<u64, GroupKvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    // The new value is bound as compact TEXT and re-parsed through `::jsonb::text`
    // so PG measures it in the same canonical form it stores.
    // A `serde_json::Value` always serializes; fall back defensively.
    let new_text = serde_json::to_string(new_value).unwrap_or_else(|_| "null".to_string());
    let (n,): (i64,) = sqlx::query_as(
        "SELECT ( \
            COALESCE((SELECT SUM(octet_length(value::text)) \
                      FROM group_kv_entries WHERE group_id = $1), 0) \
          - COALESCE((SELECT octet_length(value::text) \
                      FROM group_kv_entries \
                      WHERE group_id = $1 AND collection = $2 AND key = $3), 0) \
          + octet_length($4::jsonb::text) \
         )::BIGINT",
    )
    .bind(group_id.into_inner())
    .bind(collection)
    .bind(key)
    .bind(new_text)
    .fetch_one(exec)
    .await?;
    Ok(u64::try_from(n).unwrap_or(0))
}

fn encode_cursor(last_key: &str) -> String {
    URL_SAFE_NO_PAD.encode(last_key.as_bytes())
}

fn decode_cursor(cursor: &str) -> Result<String, GroupKvRepoError> {
    let bytes = URL_SAFE_NO_PAD
        .decode(cursor)
        .map_err(|_| GroupKvRepoError::InvalidCursor)?;
    String::from_utf8(bytes).map_err(|_| GroupKvRepoError::InvalidCursor)
}
