//! Append-only audit log access.

use domain::{AuditActor, AuditEntry, FieldChange, NewAuditEvent};
use sqlx::Row;
use uuid::Uuid;

/// Append an audit event. Accepts any executor, so callers can record the event
/// inside the same transaction as the change it describes.
pub async fn record<'e, E>(executor: E, event: &NewAuditEvent) -> Result<(), sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let (actor_kind, actor_id) = match event.actor {
        AuditActor::User(id) => ("user", Some(id)),
        AuditActor::System => ("system", None),
    };

    sqlx::query(
        "INSERT INTO audit_log \
         (actor_kind, actor_id, action, entity_type, entity_id, changes) \
         VALUES ($1, $2, $3, $4, $5, $6)",
    )
    .bind(actor_kind)
    .bind(actor_id)
    .bind(event.action.as_str())
    .bind(&event.entity_type)
    .bind(event.entity_id)
    .bind(sqlx::types::Json(&event.changes))
    .execute(executor)
    .await?;

    Ok(())
}

/// Read the full history for one entity, oldest first.
pub async fn history_for<'e, E>(
    executor: E,
    entity_type: &str,
    entity_id: Uuid,
) -> Result<Vec<AuditEntry>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    // TODO: add LIMIT/keyset pagination before exposing history_for via the API.
    let rows = sqlx::query(
        "SELECT seq, at, actor_kind, actor_id, action, entity_type, entity_id, changes \
         FROM audit_log \
         WHERE entity_type = $1 AND entity_id = $2 \
         ORDER BY seq",
    )
    .bind(entity_type)
    .bind(entity_id)
    .fetch_all(executor)
    .await?;

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

fn map_row(row: sqlx::postgres::PgRow) -> Result<AuditEntry, sqlx::Error> {
    let seq: i64 = row.try_get("seq")?;
    let at: time::OffsetDateTime = row.try_get("at")?;
    let actor_kind: String = row.try_get("actor_kind")?;
    let actor_id: Option<Uuid> = row.try_get("actor_id")?;
    let action_str: String = row.try_get("action")?;
    let entity_type: String = row.try_get("entity_type")?;
    let entity_id: Uuid = row.try_get("entity_id")?;
    let changes: sqlx::types::Json<Vec<FieldChange>> = row.try_get("changes")?;

    let actor = match actor_kind.as_str() {
        "user" => AuditActor::User(
            actor_id.ok_or_else(|| sqlx::Error::Decode("user actor missing actor_id".into()))?,
        ),
        "system" => AuditActor::System,
        other => {
            return Err(sqlx::Error::Decode(
                format!("unknown actor_kind: {other}").into(),
            ));
        }
    };

    let action = domain::AuditAction::from_db(&action_str)
        .ok_or_else(|| sqlx::Error::Decode(format!("unknown action: {action_str}").into()))?;

    Ok(AuditEntry {
        seq,
        at,
        actor,
        action,
        entity_type,
        entity_id,
        changes: changes.0,
    })
}
