//! Low-level Postgres CRUD over `vars` — the write/admin side of the
//! Phase-3 config layer (the read/resolution side lives in
//! `config_resolver`). Storage-only: it upserts and lists an owner's OWN
//! rows. Authorization, env-scope validation, and value encoding live one
//! layer up in `vars_api`.
//!
//! A var is owned by exactly one group OR one app (the migration's
//! `vars_owner_exactly_one` CHECK). Because the owner is split across two
//! nullable columns (`group_id`, `app_id`), the upsert writes
//! owner-kind-specific SQL with the matching partial-unique conflict
//! target.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use picloud_shared::{AppId, GroupId};
use serde_json::Value as JsonValue;
use sqlx::PgPool;

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

/// Which side of the polymorphic owner a var hangs off. The repo chooses
/// the conflict target (group vs app partial-unique index) from this.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarOwner {
    Group(GroupId),
    App(AppId),
}

/// One of an owner's OWN var rows (NOT a resolved/inherited value). Backs
/// the admin list surface.
#[derive(Debug, Clone)]
pub struct VarRow {
    pub environment_scope: String,
    pub key: String,
    pub value: JsonValue,
    pub is_tombstone: bool,
    pub updated_at: DateTime<Utc>,
}

/// Repo surface. A trait so service/handler tests can substitute an
/// in-memory backing without Postgres.
#[async_trait]
pub trait VarsRepo: Send + Sync {
    /// Upsert one (owner, env_scope, key) row.
    async fn set(
        &self,
        owner: VarOwner,
        env_scope: &str,
        key: &str,
        value: &JsonValue,
        is_tombstone: bool,
    ) -> Result<(), VarsRepoError>;

    /// Delete one (owner, env_scope, key) row; returns whether a row was
    /// present.
    async fn delete(
        &self,
        owner: VarOwner,
        env_scope: &str,
        key: &str,
    ) -> Result<bool, VarsRepoError>;

    /// The owner's OWN rows only (NOT resolved/inherited), ordered by
    /// (key, environment_scope).
    async fn list_for_owner(&self, owner: VarOwner) -> Result<Vec<VarRow>, VarsRepoError>;
}

pub struct PostgresVarsRepo {
    pool: PgPool,
}

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

#[async_trait]
impl VarsRepo for PostgresVarsRepo {
    async fn set(
        &self,
        owner: VarOwner,
        env_scope: &str,
        key: &str,
        value: &JsonValue,
        is_tombstone: bool,
    ) -> Result<(), VarsRepoError> {
        // Owner-kind-specific SQL: only one of the two nullable owner
        // columns is written, and the conflict target is the matching
        // partial-unique index.
        match owner {
            VarOwner::Group(g) => {
                sqlx::query(
                    // The conflict target is a PARTIAL unique index, so the
                    // index predicate (`WHERE group_id IS NOT NULL`) must be
                    // restated for Postgres to infer the arbiter.
                    "INSERT INTO vars (group_id, environment_scope, key, value, is_tombstone) \
                     VALUES ($1, $2, $3, $4, $5) \
                     ON CONFLICT (group_id, environment_scope, key) \
                       WHERE group_id IS NOT NULL DO UPDATE \
                       SET value = EXCLUDED.value, \
                           is_tombstone = EXCLUDED.is_tombstone, \
                           updated_at = NOW()",
                )
                .bind(g.into_inner())
                .bind(env_scope)
                .bind(key)
                .bind(value)
                .bind(is_tombstone)
                .execute(&self.pool)
                .await?;
            }
            VarOwner::App(a) => {
                sqlx::query(
                    // Partial-index conflict target — restate the predicate.
                    "INSERT INTO vars (app_id, environment_scope, key, value, is_tombstone) \
                     VALUES ($1, $2, $3, $4, $5) \
                     ON CONFLICT (app_id, environment_scope, key) \
                       WHERE app_id IS NOT NULL DO UPDATE \
                       SET value = EXCLUDED.value, \
                           is_tombstone = EXCLUDED.is_tombstone, \
                           updated_at = NOW()",
                )
                .bind(a.into_inner())
                .bind(env_scope)
                .bind(key)
                .bind(value)
                .bind(is_tombstone)
                .execute(&self.pool)
                .await?;
            }
        }
        Ok(())
    }

    async fn delete(
        &self,
        owner: VarOwner,
        env_scope: &str,
        key: &str,
    ) -> Result<bool, VarsRepoError> {
        let res = match owner {
            VarOwner::Group(g) => {
                sqlx::query(
                    "DELETE FROM vars \
                     WHERE group_id = $1 AND environment_scope = $2 AND key = $3",
                )
                .bind(g.into_inner())
                .bind(env_scope)
                .bind(key)
                .execute(&self.pool)
                .await?
            }
            VarOwner::App(a) => {
                sqlx::query(
                    "DELETE FROM vars \
                     WHERE app_id = $1 AND environment_scope = $2 AND key = $3",
                )
                .bind(a.into_inner())
                .bind(env_scope)
                .bind(key)
                .execute(&self.pool)
                .await?
            }
        };
        Ok(res.rows_affected() > 0)
    }

    async fn list_for_owner(&self, owner: VarOwner) -> Result<Vec<VarRow>, VarsRepoError> {
        let rows: Vec<(String, String, JsonValue, bool, DateTime<Utc>)> = match owner {
            VarOwner::Group(g) => {
                sqlx::query_as(
                    "SELECT environment_scope, key, value, is_tombstone, updated_at \
                 FROM vars WHERE group_id = $1 \
                 ORDER BY key ASC, environment_scope ASC",
                )
                .bind(g.into_inner())
                .fetch_all(&self.pool)
                .await?
            }
            VarOwner::App(a) => {
                sqlx::query_as(
                    "SELECT environment_scope, key, value, is_tombstone, updated_at \
                 FROM vars WHERE app_id = $1 \
                 ORDER BY key ASC, environment_scope ASC",
                )
                .bind(a.into_inner())
                .fetch_all(&self.pool)
                .await?
            }
        };
        Ok(rows
            .into_iter()
            .map(
                |(environment_scope, key, value, is_tombstone, updated_at)| VarRow {
                    environment_scope,
                    key,
                    value,
                    is_tombstone,
                    updated_at,
                },
            )
            .collect())
    }
}
