//! Users of this organization's instance. All SQL for users lives here.

use domain::{
    AuditAction, AuditActor, Email, FieldChange, NewAuditEvent, NewUser, Role, User, UserId,
};
use serde_json::json;
use sqlx::Row;

use crate::audit;

const ENTITY_TYPE: &str = "user";

const USER_COLUMNS: &str = "id, email, role";

/// Create a user and record a `created` audit entry (email + role only — never the
/// password hash), both on `conn`. Pass a transaction connection.
pub async fn create_user(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    new: &NewUser,
) -> Result<UserId, sqlx::Error> {
    let id = UserId::new();

    sqlx::query("INSERT INTO app_user (id, email, password_hash, role) VALUES ($1, $2, $3, $4)")
        .bind(id.to_uuid())
        .bind(new.email.as_str())
        .bind(&new.password_hash)
        .bind(new.role.as_str())
        .execute(&mut *conn)
        .await?;

    audit::record(
        &mut *conn,
        &NewAuditEvent {
            actor,
            action: AuditAction::Created,
            entity_type: ENTITY_TYPE.to_owned(),
            entity_id: id.to_uuid(),
            changes: vec![
                FieldChange {
                    field: "email".to_owned(),
                    before: None,
                    after: Some(json!(new.email.as_str())),
                },
                FieldChange {
                    field: "role".to_owned(),
                    before: None,
                    after: Some(json!(new.role.as_str())),
                },
            ],
        },
    )
    .await?;

    Ok(id)
}

/// Fetch a user by id.
pub async fn user_by_id<'e, E>(executor: E, id: UserId) -> Result<Option<User>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!("SELECT {USER_COLUMNS} FROM app_user WHERE id = $1");

    let row = sqlx::query(&sql)
        .bind(id.to_uuid())
        .fetch_optional(executor)
        .await?;

    row.map(map_user).transpose()
}

/// Fetch a user and their password hash by (normalized) email, for login.
pub async fn credentials_by_email<'e, E>(
    executor: E,
    email: &str,
) -> Result<Option<(User, String)>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    // Match the `lower(email)` unique index; `email` is already normalized by callers.
    let sql = format!("SELECT {USER_COLUMNS}, password_hash FROM app_user WHERE lower(email) = $1");

    let row = sqlx::query(&sql)
        .bind(email)
        .fetch_optional(executor)
        .await?;

    match row {
        Some(row) => {
            let hash: String = row.try_get("password_hash")?;

            Ok(Some((map_user(row)?, hash)))
        }
        None => Ok(None),
    }
}

/// List all users, ordered by email.
// TODO: add LIMIT/keyset pagination before exposing this via the API.
pub async fn list_users<'e, E>(executor: E) -> Result<Vec<User>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!("SELECT {USER_COLUMNS} FROM app_user ORDER BY email");

    let rows = sqlx::query(&sql).fetch_all(executor).await?;

    rows.into_iter().map(map_user).collect()
}

fn map_user(row: sqlx::postgres::PgRow) -> Result<User, sqlx::Error> {
    let role_str: String = row.try_get("role")?;

    let role = Role::from_db(&role_str)
        .ok_or_else(|| sqlx::Error::Decode(format!("unknown role: {role_str}").into()))?;

    Ok(User {
        id: UserId::from_uuid(row.try_get("id")?),
        email: Email::from_db(row.try_get("email")?),
        role,
    })
}
