//! CRUD over the `admin_users` table.
//!
//! Password hashes go in and come out as opaque strings — this module
//! never inspects or computes them; that's `auth.rs`'s job. The "must
//! keep at least one active admin" guard is implemented as a separate
//! count query the API layer composes around `set_active` / `delete`.

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

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

    #[error("not found: {0}")]
    NotFound(AdminUserId),

    #[error("username already taken: {0}")]
    DuplicateUsername(String),

    #[error("email already taken: {0}")]
    DuplicateEmail(String),

    #[error("invalid instance_role stored in DB: {0}")]
    InvalidInstanceRole(String),
}

/// Row returned to handlers and bootstrap. Never includes the password
/// hash by accident — that lives in `AdminUserCredentials` (separate
/// fetch from `get_credentials_by_username`).
#[derive(Debug, Clone)]
pub struct AdminUserRow {
    pub id: AdminUserId,
    pub username: String,
    pub is_active: bool,
    pub instance_role: InstanceRole,
    pub email: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub last_login_at: Option<DateTime<Utc>>,
}

/// Credentials fetched for the login path only. Splitting the hash off
/// from the public row makes it obvious in handler code which calls
/// touch a secret.
#[derive(Debug, Clone)]
pub struct AdminUserCredentials {
    pub id: AdminUserId,
    pub username: String,
    pub password_hash: String,
    pub is_active: bool,
    pub instance_role: InstanceRole,
}

