//! Audit Log Index
//!
//! Manages the index.json file that tracks all log files and their metadata.

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

/// Information about a single log file
pub const FileInfo = struct {
    /// File ID (1-based, sequential)
    id: u32,
    /// Filename (e.g., "0001_20251208_143200.log")
    filename: []const u8,
    /// First sequence number in file
    seq_start: u64,
    /// Last sequence number in file (null if still open)
    seq_end: ?u64,
    /// First timestamp in file
    ts_start: i64,
    /// Last timestamp in file (null if still open)
    ts_end: ?i64,
    /// Number of entries in file
    entries: u64,
    /// File size in bytes
    bytes: u64,
    /// Hash of first entry
    first_hash: [64]u8,
    /// Hash of last entry
    last_hash: [64]u8,
    /// Whether file is closed (rotated)
    closed: bool,

    pub fn deinit(self: *FileInfo, allocator: Allocator) void {
        allocator.free(self.filename);
        self.* = undefined;
    }

    pub fn clone(self: *const FileInfo, allocator: Allocator) !FileInfo {
        return FileInfo{
            .id = self.id,
            .filename = try allocator.dupe(u8, self.filename),
            .seq_start = self.seq_start,
            .seq_end = self.seq_end,
            .ts_start = self.ts_start,
            .ts_end = self.ts_end,
            .entries = self.entries,
            .bytes = self.bytes,
            .first_hash = self.first_hash,
            .last_hash = self.last_hash,
            .closed = self.closed,
        };
    }
};

/// Rotation configuration
pub const RotationConfig = struct {
    /// Maximum file size in bytes (default: 100MB)
    max_bytes: u64 = 100 * 1024 * 1024,
    /// Maximum file age in days (default: 30)
    max_age_days: u32 = 30,
};

