//! CRUD over the `group_members` table — explicit per-(user, group) role
//! grants. A `group_admin` here is implicitly app_admin on every app and
//! subgroup beneath the group; that inheritance is resolved by the authz
//! ancestor walk (`app_members_repo::effective_app_role`), not here.
//!
//! Mirrors `app_members_repo` — same three role literals, same shapes.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use picloud_shared::{AdminUserId, AppRole, GroupId, InstanceRole};
use sqlx::PgPool;

#[derive(Debug, thiserror::Error)]
pub enum GroupMembersRepositoryError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),
    #[error("invalid app_role stored in DB: {0}")]
    InvalidRole(String),
}

#[derive(Debug, Clone)]
pub struct GroupMembershipRow {
    pub group_id: GroupId,
    pub user_id: AdminUserId,
    pub role: AppRole,
    pub created_at: DateTime<Utc>,
}

/// `group_members` row joined with `admin_users` for the dashboard's
/// per-group Members tab.
#[derive(Debug, Clone)]
pub struct GroupMembershipDetail {
    pub user_id: AdminUserId,
    pub username: String,
    pub email: Option<String>,
    pub instance_role: InstanceRole,
    pub is_active: bool,
    pub role: AppRole,
    pub created_at: DateTime<Utc>,
}

#[async_trait]
pub trait GroupMembersRepository: Send + Sync {
    /// Atomic insert. `None` if a membership already exists (handler → 409).
    async fn try_insert(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
        role: AppRole,
    ) -> Result<Option<GroupMembershipRow>, GroupMembersRepositoryError>;

    /// Atomic role update. `None` if no row exists (handler → 404).
    async fn update_role(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
        role: AppRole,
    ) -> Result<Option<GroupMembershipRow>, GroupMembersRepositoryError>;

    /// Remove a membership. No-op when the row doesn't exist.
    async fn remove(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
    ) -> Result<(), GroupMembersRepositoryError>;

    /// Per-group member list joined with `admin_users`, ordered by username.
    async fn list_for_group_enriched(
        &self,
        group_id: GroupId,
    ) -> Result<Vec<GroupMembershipDetail>, GroupMembersRepositoryError>;
}

pub struct PostgresGroupMembersRepository {
    pool: PgPool,
}

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

#[async_trait]
impl GroupMembersRepository for PostgresGroupMembersRepository {
    async fn try_insert(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
        role: AppRole,
    ) -> Result<Option<GroupMembershipRow>, GroupMembersRepositoryError> {
        let row = sqlx::query_as::<_, GroupMembershipRecord>(
            "INSERT INTO group_members (group_id, user_id, role) \
             VALUES ($1, $2, $3) \
             ON CONFLICT (group_id, user_id) DO NOTHING \
             RETURNING group_id, user_id, role, created_at",
        )
        .bind(group_id.into_inner())
        .bind(user_id.into_inner())
        .bind(role.as_str())
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn update_role(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
        role: AppRole,
    ) -> Result<Option<GroupMembershipRow>, GroupMembersRepositoryError> {
        let row = sqlx::query_as::<_, GroupMembershipRecord>(
            "UPDATE group_members SET role = $1 \
             WHERE group_id = $2 AND user_id = $3 \
             RETURNING group_id, user_id, role, created_at",
        )
        .bind(role.as_str())
        .bind(group_id.into_inner())
        .bind(user_id.into_inner())
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn remove(
        &self,
        group_id: GroupId,
        user_id: AdminUserId,
    ) -> Result<(), GroupMembersRepositoryError> {
        sqlx::query("DELETE FROM group_members WHERE group_id = $1 AND user_id = $2")
            .bind(group_id.into_inner())
            .bind(user_id.into_inner())
            .execute(&self.pool)
            .await?;
        Ok(())
    }

    async fn list_for_group_enriched(
        &self,
        group_id: GroupId,
    ) -> Result<Vec<GroupMembershipDetail>, GroupMembersRepositoryError> {
        let rows = sqlx::query_as::<_, GroupMembershipDetailRecord>(
            "SELECT au.id, au.username, au.email, au.instance_role, au.is_active, \
                    gm.role, gm.created_at \
             FROM group_members gm \
             JOIN admin_users au ON au.id = gm.user_id \
             WHERE gm.group_id = $1 \
             ORDER BY au.username",
        )
        .bind(group_id.into_inner())
        .fetch_all(&self.pool)
        .await?;
        rows.into_iter().map(TryInto::try_into).collect()
    }
}

#[derive(sqlx::FromRow)]
struct GroupMembershipRecord {
    group_id: uuid::Uuid,
    user_id: uuid::Uuid,
    role: String,
    created_at: DateTime<Utc>,
}

impl TryFrom<GroupMembershipRecord> for GroupMembershipRow {
    type Error = GroupMembersRepositoryError;
    fn try_from(r: GroupMembershipRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            group_id: r.group_id.into(),
            user_id: r.user_id.into(),
            role: AppRole::from_db_str(&r.role)
                .ok_or(GroupMembersRepositoryError::InvalidRole(r.role))?,
            created_at: r.created_at,
        })
    }
}

#[derive(sqlx::FromRow)]
struct GroupMembershipDetailRecord {
    id: uuid::Uuid,
    username: String,
    email: Option<String>,
    instance_role: String,
    is_active: bool,
    role: String,
    created_at: DateTime<Utc>,
}

impl TryFrom<GroupMembershipDetailRecord> for GroupMembershipDetail {
    type Error = GroupMembersRepositoryError;
    fn try_from(r: GroupMembershipDetailRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            user_id: r.id.into(),
            username: r.username,
            email: r.email,
            instance_role: InstanceRole::from_db_str(&r.instance_role)
                .ok_or(GroupMembersRepositoryError::InvalidRole(r.instance_role))?,
            is_active: r.is_active,
            role: AppRole::from_db_str(&r.role)
                .ok_or(GroupMembersRepositoryError::InvalidRole(r.role))?,
            created_at: r.created_at,
        })
    }
}
