//! Admin-action audit log writes + the admin-facing read query.
//!
//! Insert is always called from inside the same transaction as the
//! action it audits — the executor parameter is `PgExecutor` so the
//! caller passes `&mut *tx` directly. [`list`] backs the audit-log viewer.

use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::{FromRow, PgExecutor, PgPool};
use uuid::Uuid;

use crate::error::AppResult;

/// One audit row joined to the actor's current username (NULL when the
/// actor account was deleted — `actor_user_id` is `ON DELETE SET NULL`).
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct AuditEntryRow {
    pub id: Uuid,
    pub actor_user_id: Option<Uuid>,
    pub actor_username: Option<String>,
    pub action: String,
    pub target_kind: String,
    pub target_id: Option<Uuid>,
    pub payload: serde_json::Value,
    pub at: DateTime<Utc>,
}

/// Optional filters for [`list`]. `None`/absent widens the scope; an
/// empty-string action/target_kind is treated as absent by the caller.
#[derive(Debug, Default, Clone)]
pub struct AuditFilter<'a> {
    pub action: Option<&'a str>,
    pub target_kind: Option<&'a str>,
    pub actor_user_id: Option<Uuid>,
    pub since: Option<DateTime<Utc>>,
}

/// Paginated, newest-first audit log. Returns the page slice plus the
/// filtered total. Ordering by `at DESC` uses `admin_audit_at_idx`.
pub async fn list(
    pool: &PgPool,
    filter: AuditFilter<'_>,
    limit: i64,
    offset: i64,
) -> AppResult<(Vec<AuditEntryRow>, i64)> {
    let items = sqlx::query_as::<_, AuditEntryRow>(
        r#"
        SELECT
            a.id,
            a.actor_user_id,
            u.username        AS actor_username,
            a.action,
            a.target_kind,
            a.target_id,
            a.payload,
            a.at
          FROM admin_audit a
          LEFT JOIN users u ON u.id = a.actor_user_id
         WHERE ($1::text IS NULL OR a.action = $1)
           AND ($2::text IS NULL OR a.target_kind = $2)
           AND ($3::uuid IS NULL OR a.actor_user_id = $3)
           AND ($4::timestamptz IS NULL OR a.at >= $4)
         -- `id` tiebreaker for stable pagination: a single admin action
         -- can write multiple audit rows in the same statement_timestamp
         -- (e.g. bulk requeue) and ties on the timestamp would otherwise
         -- skip or repeat rows across pages.
         ORDER BY a.at DESC, a.id DESC
         LIMIT $5 OFFSET $6
        "#,
    )
    .bind(filter.action)
    .bind(filter.target_kind)
    .bind(filter.actor_user_id)
    .bind(filter.since)
    .bind(limit)
    .bind(offset)
    .fetch_all(pool)
    .await?;

    let (total,): (i64,) = sqlx::query_as(
        r#"
        SELECT COUNT(*)
          FROM admin_audit a
         WHERE ($1::text IS NULL OR a.action = $1)
           AND ($2::text IS NULL OR a.target_kind = $2)
           AND ($3::uuid IS NULL OR a.actor_user_id = $3)
           AND ($4::timestamptz IS NULL OR a.at >= $4)
        "#,
    )
    .bind(filter.action)
    .bind(filter.target_kind)
    .bind(filter.actor_user_id)
    .bind(filter.since)
    .fetch_one(pool)
    .await?;

    Ok((items, total))
}

pub async fn insert<'e, E: PgExecutor<'e>>(
    executor: E,
    actor_user_id: Uuid,
    action: &str,
    target_kind: &str,
    target_id: Option<Uuid>,
    payload: serde_json::Value,
) -> AppResult<()> {
    sqlx::query(
        "INSERT INTO admin_audit (actor_user_id, action, target_kind, target_id, payload) \
         VALUES ($1, $2, $3, $4, $5)",
    )
    .bind(actor_user_id)
    .bind(action)
    .bind(target_kind)
    .bind(target_id)
    .bind(payload)
    .execute(executor)
    .await?;
    Ok(())
}