/// The audit log index
pub const Index = struct {
    allocator: Allocator,
    /// Index format version
    version: u32 = 1,
    /// Database name this audit belongs to
    db_name: []const u8,
    /// When the audit system was created
    created: i64,
    /// List of log files
    files: std.ArrayListUnmanaged(FileInfo),
    /// Rotation configuration
    rotation: RotationConfig,

    const Self = @This();

    /// Create a new index
    pub fn init(allocator: Allocator, db_name: []const u8) !Self {
        return Self{
            .allocator = allocator,
            .db_name = try allocator.dupe(u8, db_name),
            .created = std.time.microTimestamp(),
            .files = .empty,
            .rotation = .{},
        };
    }

    /// Load index from JSON
    pub fn load(allocator: Allocator, json_data: []const u8) !Self {
        const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_data, .{});
        defer parsed.deinit();

        const root = parsed.value.object;

        var index = Self{
            .allocator = allocator,
            .version = @intCast(root.get("version").?.integer),
            .db_name = try allocator.dupe(u8, root.get("db_name").?.string),
            .created = root.get("created").?.integer,
            .files = .empty,
            .rotation = .{},
        };

        // Parse rotation config
        if (root.get("rotation")) |rot| {
            if (rot.object.get("max_bytes")) |mb| {
                index.rotation.max_bytes = @intCast(mb.integer);
            }
            if (rot.object.get("max_age_days")) |mad| {
                index.rotation.max_age_days = @intCast(mad.integer);
            }
        }

        // Parse files
        if (root.get("files")) |files_arr| {
            for (files_arr.array.items) |file_val| {
                const file_obj = file_val.object;

                var first_hash: [64]u8 = undefined;
                var last_hash: [64]u8 = undefined;

                const fh_str = file_obj.get("first_hash").?.string;
                const lh_str = file_obj.get("last_hash").?.string;
                @memcpy(&first_hash, fh_str[0..64]);
                @memcpy(&last_hash, lh_str[0..64]);

                const file_info = FileInfo{
                    .id = @intCast(file_obj.get("id").?.integer),
                    .filename = try allocator.dupe(u8, file_obj.get("filename").?.string),
                    .seq_start = @intCast(file_obj.get("seq_start").?.integer),
                    .seq_end = if (file_obj.get("seq_end")) |se| blk: {
                        if (se == .null) break :blk null;
                        break :blk @intCast(se.integer);
                    } else null,
                    .ts_start = file_obj.get("ts_start").?.integer,
                    .ts_end = if (file_obj.get("ts_end")) |te| blk: {
                        if (te == .null) break :blk null;
                        break :blk te.integer;
                    } else null,
                    .entries = @intCast(file_obj.get("entries").?.integer),
                    .bytes = @intCast(file_obj.get("bytes").?.integer),
                    .first_hash = first_hash,
                    .last_hash = last_hash,
                    .closed = file_obj.get("closed").?.bool,
                };
                try index.files.append(index.allocator, file_info);
            }
        }

        return index;
    }

    pub fn deinit(self: *Self) void {
        for (self.files.items) |*f| {
            f.deinit(self.allocator);
        }
        self.files.deinit(self.allocator);
        self.allocator.free(self.db_name);
        self.* = undefined;
    }

    /// Add a new file to the index
    pub fn addFile(self: *Self, info: FileInfo) !void {
        try self.files.append(self.allocator, try info.clone(self.allocator));
    }

    /// Get the current (open) file
    pub fn getCurrentFile(self: anytype) ?*FileInfo {
        if (self.files.items.len == 0) return null;
        const last = &self.files.items[self.files.items.len - 1];
        if (!last.closed) return last;
        return null;
    }

    /// Update the current file's metadata
    pub fn updateCurrentFile(self: *Self, seq_end: u64, ts_end: i64, entries: u64, bytes: u64, last_hash: [64]u8) void {
        if (self.getCurrentFile()) |file| {
            file.seq_end = seq_end;
            file.ts_end = ts_end;
            file.entries = entries;
            file.bytes = bytes;
            file.last_hash = last_hash;
        }
    }

    /// Close the current file
    pub fn closeCurrentFile(self: *Self) void {
        if (self.getCurrentFile()) |file| {
            file.closed = true;
        }
    }

    /// Get next file ID
    pub fn getNextFileId(self: *const Self) u32 {
        if (self.files.items.len == 0) return 1;
        return self.files.items[self.files.items.len - 1].id + 1;
    }

    /// Get last hash (for chain continuity)
    pub fn getLastHash(self: *const Self) [64]u8 {
        if (self.files.items.len == 0) return [_]u8{'0'} ** 64;
        return self.files.items[self.files.items.len - 1].last_hash;
    }

    /// Get last sequence number
    pub fn getLastSeq(self: *const Self) u64 {
        if (self.files.items.len == 0) return 0;
        const last = &self.files.items[self.files.items.len - 1];
        return last.seq_end orelse last.seq_start;
    }

    /// Find file containing a specific sequence number
    pub fn findFileBySeq(self: *const Self, seq: u64) ?*const FileInfo {
        for (self.files.items) |*file| {
            const end = file.seq_end orelse std.math.maxInt(u64);
            if (seq >= file.seq_start and seq <= end) {
                return file;
            }
        }
        return null;
    }

    /// Find files within a time range
    pub fn findFilesByTimeRange(self: *Self, start_us: i64, end_us: i64) ![]const FileInfo {
        var result: std.ArrayListUnmanaged(FileInfo) = .empty;
        errdefer result.deinit(self.allocator);

        for (self.files.items) |*file| {
            const file_end = file.ts_end orelse std.math.maxInt(i64);
            // Check overlap
            if (file.ts_start <= end_us and file_end >= start_us) {
                try result.append(self.allocator, try file.clone(self.allocator));
            }
        }

        return result.toOwnedSlice(self.allocator);
    }

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

        const writer = list.writer(self.allocator);

        try writer.writeAll("{\n");
        try writer.print("  \"version\": {d},\n", .{self.version});
        try writer.print("  \"db_name\": \"{s}\",\n", .{self.db_name});
        try writer.print("  \"created\": {d},\n", .{self.created});

        // Files array
        try writer.writeAll("  \"files\": [\n");
        for (self.files.items, 0..) |*file, i| {
            try writer.writeAll("    {\n");
            try writer.print("      \"id\": {d},\n", .{file.id});
            try writer.print("      \"filename\": \"{s}\",\n", .{file.filename});
            try writer.print("      \"seq_start\": {d},\n", .{file.seq_start});
            if (file.seq_end) |se| {
                try writer.print("      \"seq_end\": {d},\n", .{se});
            } else {
                try writer.writeAll("      \"seq_end\": null,\n");
            }
            try writer.print("      \"ts_start\": {d},\n", .{file.ts_start});
            if (file.ts_end) |te| {
                try writer.print("      \"ts_end\": {d},\n", .{te});
            } else {
                try writer.writeAll("      \"ts_end\": null,\n");
            }
            try writer.print("      \"entries\": {d},\n", .{file.entries});
            try writer.print("      \"bytes\": {d},\n", .{file.bytes});
            try writer.print("      \"first_hash\": \"{s}\",\n", .{file.first_hash});
            try writer.print("      \"last_hash\": \"{s}\",\n", .{file.last_hash});
            try writer.print("      \"closed\": {s}\n", .{if (file.closed) "true" else "false"});
            if (i < self.files.items.len - 1) {
                try writer.writeAll("    },\n");
            } else {
                try writer.writeAll("    }\n");
            }
        }
        try writer.writeAll("  ],\n");

        // Rotation config
        try writer.writeAll("  \"rotation\": {\n");
        try writer.print("    \"max_bytes\": {d},\n", .{self.rotation.max_bytes});
        try writer.print("    \"max_age_days\": {d}\n", .{self.rotation.max_age_days});
        try writer.writeAll("  }\n");

        try writer.writeAll("}\n");

        return list.toOwnedSlice(self.allocator);
    }
};

