//! Group-collection markers (§11.6) — the `group_collections` table (0052).
//!
//! A marker `(owner, name, kind)` declares that a collection name is
//! group-shared. `kind` selects the storage backend: `'kv'` →
//! `group_kv_entries` (0053), `'docs'` → `group_docs` (0054). This module holds
//! the read + transactional-write helpers, the runtime resolver, and the
//! injectable [`GroupCollectionResolver`] trait the per-kind services consume.
//!
//! The load-bearing function is [`resolve_owning_group`]: it walks the reading
//! app's ancestor chain ([`CHAIN_LEVELS_CTE`]) for the nearest group declaring
//! the collection of the requested `kind`. That walk **is** the isolation
//! boundary — a foreign app's chain never contains the owning group, so the
//! name does not resolve.

use async_trait::async_trait;
use picloud_shared::{AppId, GroupId, ScriptOwner};
use sqlx::{PgPool, Postgres, Transaction};

use crate::config_resolver::CHAIN_LEVELS_CTE;
/// List **all** shared-collection declarations at `owner` as `(name, kind)`
/// pairs, sorted. Used by the kind-aware apply diff and `collections ls`.
pub async fn list_all_for_owner(
    pool: &PgPool,
    owner: ScriptOwner,
) -> Result<Vec<(String, String)>, sqlx::Error> {
    let rows: Vec<(String, String)> = match owner {
        ScriptOwner::App(a) => {
            sqlx::query_as(
                "SELECT name, kind FROM group_collections \
                 WHERE app_id = $1 ORDER BY kind, LOWER(name)",
            )
            .bind(a.into_inner())
            .fetch_all(pool)
            .await?
        }
        ScriptOwner::Group(g) => {
            sqlx::query_as(
                "SELECT name, kind FROM group_collections \
                 WHERE group_id = $1 ORDER BY kind, LOWER(name)",
            )
            .bind(g.into_inner())
            .fetch_all(pool)
            .await?
        }
    };
    Ok(rows)
}

/// Resolve the group that OWNS the shared collection `(name, kind)` for a
/// reading app: the nearest ancestor group on the app's chain that declares it.
/// Returns `None` when no group on the chain shares that `(name, kind)` — the
/// structural "not shared with you" boundary. Nearest-wins (CoW shadowing) is
/// enforced by `ORDER BY depth ASC LIMIT 1` and is security-relevant.
///
/// The join is on `group_owner` only: an app-declared marker (the degenerate
/// case) never makes a collection visible to *other* apps — sharing is a group
/// property.
pub async fn resolve_owning_group(
    pool: &PgPool,
    app_id: AppId,
    name: &str,
    kind: &str,
) -> Result<Option<GroupId>, sqlx::Error> {
    let row: Option<(uuid::Uuid,)> = sqlx::query_as(&format!(
        "{CHAIN_LEVELS_CTE} \
         SELECT gc.group_id FROM group_collections gc \
         JOIN chain c ON gc.group_id = c.group_owner \
         WHERE LOWER(gc.name) = LOWER($2) AND gc.kind = $3 \
         ORDER BY c.depth ASC LIMIT 1",
    ))
    .bind(app_id.into_inner())
    .bind(name)
    .bind(kind)
    .fetch_optional(pool)
    .await?;
    Ok(row.map(|(id,)| GroupId::from(id)))
}

/// Insert a `(name, kind)` marker at `owner`, in the apply transaction.
/// Idempotent: a re-apply of an already-declared marker is a no-op
/// (`ON CONFLICT DO NOTHING`), so it survives without a spurious version bump.
pub async fn insert_collection_tx(
    tx: &mut Transaction<'_, Postgres>,
    owner: ScriptOwner,
    name: &str,
    kind: &str,
) -> Result<(), sqlx::Error> {
    match owner {
        ScriptOwner::App(a) => {
            sqlx::query(
                "INSERT INTO group_collections (app_id, name, kind) VALUES ($1, $2, $3) \
                 ON CONFLICT (app_id, LOWER(name), kind) WHERE app_id IS NOT NULL DO NOTHING",
            )
            .bind(a.into_inner())
            .bind(name)
            .bind(kind)
            .execute(&mut **tx)
            .await?;
        }
        ScriptOwner::Group(g) => {
            sqlx::query(
                "INSERT INTO group_collections (group_id, name, kind) VALUES ($1, $2, $3) \
                 ON CONFLICT (group_id, LOWER(name), kind) WHERE group_id IS NOT NULL DO NOTHING",
            )
            .bind(g.into_inner())
            .bind(name)
            .bind(kind)
            .execute(&mut **tx)
            .await?;
        }
    }
    Ok(())
}

/// Delete a `(name, kind)` marker at `owner` (case-insensitive), in the apply
/// transaction. Used by `--prune`. The storage data is NOT dropped here —
/// pruning a marker hides the store but leaves the data until the owning group
/// is deleted.
pub async fn delete_collection_tx(
    tx: &mut Transaction<'_, Postgres>,
    owner: ScriptOwner,
    name: &str,
    kind: &str,
) -> Result<(), sqlx::Error> {
    match owner {
        ScriptOwner::App(a) => {
            sqlx::query(
                "DELETE FROM group_collections \
                 WHERE app_id = $1 AND LOWER(name) = LOWER($2) AND kind = $3",
            )
            .bind(a.into_inner())
            .bind(name)
            .bind(kind)
            .execute(&mut **tx)
            .await?;
        }
        ScriptOwner::Group(g) => {
            sqlx::query(
                "DELETE FROM group_collections \
                 WHERE group_id = $1 AND LOWER(name) = LOWER($2) AND kind = $3",
            )
            .bind(g.into_inner())
            .bind(name)
            .bind(kind)
            .execute(&mut **tx)
            .await?;
        }
    }
    Ok(())
}

/// Resolves a shared-collection name+kind to its owning group for a calling app
/// (nearest ancestor group on the app's chain that declares it). Behind a trait
/// so the per-kind services (`GroupKvServiceImpl`, `GroupDocsServiceImpl`) can
/// inject a fake in unit tests without Postgres.
#[async_trait]
pub trait GroupCollectionResolver: Send + Sync {
    async fn resolve_owning_group(
        &self,
        app_id: AppId,
        name: &str,
        kind: &str,
    ) -> Result<Option<GroupId>, sqlx::Error>;
}

/// Postgres-backed resolver — delegates to [`resolve_owning_group`].
pub struct PostgresGroupCollectionResolver {
    pool: PgPool,
}

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

#[async_trait]
impl GroupCollectionResolver for PostgresGroupCollectionResolver {
    async fn resolve_owning_group(
        &self,
        app_id: AppId,
        name: &str,
        kind: &str,
    ) -> Result<Option<GroupId>, sqlx::Error> {
        resolve_owning_group(&self.pool, app_id, name, kind).await
    }
}
