//! Low-level Postgres CRUD over `kv_entries`. Stays storage-only;
//! authorization, event emission, and empty-collection validation live
//! one layer up in `KvServiceImpl`.

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

#[derive(Debug, thiserror::Error)]
pub enum KvRepoError {
    #[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 KvRepo: Send + Sync {
    async fn get(
        &self,
        app_id: AppId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, KvRepoError>;

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

    /// 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. A single SQL statement, so it is atomic without a
    /// transaction.
    async fn set_if(
        &self,
        app_id: AppId,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, KvRepoError>;

    /// Returns the deleted value if present, `None` if the row didn't
    /// exist. The caller turns the `bool was-present` part into the
    /// SDK's return value; the `Option<value>` part feeds the
    /// `old_payload` field of the emitted delete event.
    async fn delete(
        &self,
        app_id: AppId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, KvRepoError>;

    async fn has(&self, app_id: AppId, collection: &str, key: &str) -> Result<bool, KvRepoError>;

    async fn list(
        &self,
        app_id: AppId,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, KvRepoError>;
}

pub struct PostgresKvRepo {
    pool: PgPool,
}

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

/// Hard ceiling on `list` page size — scripts that pass anything larger
/// silently get clamped to this. Cursor-style pagination keeps a single
/// request bounded; clients fetch the next page via the returned cursor.
const KV_LIST_MAX_LIMIT: u32 = 1_000;
const KV_LIST_DEFAULT_LIMIT: u32 = 100;

// ----------------------------------------------------------------------------
// Connection-scoped mutations
//
// Generic over the executor so the same SQL serves the pooled `KvRepo` methods
// above and `crate::atomic_write`, which runs the write and the resulting
// trigger fan-out on ONE connection inside ONE transaction.
// ----------------------------------------------------------------------------

/// Upsert. Returns the previous value, so the caller knows whether this was an
/// `insert` or an `update` for the emitted `ServiceEvent`.
pub(crate) async fn set_on<'c, E>(
    exec: E,
    app_id: AppId,
    collection: &str,
    key: &str,
    value: serde_json::Value,
) -> Result<Option<serde_json::Value>, KvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    // `RETURNING` after `ON CONFLICT DO UPDATE` can't see the old value, so
    // capture the prior value in a CTE alongside the upsert.
    let row: Option<(Option<serde_json::Value>,)> = sqlx::query_as(
        "WITH prev AS (\
            SELECT value FROM kv_entries \
            WHERE app_id = $1 AND collection = $2 AND key = $3\
         ), \
         upserted AS (\
            INSERT INTO kv_entries (app_id, collection, key, value) \
            VALUES ($1, $2, $3, $4) \
            ON CONFLICT (app_id, collection, key) DO UPDATE \
              SET value = EXCLUDED.value, updated_at = NOW() \
            RETURNING 1\
         ) \
         SELECT (SELECT value FROM prev) FROM upserted",
    )
    .bind(app_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,
    app_id: AppId,
    collection: &str,
    key: &str,
    expected: Option<serde_json::Value>,
    new: serde_json::Value,
) -> Result<bool, KvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let affected = match expected {
        // Swap iff the current value matches (JSONB `=` is semantic equality).
        Some(exp) => sqlx::query(
            "UPDATE kv_entries SET value = $4, updated_at = NOW() \
                 WHERE app_id = $1 AND collection = $2 AND key = $3 AND value = $5",
        )
        .bind(app_id.into_inner())
        .bind(collection)
        .bind(key)
        .bind(new)
        .bind(exp)
        .execute(exec)
        .await?
        .rows_affected(),
        // Insert iff absent.
        None => sqlx::query(
            "INSERT INTO kv_entries (app_id, collection, key, value) \
                 VALUES ($1, $2, $3, $4) \
                 ON CONFLICT (app_id, collection, key) DO NOTHING",
        )
        .bind(app_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,
    app_id: AppId,
    collection: &str,
    key: &str,
) -> Result<Option<serde_json::Value>, KvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let row: Option<(serde_json::Value,)> = sqlx::query_as(
        "DELETE FROM kv_entries \
         WHERE app_id = $1 AND collection = $2 AND key = $3 \
         RETURNING value",
    )
    .bind(app_id.into_inner())
    .bind(collection)
    .bind(key)
    .fetch_optional(exec)
    .await?;
    Ok(row.map(|(v,)| v))
}

#[async_trait]
impl KvRepo for PostgresKvRepo {
    async fn get(
        &self,
        app_id: AppId,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, KvRepoError> {
        let row: Option<(serde_json::Value,)> = sqlx::query_as(
            "SELECT value FROM kv_entries \
             WHERE app_id = $1 AND collection = $2 AND key = $3",
        )
        .bind(app_id.into_inner())
        .bind(collection)
        .bind(key)
        .fetch_optional(&self.pool)
        .await?;
        Ok(row.map(|(v,)| v))
    }

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

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

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

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

    async fn list(
        &self,
        app_id: AppId,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, KvRepoError> {
        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,
        };

        // Keyset pagination: rows beyond `last_key` ordered by key.
        // `+1` to detect a "more pages" condition without a separate
        // COUNT query.
        let take = i64::from(limit) + 1;
        let rows: Vec<(String,)> = sqlx::query_as(
            "SELECT key FROM kv_entries \
             WHERE app_id = $1 AND collection = $2 \
               AND ($3::text IS NULL OR key > $3) \
             ORDER BY key ASC \
             LIMIT $4",
        )
        .bind(app_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 })
    }
}

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

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

/// Total key count for the app, across all its collections. Backs the per-app
/// row ceiling; an index-only count, and only run when a write ADDS a key.
pub(crate) async fn count_rows_on<'c, E>(exec: E, app_id: AppId) -> Result<u64, KvRepoError>
where
    E: sqlx::PgExecutor<'c>,
{
    let (n,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM kv_entries WHERE app_id = $1")
        .bind(app_id.into_inner())
        .fetch_one(exec)
        .await?;
    Ok(u64::try_from(n).unwrap_or(0))
}