#[async_trait]
pub trait AdminUserRepository: Send + Sync {
    async fn get(&self, id: AdminUserId) -> Result<Option<AdminUserRow>, AdminUserRepositoryError>;
    async fn get_by_username(
        &self,
        username: &str,
    ) -> Result<Option<AdminUserRow>, AdminUserRepositoryError>;
    async fn get_credentials_by_username(
        &self,
        username: &str,
    ) -> Result<Option<AdminUserCredentials>, AdminUserRepositoryError>;
    async fn list(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError>;
    /// Create a new admin. `instance_role` defaults to `Owner` for the
    /// env-var bootstrap path; admin-creates-admin flows pass an
    /// explicit role. `email` is optional — pass `None` to leave the
    /// column NULL.
    async fn create(
        &self,
        username: &str,
        password_hash: &str,
        instance_role: InstanceRole,
        email: Option<&str>,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    async fn update_username(
        &self,
        id: AdminUserId,
        username: &str,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    async fn update_password_hash(
        &self,
        id: AdminUserId,
        password_hash: &str,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    /// Set or clear the email address. `None` writes NULL to the column.
    async fn update_email(
        &self,
        id: AdminUserId,
        email: Option<&str>,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    /// Update the instance_role. Used by `PATCH /api/v1/admin/admins/{id}`;
    /// callers enforce the last-owner guard (`count_other_active_owners`)
    /// before invoking when role transitions away from `Owner`.
    async fn update_instance_role(
        &self,
        id: AdminUserId,
        instance_role: InstanceRole,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    async fn set_active(
        &self,
        id: AdminUserId,
        is_active: bool,
    ) -> Result<AdminUserRow, AdminUserRepositoryError>;
    async fn delete(&self, id: AdminUserId) -> Result<(), AdminUserRepositoryError>;
    async fn touch_last_login(&self, id: AdminUserId) -> Result<(), AdminUserRepositoryError>;
    /// Count of `is_active = true` rows. Used at bootstrap to decide
    /// whether to seed the first admin.
    async fn count_active(&self) -> Result<i64, AdminUserRepositoryError>;
    /// Count of `is_active = true` rows excluding the given id. Used by
    /// last-admin protection: "would deactivating / deleting this user
    /// leave zero active admins?"
    async fn count_active_excluding(
        &self,
        id: AdminUserId,
    ) -> Result<i64, AdminUserRepositoryError>;
    /// All active owners — used for the multi-owner startup warning.
    async fn list_active_owners(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError>;
    /// Count of active owners excluding the given id. Used by the
    /// last-owner guard when demoting / deactivating / deleting an
    /// owner: "would this leave zero owners?"
    async fn count_other_active_owners(
        &self,
        id: AdminUserId,
    ) -> Result<i64, AdminUserRepositoryError>;
}

pub struct PostgresAdminUserRepository {
    pool: PgPool,
}

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

#[async_trait]
impl AdminUserRepository for PostgresAdminUserRepository {
    async fn get(&self, id: AdminUserId) -> Result<Option<AdminUserRow>, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminUserRecord>(
            "SELECT id, username, is_active, instance_role, email, \
                    created_at, updated_at, last_login_at \
             FROM admin_users WHERE id = $1",
        )
        .bind(id.into_inner())
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn get_by_username(
        &self,
        username: &str,
    ) -> Result<Option<AdminUserRow>, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminUserRecord>(
            "SELECT id, username, is_active, instance_role, email, \
                    created_at, updated_at, last_login_at \
             FROM admin_users WHERE username = $1",
        )
        .bind(username)
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn get_credentials_by_username(
        &self,
        username: &str,
    ) -> Result<Option<AdminUserCredentials>, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminCredsRecord>(
            "SELECT id, username, password_hash, is_active, instance_role \
             FROM admin_users WHERE username = $1",
        )
        .bind(username)
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn list(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError> {
        let rows = sqlx::query_as::<_, AdminUserRecord>(
            "SELECT id, username, is_active, instance_role, email, \
                    created_at, updated_at, last_login_at \
             FROM admin_users ORDER BY username",
        )
        .fetch_all(&self.pool)
        .await?;
        rows.into_iter().map(TryInto::try_into).collect()
    }

    async fn create(
        &self,
        username: &str,
        password_hash: &str,
        instance_role: InstanceRole,
        email: Option<&str>,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let res = sqlx::query_as::<_, AdminUserRecord>(
            "INSERT INTO admin_users (username, password_hash, instance_role, email) \
             VALUES ($1, $2, $3, $4) \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(username)
        .bind(password_hash)
        .bind(instance_role.as_str())
        .bind(email)
        .fetch_one(&self.pool)
        .await;

        match res {
            Ok(row) => row.try_into(),
            Err(sqlx::Error::Database(e)) if e.is_unique_violation() => {
                // username and email both have unique constraints; the
                // create path can collide on either, so peek at the
                // constraint name to surface the right error.
                if e.constraint() == Some("admin_users_email_key") {
                    Err(AdminUserRepositoryError::DuplicateEmail(
                        email.unwrap_or("").to_string(),
                    ))
                } else {
                    Err(AdminUserRepositoryError::DuplicateUsername(
                        username.to_string(),
                    ))
                }
            }
            Err(e) => Err(e.into()),
        }
    }

    async fn update_username(
        &self,
        id: AdminUserId,
        username: &str,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let res = sqlx::query_as::<_, AdminUserRecord>(
            "UPDATE admin_users SET username = $2, updated_at = NOW() \
             WHERE id = $1 \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(id.into_inner())
        .bind(username)
        .fetch_optional(&self.pool)
        .await;

        match res {
            Ok(Some(row)) => row.try_into(),
            Ok(None) => Err(AdminUserRepositoryError::NotFound(id)),
            Err(sqlx::Error::Database(e)) if e.is_unique_violation() => Err(
                AdminUserRepositoryError::DuplicateUsername(username.to_string()),
            ),
            Err(e) => Err(e.into()),
        }
    }

    async fn update_password_hash(
        &self,
        id: AdminUserId,
        password_hash: &str,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminUserRecord>(
            "UPDATE admin_users SET password_hash = $2, updated_at = NOW() \
             WHERE id = $1 \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(id.into_inner())
        .bind(password_hash)
        .fetch_optional(&self.pool)
        .await?;
        row.ok_or(AdminUserRepositoryError::NotFound(id))
            .and_then(TryInto::try_into)
    }

    async fn update_email(
        &self,
        id: AdminUserId,
        email: Option<&str>,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let res = sqlx::query_as::<_, AdminUserRecord>(
            "UPDATE admin_users SET email = $2, updated_at = NOW() \
             WHERE id = $1 \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(id.into_inner())
        .bind(email)
        .fetch_optional(&self.pool)
        .await;

        match res {
            Ok(Some(row)) => row.try_into(),
            Ok(None) => Err(AdminUserRepositoryError::NotFound(id)),
            Err(sqlx::Error::Database(e)) if e.is_unique_violation() => Err(
                AdminUserRepositoryError::DuplicateEmail(email.unwrap_or("").to_string()),
            ),
            Err(e) => Err(e.into()),
        }
    }

    async fn update_instance_role(
        &self,
        id: AdminUserId,
        instance_role: InstanceRole,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminUserRecord>(
            "UPDATE admin_users SET instance_role = $2, updated_at = NOW() \
             WHERE id = $1 \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(id.into_inner())
        .bind(instance_role.as_str())
        .fetch_optional(&self.pool)
        .await?;
        row.ok_or(AdminUserRepositoryError::NotFound(id))
            .and_then(TryInto::try_into)
    }

    async fn set_active(
        &self,
        id: AdminUserId,
        is_active: bool,
    ) -> Result<AdminUserRow, AdminUserRepositoryError> {
        let row = sqlx::query_as::<_, AdminUserRecord>(
            "UPDATE admin_users SET is_active = $2, updated_at = NOW() \
             WHERE id = $1 \
             RETURNING id, username, is_active, instance_role, email, \
                       created_at, updated_at, last_login_at",
        )
        .bind(id.into_inner())
        .bind(is_active)
        .fetch_optional(&self.pool)
        .await?;
        row.ok_or(AdminUserRepositoryError::NotFound(id))
            .and_then(TryInto::try_into)
    }

    async fn delete(&self, id: AdminUserId) -> Result<(), AdminUserRepositoryError> {
        let res = sqlx::query("DELETE FROM admin_users WHERE id = $1")
            .bind(id.into_inner())
            .execute(&self.pool)
            .await?;
        if res.rows_affected() == 0 {
            return Err(AdminUserRepositoryError::NotFound(id));
        }
        Ok(())
    }

    async fn touch_last_login(&self, id: AdminUserId) -> Result<(), AdminUserRepositoryError> {
        sqlx::query("UPDATE admin_users SET last_login_at = NOW() WHERE id = $1")
            .bind(id.into_inner())
            .execute(&self.pool)
            .await?;
        Ok(())
    }

    async fn count_active(&self) -> Result<i64, AdminUserRepositoryError> {
        let (count,): (i64,) =
            sqlx::query_as("SELECT COUNT(*)::BIGINT FROM admin_users WHERE is_active")
                .fetch_one(&self.pool)
                .await?;
        Ok(count)
    }

    async fn count_active_excluding(
        &self,
        id: AdminUserId,
    ) -> Result<i64, AdminUserRepositoryError> {
        let (count,): (i64,) =
            sqlx::query_as("SELECT COUNT(*)::BIGINT FROM admin_users WHERE is_active AND id <> $1")
                .bind(id.into_inner())
                .fetch_one(&self.pool)
                .await?;
        Ok(count)
    }

    async fn list_active_owners(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError> {
        let rows = sqlx::query_as::<_, AdminUserRecord>(
            "SELECT id, username, is_active, instance_role, email, \
                    created_at, updated_at, last_login_at \
             FROM admin_users \
             WHERE is_active AND instance_role = 'owner' \
             ORDER BY username",
        )
        .fetch_all(&self.pool)
        .await?;
        rows.into_iter().map(TryInto::try_into).collect()
    }

    async fn count_other_active_owners(
        &self,
        id: AdminUserId,
    ) -> Result<i64, AdminUserRepositoryError> {
        let (count,): (i64,) = sqlx::query_as(
            "SELECT COUNT(*)::BIGINT FROM admin_users \
             WHERE is_active AND instance_role = 'owner' AND id <> $1",
        )
        .bind(id.into_inner())
        .fetch_one(&self.pool)
        .await?;
        Ok(count)
    }
}

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

impl TryFrom<AdminUserRecord> for AdminUserRow {
    type Error = AdminUserRepositoryError;
    fn try_from(r: AdminUserRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            id: r.id.into(),
            username: r.username,
            is_active: r.is_active,
            instance_role: InstanceRole::from_db_str(&r.instance_role).ok_or(
                AdminUserRepositoryError::InvalidInstanceRole(r.instance_role),
            )?,
            email: r.email,
            created_at: r.created_at,
            updated_at: r.updated_at,
            last_login_at: r.last_login_at,
        })
    }
}

#[derive(sqlx::FromRow)]
struct AdminCredsRecord {
    id: uuid::Uuid,
    username: String,
    password_hash: String,
    is_active: bool,
    instance_role: String,
}

impl TryFrom<AdminCredsRecord> for AdminUserCredentials {
    type Error = AdminUserRepositoryError;
    fn try_from(r: AdminCredsRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            id: r.id.into(),
            username: r.username,
            password_hash: r.password_hash,
            is_active: r.is_active,
            instance_role: InstanceRole::from_db_str(&r.instance_role).ok_or(
                AdminUserRepositoryError::InvalidInstanceRole(r.instance_role),
            )?,
        })
    }
}
