//! Structured audit log — persistent, machine-readable event journal.
//!
//! Events are serialized as JSON lines and appended to a file or SQL table.
//! Each event carries a correlation `trace_id` for cross-referencing with
//! RPC request traces.

use std::fs::OpenOptions;
use std::io::Write as IoWrite;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use serde::Serialize;

// ── Audit event types ─────────────────────────────────────────────────────

/// Action categories for the audit log.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditAction {
    AuthRegister,
    AuthLoginSuccess,
    AuthLoginFailure,
    Enqueue,
    BatchEnqueue,
    Fetch,
    FetchWait,
    KeyUpload,
    HybridKeyUpload,
    BanUser,
    UnbanUser,
    ReportMessage,
    AccountDelete,
    DeviceRegister,
    DeviceRevoke,
    BlobUpload,
    RecoveryStore,
    RecoveryFetch,
    RecoveryDelete,
}

/// Outcome of an audited action.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditOutcome {
    Success,
    Denied,
    Error,
    RateLimited,
}

/// A single audit event record.
#[derive(Debug, Clone, Serialize)]
pub struct AuditEvent {
    /// ISO-8601 timestamp.
    pub timestamp: String,
    /// RPC correlation ID.
    pub trace_id: String,
    /// Hex-encoded actor identity key (truncated for privacy when redact=true).
    pub actor: String,
    /// The action performed.
    pub action: AuditAction,
    /// Target identifier (recipient key, username, etc.).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    /// Outcome of the action.
    pub outcome: AuditOutcome,
    /// Free-form details.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<String>,
}

// ── Audit logger trait ────────────────────────────────────────────────────

/// Trait for audit log backends.
pub trait AuditLogger: Send + Sync {
    fn log(&self, event: AuditEvent);
}

// ── File-backed implementation ───────────────────────────────────────────

/// Appends JSON-line events to a file.
pub struct FileAuditLogger {
    path: PathBuf,
    file: Mutex<std::fs::File>,
}

impl FileAuditLogger {
    /// Open (or create) the audit log file at `path`.
    pub fn open(path: &Path) -> Result<Self, std::io::Error> {
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;
        Ok(Self {
            path: path.to_path_buf(),
            file: Mutex::new(file),
        })
    }

    /// Return the path to the audit log file.
    pub fn path(&self) -> &Path {
        &self.path
    }
}

impl AuditLogger for FileAuditLogger {
    fn log(&self, event: AuditEvent) {
        let Ok(mut line) = serde_json::to_string(&event) else {
            tracing::warn!("audit: failed to serialize event");
            return;
        };
        line.push('\n');

        let Ok(mut f) = self.file.lock() else {
            tracing::warn!("audit: log file lock poisoned");
            return;
        };
        if let Err(e) = f.write_all(line.as_bytes()) {
            tracing::warn!(error = %e, "audit: failed to write event");
        }
    }
}

// ── No-op implementation ─────────────────────────────────────────────────

/// Does nothing. Used when audit logging is disabled.
pub struct NoopAuditLogger;

impl AuditLogger for NoopAuditLogger {
    fn log(&self, _event: AuditEvent) {}
}

// ── Helpers ──────────────────────────────────────────────────────────────

/// Format identity key bytes as hex, optionally truncated for privacy.
pub fn format_actor(identity_key: &[u8], redact: bool) -> String {
    let full = hex::encode(identity_key);
    if redact && full.len() > 12 {
        format!("{}...", &full[..12])
    } else {
        full
    }
}

/// Current ISO-8601 UTC timestamp.
pub fn now_iso8601() -> String {
    // Use SystemTime to avoid pulling in chrono.
    let d = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = d.as_secs();
    // Simple UTC formatting: enough for audit logs.
    format!("{secs}")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Read;

    #[test]
    fn file_audit_logger_writes_json_lines() {
        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("audit.jsonl");
        let logger = FileAuditLogger::open(&path).expect("open");

        logger.log(AuditEvent {
            timestamp: "1709500000".to_string(),
            trace_id: "test-trace-001".to_string(),
            actor: "abcdef123456".to_string(),
            action: AuditAction::Enqueue,
            target: Some("recipient-hex".to_string()),
            outcome: AuditOutcome::Success,
            details: None,
        });

        logger.log(AuditEvent {
            timestamp: "1709500001".to_string(),
            trace_id: "test-trace-002".to_string(),
            actor: "abcdef123456".to_string(),
            action: AuditAction::AuthLoginFailure,
            target: None,
            outcome: AuditOutcome::Denied,
            details: Some("bad password".to_string()),
        });

        drop(logger);

        let mut content = String::new();
        std::fs::File::open(&path)
            .expect("open for read")
            .read_to_string(&mut content)
            .expect("read");

        let lines: Vec<&str> = content.trim().split('\n').collect();
        assert_eq!(lines.len(), 2);

        // Verify JSON parses.
        let v: serde_json::Value = serde_json::from_str(lines[0]).expect("parse line 0");
        assert_eq!(v["action"], "enqueue");
        assert_eq!(v["outcome"], "success");
        assert_eq!(v["trace_id"], "test-trace-001");

        let v: serde_json::Value = serde_json::from_str(lines[1]).expect("parse line 1");
        assert_eq!(v["action"], "auth_login_failure");
        assert_eq!(v["details"], "bad password");
    }

    #[test]
    fn format_actor_truncates_when_redacted() {
        let key = vec![0xAA; 32];
        let full = format_actor(&key, false);
        assert_eq!(full.len(), 64);
        let redacted = format_actor(&key, true);
        assert!(redacted.ends_with("..."));
        assert_eq!(redacted.len(), 15); // 12 hex chars + "..."
    }

    #[test]
    fn noop_logger_does_not_panic() {
        let logger = NoopAuditLogger;
        logger.log(AuditEvent {
            timestamp: "0".to_string(),
            trace_id: "noop".to_string(),
            actor: "none".to_string(),
            action: AuditAction::Fetch,
            target: None,
            outcome: AuditOutcome::Success,
            details: None,
        });
    }
}