/// Generate filename for a new log file
pub fn generateFilename(allocator: Allocator, file_id: u32, timestamp_us: i64) ![]u8 {
    const secs: u64 = @intCast(@divFloor(timestamp_us, 1_000_000));
    const es = std.time.epoch.EpochSeconds{ .secs = @intCast(secs) };
    const day_seconds = es.getDaySeconds();
    const year_day = es.getEpochDay().calculateYearDay();

    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 hour = day_seconds.getHoursIntoDay();
    const minute = day_seconds.getMinutesIntoHour();
    const second = day_seconds.getSecondsIntoMinute();

    return std.fmt.allocPrint(allocator, "{d:0>4}_{d:0>4}{d:0>2}{d:0>2}_{d:0>2}{d:0>2}{d:0>2}.log", .{
        file_id, year, month, day, hour, minute, second,
    });
}

test "Index basic operations" {
    const allocator = std.testing.allocator;

    var index = try Index.init(allocator, "test.db");
    defer index.deinit();

    try std.testing.expectEqualStrings("test.db", index.db_name);
    try std.testing.expectEqual(@as(usize, 0), index.files.items.len);
    try std.testing.expectEqual(@as(u32, 1), index.getNextFileId());
}

test "Index add and find file" {
    const allocator = std.testing.allocator;

    var index = try Index.init(allocator, "test.db");
    defer index.deinit();

    const file_info = FileInfo{
        .id = 1,
        .filename = try allocator.dupe(u8, "0001_test.log"),
        .seq_start = 1,
        .seq_end = 100,
        .ts_start = 1000000,
        .ts_end = 2000000,
        .entries = 100,
        .bytes = 50000,
        .first_hash = [_]u8{'0'} ** 64,
        .last_hash = [_]u8{'a'} ** 64,
        .closed = true,
    };
    defer allocator.free(file_info.filename);

    try index.addFile(file_info);

    try std.testing.expectEqual(@as(usize, 1), index.files.items.len);
    try std.testing.expectEqual(@as(u32, 2), index.getNextFileId());

    // Find by seq
    const found = index.findFileBySeq(50);
    try std.testing.expect(found != null);
    try std.testing.expectEqual(@as(u32, 1), found.?.id);

    // Not found
    const not_found = index.findFileBySeq(200);
    try std.testing.expect(not_found == null);
}

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

    var index = try Index.init(allocator, "mydb.sqlite");
    defer index.deinit();

    index.rotation.max_bytes = 50 * 1024 * 1024;
    index.rotation.max_age_days = 15;

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

    // Parse it back
    var parsed = try Index.load(allocator, json);
    defer parsed.deinit();

    try std.testing.expectEqualStrings("mydb.sqlite", parsed.db_name);
    try std.testing.expectEqual(@as(u64, 50 * 1024 * 1024), parsed.rotation.max_bytes);
    try std.testing.expectEqual(@as(u32, 15), parsed.rotation.max_age_days);
}

test "generateFilename" {
    const allocator = std.testing.allocator;

    // Test with a known timestamp: 2024-12-08 14:32:15 UTC
    const ts: i64 = 1733667135000000;
    const filename = try generateFilename(allocator, 1, ts);
    defer allocator.free(filename);

    // Should be format: NNNN_YYYYMMDD_HHMMSS.log
    try std.testing.expect(filename.len > 0);
    try std.testing.expect(std.mem.startsWith(u8, filename, "0001_"));
    try std.testing.expect(std.mem.endsWith(u8, filename, ".log"));
}
