//! Audit Log Entry
//!
//! Defines the structure and JSON serialization for audit log entries.

const std = @import("std");
const Allocator = std.mem.Allocator;

/// Operation type
pub const Operation = enum {
    INSERT,
    UPDATE,
    DELETE,

    pub fn toString(self: Operation) []const u8 {
        return switch (self) {
            .INSERT => "INSERT",
            .UPDATE => "UPDATE",
            .DELETE => "DELETE",
        };
    }

    pub fn fromString(s: []const u8) ?Operation {
        if (std.mem.eql(u8, s, "INSERT")) return .INSERT;
        if (std.mem.eql(u8, s, "UPDATE")) return .UPDATE;
        if (std.mem.eql(u8, s, "DELETE")) return .DELETE;
        return null;
    }
};

/// Context information for an audit entry
pub const Context = struct {
    user: ?[]const u8 = null,
    app: ?[]const u8 = null,
    host: ?[]const u8 = null,
    pid: ?u32 = null,

    pub fn deinit(self: *Context, allocator: Allocator) void {
        if (self.user) |u| allocator.free(u);
        if (self.app) |a| allocator.free(a);
        if (self.host) |h| allocator.free(h);
        self.* = .{};
    }

    pub fn clone(self: *const Context, allocator: Allocator) !Context {
        return Context{
            .user = if (self.user) |u| try allocator.dupe(u8, u) else null,
            .app = if (self.app) |a| try allocator.dupe(u8, a) else null,
            .host = if (self.host) |h| try allocator.dupe(u8, h) else null,
            .pid = self.pid,
        };
    }
};

/// A single audit log entry
pub const Entry = struct {
    /// Sequence number (global, never repeats)
    seq: u64,
    /// Timestamp in microseconds since epoch
    timestamp_us: i64,
    /// Transaction ID (groups operations)
    tx_id: u64,
    /// Context (user, app, host, pid)
    ctx: Context,
    /// Operation type
    op: Operation,
    /// Table name
    table: []const u8,
    /// SQLite rowid
    rowid: i64,
    /// SQL statement (if available)
    sql: ?[]const u8,
    /// Values before operation (null for INSERT)
    before: ?[]const u8,
    /// Values after operation (null for DELETE)
    after: ?[]const u8,
    /// Hash of previous entry
    prev_hash: [64]u8,
    /// Hash of this entry
    hash: [64]u8,

    const Self = @This();

    pub fn deinit(self: *Self, allocator: Allocator) void {
        self.ctx.deinit(allocator);
        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;
    }

    /// Format timestamp as ISO8601 with microseconds
    pub fn formatTimestamp(self: *const Self, buf: *[30]u8) []const u8 {
        const secs: u64 = @intCast(@divFloor(self.timestamp_us, 1_000_000));
        const us: u64 = @intCast(@mod(self.timestamp_us, 1_000_000));

        const epoch_secs: i64 = @intCast(secs);
        const es = std.time.epoch.EpochSeconds{ .secs = @intCast(epoch_secs) };
        const day_seconds = es.getDaySeconds();
        const year_day = es.getEpochDay().calculateYearDay();

        const hours = day_seconds.getHoursIntoDay();
        const minutes = day_seconds.getMinutesIntoHour();
        const seconds_val = day_seconds.getSecondsIntoMinute();

        const month_day = year_day.calculateMonthDay();
        const year = year_day.year;
        const month = month_day.month.numeric();
        const day = month_day.day_index + 1;

        const len = std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>6}Z", .{
            year, month, day, hours, minutes, seconds_val, us,
        }) catch return "";
        return buf[0..len.len];
    }

    /// Serialize entry to JSON (without trailing newline)
    pub fn toJson(self: *const Self, allocator: Allocator) ![]u8 {
        var list: std.ArrayListUnmanaged(u8) = .empty;
        errdefer list.deinit(allocator);

        const writer = list.writer(allocator);

        // Format timestamp
        var ts_buf: [30]u8 = undefined;
        const ts_str = self.formatTimestamp(&ts_buf);

        try writer.writeAll("{");

        // seq
        try writer.print("\"seq\":{d},", .{self.seq});

        // ts
        try writer.print("\"ts\":\"{s}\",", .{ts_str});

        // tx_id
        try writer.print("\"tx_id\":{d},", .{self.tx_id});

        // ctx
        try writer.writeAll("\"ctx\":{");
        var ctx_first = true;
        if (self.ctx.user) |u| {
            try writer.print("\"user\":", .{});
            try writeJsonString(writer, u);
            ctx_first = false;
        }
        if (self.ctx.app) |a| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"app\":", .{});
            try writeJsonString(writer, a);
            ctx_first = false;
        }
        if (self.ctx.host) |h| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"host\":", .{});
            try writeJsonString(writer, h);
            ctx_first = false;
        }
        if (self.ctx.pid) |p| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"pid\":{d}", .{p});
        }
        try writer.writeAll("},");

        // op
        try writer.print("\"op\":\"{s}\",", .{self.op.toString()});

        // table
        try writer.print("\"table\":", .{});
        try writeJsonString(writer, self.table);
        try writer.writeAll(",");

        // rowid
        try writer.print("\"rowid\":{d},", .{self.rowid});

        // sql (optional)
        if (self.sql) |s| {
            try writer.print("\"sql\":", .{});
            try writeJsonString(writer, s);
            try writer.writeAll(",");
        }

        // before (optional, already JSON)
        if (self.before) |b| {
            try writer.print("\"before\":{s},", .{b});
        }

        // after (optional, already JSON)
        if (self.after) |a| {
            try writer.print("\"after\":{s},", .{a});
        }

        // prev_hash
        try writer.print("\"prev_hash\":\"{s}\",", .{self.prev_hash});

        // hash
        try writer.print("\"hash\":\"{s}\"", .{self.hash});

        try writer.writeAll("}");

        return list.toOwnedSlice(allocator);
    }

    /// Serialize entry to JSON for hashing (without hash field)
    pub fn toJsonForHash(self: *const Self, allocator: Allocator) ![]u8 {
        var list: std.ArrayListUnmanaged(u8) = .empty;
        errdefer list.deinit(allocator);

        const writer = list.writer(allocator);

        // Format timestamp
        var ts_buf: [30]u8 = undefined;
        const ts_str = self.formatTimestamp(&ts_buf);

        try writer.writeAll("{");

        // seq
        try writer.print("\"seq\":{d},", .{self.seq});

        // ts
        try writer.print("\"ts\":\"{s}\",", .{ts_str});

        // tx_id
        try writer.print("\"tx_id\":{d},", .{self.tx_id});

        // ctx
        try writer.writeAll("\"ctx\":{");
        var ctx_first = true;
        if (self.ctx.user) |u| {
            try writer.print("\"user\":", .{});
            try writeJsonString(writer, u);
            ctx_first = false;
        }
        if (self.ctx.app) |a| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"app\":", .{});
            try writeJsonString(writer, a);
            ctx_first = false;
        }
        if (self.ctx.host) |h| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"host\":", .{});
            try writeJsonString(writer, h);
            ctx_first = false;
        }
        if (self.ctx.pid) |p| {
            if (!ctx_first) try writer.writeAll(",");
            try writer.print("\"pid\":{d}", .{p});
        }
        try writer.writeAll("},");

        // op
        try writer.print("\"op\":\"{s}\",", .{self.op.toString()});

        // table
        try writer.print("\"table\":", .{});
        try writeJsonString(writer, self.table);
        try writer.writeAll(",");

        // rowid
        try writer.print("\"rowid\":{d},", .{self.rowid});

        // sql (optional)
        if (self.sql) |s| {
            try writer.print("\"sql\":", .{});
            try writeJsonString(writer, s);
            try writer.writeAll(",");
        }

        // before (optional, already JSON)
        if (self.before) |b| {
            try writer.print("\"before\":{s},", .{b});
        }

        // after (optional, already JSON)
        if (self.after) |a| {
            try writer.print("\"after\":{s},", .{a});
        }

        // prev_hash (include in hash calculation)
        try writer.print("\"prev_hash\":\"{s}\"", .{self.prev_hash});

        try writer.writeAll("}");

        return list.toOwnedSlice(allocator);
    }
};

