use serde::{Deserialize, Serialize};
use serde_json::Value;
use time::OffsetDateTime;
use uuid::Uuid;

/// What kind of change an audit entry records.
///
/// NOTE: kept in sync by hand with the
/// `CHECK (action IN ('created', 'updated', 'deleted'))` constraint in
/// `crates/db/migrations/0001_audit_log.sql` — add a variant in both places.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuditAction {
    Created,
    Updated,
    Deleted,
}

impl AuditAction {
    /// The database/text representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            AuditAction::Created => "created",
            AuditAction::Updated => "updated",
            AuditAction::Deleted => "deleted",
        }
    }

    /// Parse from the database/text representation.
    pub fn from_db(s: &str) -> Option<Self> {
        match s {
            "created" => Some(AuditAction::Created),
            "updated" => Some(AuditAction::Updated),
            "deleted" => Some(AuditAction::Deleted),
            _ => None,
        }
    }
}

/// Who performed the change.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "id")]
pub enum AuditActor {
    /// A specific user, referenced by id (a `UserId` newtype arrives with auth).
    User(Uuid),
    /// The system itself (migrations, automated processes).
    System,
}

/// One field's before/after values within a change.
///
/// Note: after a JSON round-trip, `Some(Value::Null)` is indistinguishable from
/// `None`. Use `None` to mean "no value"; do not encode an absent value as
/// `Some(Value::Null)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldChange {
    /// Field name (catalogue field key or column name).
    pub field: String,
    /// Value before the change (None when newly set).
    pub before: Option<Value>,
    /// Value after the change (None when cleared).
    pub after: Option<Value>,
}

/// An audit event to be recorded.
#[derive(Debug, Clone, PartialEq)]
pub struct NewAuditEvent {
    pub actor: AuditActor,
    pub action: AuditAction,
    pub entity_type: String,
    pub entity_id: Uuid,
    pub changes: Vec<FieldChange>,
}

/// A recorded audit entry, read back from the log.
#[derive(Debug, Clone, PartialEq)]
pub struct AuditEntry {
    /// Monotonic sequence number (insertion order).
    pub seq: i64,
    /// When it was recorded.
    pub at: OffsetDateTime,
    pub actor: AuditActor,
    pub action: AuditAction,
    pub entity_type: String,
    pub entity_id: Uuid,
    pub changes: Vec<FieldChange>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn action_round_trips_via_db_string() {
        for a in [
            AuditAction::Created,
            AuditAction::Updated,
            AuditAction::Deleted,
        ] {
            assert_eq!(AuditAction::from_db(a.as_str()), Some(a));
        }
        assert_eq!(AuditAction::from_db("bogus"), None);
    }

    #[test]
    fn field_change_serde_round_trip() {
        let fc = FieldChange {
            field: "name".into(),
            before: Some(json!("Vase")),
            after: Some(json!("Roman Vase")),
        };
        let v = serde_json::to_value(&fc).unwrap();
        assert_eq!(v["field"], "name");
        assert_eq!(v["before"], "Vase");
        assert_eq!(v["after"], "Roman Vase");
        let back: FieldChange = serde_json::from_value(v).unwrap();
        assert_eq!(back, fc);
    }

    #[test]
    fn actor_serde_round_trips() {
        for actor in [AuditActor::User(Uuid::nil()), AuditActor::System] {
            let v = serde_json::to_value(actor).unwrap();
            let back: AuditActor = serde_json::from_value(v).unwrap();
            assert_eq!(back, actor);
        }
        assert_eq!(
            serde_json::to_value(AuditActor::User(Uuid::nil())).unwrap()["kind"],
            "user"
        );
        assert_eq!(
            serde_json::to_value(AuditActor::System).unwrap()["kind"],
            "system"
        );
    }

    #[test]
    fn action_serde_matches_as_str() {
        for a in [
            AuditAction::Created,
            AuditAction::Updated,
            AuditAction::Deleted,
        ] {
            assert_eq!(
                serde_json::to_value(a).unwrap(),
                serde_json::Value::String(a.as_str().to_owned())
            );
        }
    }
}
