//! Audit Log
//!
//! Main audit logging system that captures database operations via SQLite hooks.

const std = @import("std");
const Allocator = std.mem.Allocator;
const c = @import("../c.zig").c;
const Database = @import("../database.zig").Database;
const UpdateOperation = @import("../types.zig").UpdateOperation;

const Entry = @import("entry.zig").Entry;
const EntryBuilder = @import("entry.zig").EntryBuilder;
const Operation = @import("entry.zig").Operation;
const AuditContext = @import("context.zig").AuditContext;
const ContextEntryContext = @import("context.zig").EntryContext;
const Writer = @import("writer.zig").Writer;
const Index = @import("index.zig").Index;
const RotationConfig = @import("index.zig").RotationConfig;

/// Capture configuration
pub const CaptureConfig = struct {
    /// Capture SQL statements
    sql: bool = true,
    /// Capture values before operation
    before_values: bool = true,
    /// Capture values after operation
    after_values: bool = true,
    /// Tables to include (null = all tables)
    include_tables: ?[]const []const u8 = null,
    /// Tables to exclude
    exclude_tables: ?[]const []const u8 = null,
};

/// Audit log configuration
pub const Config = struct {
    /// Directory for log files
    log_dir: []const u8,
    /// Rotation configuration
    rotation: RotationConfig = .{},
    /// Capture configuration
    capture: CaptureConfig = .{},
    /// Initial application name
    app_name: ?[]const u8 = null,
    /// Initial user
    user: ?[]const u8 = null,
};

/// Pending operation (buffered until commit)
const PendingOp = struct {
    op: Operation,
    table: []const u8,
    rowid: i64,
    sql: ?[]const u8,
    before: ?[]const u8,
    after: ?[]const u8,
    timestamp_us: i64,

    fn deinit(self: *PendingOp, allocator: Allocator) void {
        allocator.free(self.table);
        if (self.sql) |s| allocator.free(s);
        if (self.before) |b| allocator.free(b);
        if (self.after) |a| allocator.free(a);
        self.* = undefined;
    }
};

/// Transaction buffer
const TxBuffer = struct {
    allocator: Allocator,
    tx_id: u64,
    ops: std.ArrayListUnmanaged(PendingOp),

    fn init(allocator: Allocator, tx_id: u64) TxBuffer {
        return .{
            .allocator = allocator,
            .tx_id = tx_id,
            .ops = .empty,
        };
    }

    fn deinit(self: *TxBuffer) void {
        for (self.ops.items) |*op| {
            op.deinit(self.allocator);
        }
        self.ops.deinit(self.allocator);
        self.* = undefined;
    }

    fn addOp(self: *TxBuffer, op: PendingOp) !void {
        try self.ops.append(self.allocator, op);
    }

    fn clear(self: *TxBuffer) void {
        for (self.ops.items) |*op| {
            op.deinit(self.allocator);
        }
        self.ops.clearRetainingCapacity();
    }
};