/// Write a JSON-escaped string
fn writeJsonString(writer: anytype, s: []const u8) !void {
    try writer.writeByte('"');
    for (s) |ch| {
        switch (ch) {
            '"' => try writer.writeAll("\\\""),
            '\\' => try writer.writeAll("\\\\"),
            '\n' => try writer.writeAll("\\n"),
            '\r' => try writer.writeAll("\\r"),
            '\t' => try writer.writeAll("\\t"),
            0x00...0x08, 0x0b, 0x0c, 0x0e...0x1f => {
                try writer.print("\\u{x:0>4}", .{ch});
            },
            else => try writer.writeByte(ch),
        }
    }
    try writer.writeByte('"');
}

/// Builder for creating entries
pub const EntryBuilder = struct {
    allocator: Allocator,
    seq: u64 = 0,
    timestamp_us: i64 = 0,
    tx_id: u64 = 0,
    ctx: Context = .{},
    op: Operation = .INSERT,
    table: ?[]const u8 = null,
    rowid: i64 = 0,
    sql: ?[]const u8 = null,
    before: ?[]const u8 = null,
    after: ?[]const u8 = null,
    prev_hash: [64]u8 = [_]u8{'0'} ** 64,

    const Self = @This();

    pub fn init(allocator: Allocator) Self {
        return .{ .allocator = allocator };
    }

    pub fn setSeq(self: *Self, seq: u64) *Self {
        self.seq = seq;
        return self;
    }

    pub fn setTimestamp(self: *Self, timestamp_us: i64) *Self {
        self.timestamp_us = timestamp_us;
        return self;
    }

    pub fn setTimestampNow(self: *Self) *Self {
        self.timestamp_us = std.time.microTimestamp();
        return self;
    }

    pub fn setTxId(self: *Self, tx_id: u64) *Self {
        self.tx_id = tx_id;
        return self;
    }

    pub fn setContext(self: *Self, ctx: Context) *Self {
        self.ctx = ctx;
        return self;
    }

    pub fn setOp(self: *Self, op: Operation) *Self {
        self.op = op;
        return self;
    }

    pub fn setTable(self: *Self, table: []const u8) !*Self {
        self.table = try self.allocator.dupe(u8, table);
        return self;
    }

    pub fn setRowid(self: *Self, rowid: i64) *Self {
        self.rowid = rowid;
        return self;
    }

    pub fn setSql(self: *Self, sql: []const u8) !*Self {
        self.sql = try self.allocator.dupe(u8, sql);
        return self;
    }

    pub fn setBefore(self: *Self, before: []const u8) !*Self {
        self.before = try self.allocator.dupe(u8, before);
        return self;
    }

    pub fn setAfter(self: *Self, after: []const u8) !*Self {
        self.after = try self.allocator.dupe(u8, after);
        return self;
    }

    pub fn setPrevHash(self: *Self, prev_hash: [64]u8) *Self {
        self.prev_hash = prev_hash;
        return self;
    }

    /// Build the entry and compute its hash
    pub fn build(self: *Self) !Entry {
        const table = self.table orelse return error.MissingTable;

        var entry = Entry{
            .seq = self.seq,
            .timestamp_us = self.timestamp_us,
            .tx_id = self.tx_id,
            .ctx = try self.ctx.clone(self.allocator),
            .op = self.op,
            .table = try self.allocator.dupe(u8, table),
            .rowid = self.rowid,
            .sql = if (self.sql) |s| try self.allocator.dupe(u8, s) else null,
            .before = if (self.before) |b| try self.allocator.dupe(u8, b) else null,
            .after = if (self.after) |a| try self.allocator.dupe(u8, a) else null,
            .prev_hash = self.prev_hash,
            .hash = undefined,
        };

        // Compute hash
        const json_for_hash = try entry.toJsonForHash(self.allocator);
        defer self.allocator.free(json_for_hash);

        var hash: [32]u8 = undefined;
        std.crypto.hash.sha2.Sha256.hash(json_for_hash, &hash, .{});

        // Convert to hex
        entry.hash = std.fmt.bytesToHex(hash, .lower);

        return entry;
    }

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

test "Entry JSON serialization" {
    const allocator = std.testing.allocator;

    var builder = EntryBuilder.init(allocator);
    defer builder.deinit();

    _ = builder.setSeq(1);
    _ = builder.setTimestamp(1733667135123456); // 2024-12-08T14:32:15.123456Z approx
    _ = builder.setTxId(42);
    _ = builder.setContext(.{
        .user = "alice",
        .app = "test_app",
        .host = "localhost",
        .pid = 12345,
    });
    _ = builder.setOp(.UPDATE);
    _ = try builder.setTable("users");
    _ = builder.setRowid(157);
    _ = try builder.setSql("UPDATE users SET name = 'Bob' WHERE id = 157");
    _ = try builder.setBefore("{\"id\":157,\"name\":\"Alice\"}");
    _ = try builder.setAfter("{\"id\":157,\"name\":\"Bob\"}");

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

    const json = try entry.toJson(allocator);
    defer allocator.free(json);

    // Verify it contains expected fields
    try std.testing.expect(std.mem.indexOf(u8, json, "\"seq\":1") != null);
    try std.testing.expect(std.mem.indexOf(u8, json, "\"op\":\"UPDATE\"") != null);
    try std.testing.expect(std.mem.indexOf(u8, json, "\"table\":\"users\"") != null);
    try std.testing.expect(std.mem.indexOf(u8, json, "\"user\":\"alice\"") != null);
    try std.testing.expect(std.mem.indexOf(u8, json, "\"hash\":") != null);
}

test "Entry hash chain" {
    const allocator = std.testing.allocator;

    // Create first entry (genesis)
    var builder1 = EntryBuilder.init(allocator);
    _ = builder1.setSeq(1);
    _ = builder1.setTimestamp(1000000);
    _ = builder1.setTxId(1);
    _ = builder1.setOp(.INSERT);
    _ = try builder1.setTable("test");
    _ = builder1.setRowid(1);

    var entry1 = try builder1.build();
    defer entry1.deinit(allocator);
    builder1.deinit();

    // Verify genesis has zeros for prev_hash
    try std.testing.expectEqualStrings(&([_]u8{'0'} ** 64), &entry1.prev_hash);

    // Create second entry with first's hash
    var builder2 = EntryBuilder.init(allocator);
    _ = builder2.setSeq(2);
    _ = builder2.setTimestamp(1000001);
    _ = builder2.setTxId(1);
    _ = builder2.setOp(.UPDATE);
    _ = try builder2.setTable("test");
    _ = builder2.setRowid(1);
    _ = builder2.setPrevHash(entry1.hash);

    var entry2 = try builder2.build();
    defer entry2.deinit(allocator);
    builder2.deinit();

    // Verify chain
    try std.testing.expectEqualStrings(&entry1.hash, &entry2.prev_hash);

    // Hashes should be different
    try std.testing.expect(!std.mem.eql(u8, &entry1.hash, &entry2.hash));
}