/// Audit Log system
pub const AuditLog = struct {
    allocator: Allocator,
    /// Database being audited
    db: *Database,
    /// Log writer
    writer: Writer,
    /// Context (user, app, etc.)
    context: AuditContext,
    /// Capture configuration
    capture: CaptureConfig,
    /// Current transaction buffer
    tx_buffer: ?TxBuffer,
    /// Transaction counter
    tx_counter: u64,
    /// Whether audit is enabled
    enabled: bool,
    /// Pre-update values cache (for capturing before values)
    pre_update_cache: ?PreUpdateCache,

    const Self = @This();

    /// Pre-update cache for capturing values before update/delete
    const PreUpdateCache = struct {
        table: []const u8,
        rowid: i64,
        values_json: []const u8,

        fn deinit(self: *PreUpdateCache, allocator: Allocator) void {
            allocator.free(self.table);
            allocator.free(self.values_json);
            self.* = undefined;
        }
    };

    /// Initialize audit log
    /// IMPORTANT: After init, you must call `start()` to activate the hooks.
    /// This is required because SQLite hooks store a pointer to this struct,
    /// and the struct must be at its final memory location before hooks are installed.
    pub fn init(allocator: Allocator, db: *Database, config: Config) !Self {
        // Extract db name from path (for index)
        const db_path = db.filename("main") orelse "unknown.db";
        const db_name = std.fs.path.basename(db_path);

        var writer = try Writer.init(allocator, config.log_dir, db_name);
        errdefer writer.deinit();

        var context = AuditContext.init(allocator);
        errdefer context.deinit();

        if (config.app_name) |app| {
            try context.setApp(app);
        }
        if (config.user) |user| {
            try context.setUser(user);
        }

        return Self{
            .allocator = allocator,
            .db = db,
            .writer = writer,
            .context = context,
            .capture = config.capture,
            .tx_buffer = null,
            .tx_counter = 0,
            .enabled = false, // Disabled until start() is called
            .pre_update_cache = null,
        };
    }

    /// Start audit logging by installing SQLite hooks.
    /// Must be called after the AuditLog struct is at its final memory location.
    pub fn start(self: *Self) void {
        self.installHooks();
        self.enabled = true;
    }

    pub fn deinit(self: *Self) void {
        // Remove hooks
        self.removeHooks();

        // Clean up pre-update cache
        if (self.pre_update_cache) |*cache| {
            cache.deinit(self.allocator);
        }

        // Clean up transaction buffer
        if (self.tx_buffer) |*buf| {
            buf.deinit();
        }

        self.writer.deinit();
        self.context.deinit();
        self.* = undefined;
    }

    /// Set current user
    pub fn setUser(self: *Self, user: ?[]const u8) !void {
        try self.context.setUser(user);
    }

    /// Set application name
    pub fn setApp(self: *Self, app: ?[]const u8) !void {
        try self.context.setApp(app);
    }

    /// Enable/disable audit logging
    pub fn setEnabled(self: *Self, enabled: bool) void {
        self.enabled = enabled;
    }

    /// Force log rotation
    pub fn rotate(self: *Self) !void {
        try self.writer.rotate();
    }

    /// Flush pending writes
    pub fn flush(self: *Self) !void {
        try self.writer.flush();
    }

    /// Get statistics
    pub fn stats(self: *const Self) Stats {
        const writer_stats = self.writer.getStats();
        return Stats{
            .total_entries = writer_stats.total_entries,
            .total_bytes = writer_stats.total_bytes,
            .file_count = writer_stats.file_count,
        };
    }

    // ========================================================================
    // Hook handlers
    // ========================================================================

    fn installHooks(self: *Self) void {
        const self_ptr = @intFromPtr(self);

        // Pre-update hook (captures before values)
        if (self.capture.before_values) {
            _ = c.sqlite3_preupdate_hook(
                self.db.handle,
                preUpdateHookCallback,
                @ptrFromInt(self_ptr),
            );
        }

        // Update hook (captures operation)
        _ = c.sqlite3_update_hook(
            self.db.handle,
            updateHookCallback,
            @ptrFromInt(self_ptr),
        );

        // Commit hook
        _ = c.sqlite3_commit_hook(
            self.db.handle,
            commitHookCallback,
            @ptrFromInt(self_ptr),
        );

        // Rollback hook
        _ = c.sqlite3_rollback_hook(
            self.db.handle,
            rollbackHookCallback,
            @ptrFromInt(self_ptr),
        );
    }

    fn removeHooks(self: *Self) void {
        _ = c.sqlite3_preupdate_hook(self.db.handle, null, null);
        _ = c.sqlite3_update_hook(self.db.handle, null, null);
        _ = c.sqlite3_commit_hook(self.db.handle, null, null);
        _ = c.sqlite3_rollback_hook(self.db.handle, null, null);
    }

    // Pre-update hook: capture before values
    fn preUpdateHookCallback(
        user_data: ?*anyopaque,
        db_handle: ?*c.sqlite3,
        op: c_int,
        _: [*c]const u8, // db_name
        table_name: [*c]const u8,
        old_rowid: c.sqlite3_int64,
        _: c.sqlite3_int64, // new_rowid
    ) callconv(.c) void {
        const self: *Self = @ptrCast(@alignCast(user_data orelse return));
        if (!self.enabled) return;

        const table = std.mem.span(table_name);
        if (!self.shouldCaptureTable(table)) return;

        // Only capture before values for UPDATE and DELETE
        const operation: c_int = op;
        if (operation != c.SQLITE_UPDATE and operation != c.SQLITE_DELETE) return;

        // Get column count
        const col_count = c.sqlite3_preupdate_count(db_handle);
        if (col_count <= 0) return;

        // Build JSON for old values
        var json_list: std.ArrayListUnmanaged(u8) = .empty;
        defer json_list.deinit(self.allocator);

        const writer = json_list.writer(self.allocator);
        writer.writeByte('{') catch return;

        var first = true;
        var col: c_int = 0;
        while (col < col_count) : (col += 1) {
            var value: ?*c.sqlite3_value = null;
            if (c.sqlite3_preupdate_old(db_handle, col, &value) != c.SQLITE_OK) continue;
            if (value == null) continue;

            if (!first) writer.writeAll(",") catch return;
            first = false;

            // Get column name (we'd need table info for this, use index for now)
            writer.print("\"col{d}\":", .{col}) catch return;
            writeValueAsJson(writer, value.?) catch return;
        }

        writer.writeByte('}') catch return;

        // Cache the pre-update values
        if (self.pre_update_cache) |*cache| {
            cache.deinit(self.allocator);
        }

        self.pre_update_cache = PreUpdateCache{
            .table = self.allocator.dupe(u8, table) catch return,
            .rowid = old_rowid,
            .values_json = json_list.toOwnedSlice(self.allocator) catch return,
        };
    }

    // Update hook: capture operation
    fn updateHookCallback(
        user_data: ?*anyopaque,
        op: c_int,
        _: [*c]const u8, // db_name
        table_name: [*c]const u8,
        rowid: c.sqlite3_int64,
    ) callconv(.c) void {
        const self: *Self = @ptrCast(@alignCast(user_data orelse return));
        if (!self.enabled) return;

        const table = std.mem.span(table_name);
        if (!self.shouldCaptureTable(table)) return;

        const operation: Operation = switch (op) {
            c.SQLITE_INSERT => .INSERT,
            c.SQLITE_UPDATE => .UPDATE,
            c.SQLITE_DELETE => .DELETE,
            else => return,
        };

        // Get before values from cache
        var before_json: ?[]const u8 = null;
        if (self.pre_update_cache) |*cache| {
            if (std.mem.eql(u8, cache.table, table) and cache.rowid == rowid) {
                before_json = self.allocator.dupe(u8, cache.values_json) catch null;
            }
            cache.deinit(self.allocator);
            self.pre_update_cache = null;
        }

        // Create pending operation
        const pending = PendingOp{
            .op = operation,
            .table = self.allocator.dupe(u8, table) catch {
                if (before_json) |b| self.allocator.free(b);
                return;
            },
            .rowid = rowid,
            .sql = null, // SQL capture requires statement tracking
            .before = before_json,
            .after = null, // Would need post-update query
            .timestamp_us = std.time.microTimestamp(),
        };

        // Ensure we have a transaction buffer
        if (self.tx_buffer == null) {
            self.tx_counter += 1;
            self.tx_buffer = TxBuffer.init(self.allocator, self.tx_counter);
        }

        self.tx_buffer.?.addOp(pending) catch return;
    }

    // Commit hook: flush transaction buffer
    fn commitHookCallback(user_data: ?*anyopaque) callconv(.c) c_int {
        const self: *Self = @ptrCast(@alignCast(user_data orelse return 0));
        if (!self.enabled) return 0;

        if (self.tx_buffer) |*buf| {
            // Write all pending operations
            for (buf.ops.items) |*op| {
                self.writeEntry(buf.tx_id, op) catch continue;
            }
            buf.deinit();
            self.tx_buffer = null;
        }

        // Flush writer
        self.writer.flush() catch {};

        return 0; // 0 = allow commit
    }

    // Rollback hook: discard transaction buffer
    fn rollbackHookCallback(user_data: ?*anyopaque) callconv(.c) void {
        const self: *Self = @ptrCast(@alignCast(user_data orelse return));

        if (self.tx_buffer) |*buf| {
            buf.deinit();
            self.tx_buffer = null;
        }
    }

    // Write a single entry
    fn writeEntry(self: *Self, tx_id: u64, op: *const PendingOp) !void {
        var builder = EntryBuilder.init(self.allocator);
        errdefer builder.deinit();

        _ = builder.setSeq(self.writer.nextSeq());
        _ = builder.setTimestamp(op.timestamp_us);
        _ = builder.setTxId(tx_id);
        _ = builder.setOp(op.op);
        _ = try builder.setTable(op.table);
        _ = builder.setRowid(op.rowid);
        _ = builder.setPrevHash(self.writer.getLastHash());

        // Get context
        const ctx = self.context.toEntryContext() catch ContextEntryContext{
            .user = null,
            .app = null,
            .host = null,
            .pid = null,
        };
        _ = builder.setContext(.{
            .user = ctx.user,
            .app = ctx.app,
            .host = ctx.host,
            .pid = ctx.pid,
        });

        if (op.sql) |sql| {
            _ = try builder.setSql(sql);
        }
        if (op.before) |before| {
            _ = try builder.setBefore(before);
        }
        if (op.after) |after| {
            _ = try builder.setAfter(after);
        }

        var entry = try builder.build();
        defer entry.deinit(self.allocator);
        builder.deinit();

        try self.writer.write(&entry);
    }

    fn shouldCaptureTable(self: *const Self, table: []const u8) bool {
        // Check exclude list first
        if (self.capture.exclude_tables) |exclude| {
            for (exclude) |excl| {
                if (std.mem.eql(u8, table, excl)) return false;
            }
        }

        // Check include list
        if (self.capture.include_tables) |include| {
            for (include) |incl| {
                if (std.mem.eql(u8, table, incl)) return true;
            }
            return false;
        }

        return true;
    }
};

/// Helper to write sqlite3_value as JSON
fn writeValueAsJson(writer: anytype, value: *c.sqlite3_value) !void {
    const vtype = c.sqlite3_value_type(value);

    switch (vtype) {
        c.SQLITE_INTEGER => {
            const val = c.sqlite3_value_int64(value);
            try writer.print("{d}", .{val});
        },
        c.SQLITE_FLOAT => {
            const val = c.sqlite3_value_double(value);
            try writer.print("{d}", .{val});
        },
        c.SQLITE_TEXT => {
            const len: usize = @intCast(c.sqlite3_value_bytes(value));
            const text = c.sqlite3_value_text(value);
            if (text) |t| {
                try writer.writeByte('"');
                const slice = t[0..len];
                for (slice) |ch| {
                    switch (ch) {
                        '"' => try writer.writeAll("\\\""),
                        '\\' => try writer.writeAll("\\\\"),
                        '\n' => try writer.writeAll("\\n"),
                        '\r' => try writer.writeAll("\\r"),
                        '\t' => try writer.writeAll("\\t"),
                        else => try writer.writeByte(ch),
                    }
                }
                try writer.writeByte('"');
            } else {
                try writer.writeAll("null");
            }
        },
        c.SQLITE_BLOB => {
            // Encode as base64 or hex
            const len: usize = @intCast(c.sqlite3_value_bytes(value));
            const blob = c.sqlite3_value_blob(value);
            if (blob) |b| {
                const ptr: [*]const u8 = @ptrCast(b);
                try writer.writeAll("\"<blob:");
                try writer.print("{d}", .{len});
                try writer.writeAll(">\"");
                _ = ptr;
            } else {
                try writer.writeAll("null");
            }
        },
        c.SQLITE_NULL => {
            try writer.writeAll("null");
        },
        else => {
            try writer.writeAll("null");
        },
    }
}

/// Statistics
pub const Stats = struct {
    total_entries: u64,
    total_bytes: u64,
    file_count: usize,
};

test "AuditLog basic" {
    // This test needs a real database
    const allocator = std.testing.allocator;

    // Create test directory
    const tmp_dir = "/tmp/zsqlite_audit_log_test";
    defer std.fs.cwd().deleteTree(tmp_dir) catch {};

    // Open database
    var db = Database.open(":memory:") catch return;
    defer db.close();

    // Initialize audit log
    var audit = try AuditLog.init(allocator, &db, .{
        .log_dir = tmp_dir,
        .app_name = "test_app",
    });
    defer audit.deinit();

    // Start hooks - must be done after audit is at final memory location
    audit.start();

    try audit.setUser("test_user");

    // Create table and insert data
    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO test (name) VALUES ('Alice')");
    try db.exec("UPDATE test SET name = 'Bob' WHERE id = 1");
    try db.exec("DELETE FROM test WHERE id = 1");

    // Flush
    try audit.flush();

    // Check stats
    const stat = audit.stats();
    // Note: In-memory DB with autocommit may not trigger hooks as expected
    // This is a basic sanity test
    _ = stat;
}
