//! Audit Log Writer
//!
//! Manages writing audit entries to files with rotation support.

const std = @import("std");
const Allocator = std.mem.Allocator;
const Entry = @import("entry.zig").Entry;
const Index = @import("index.zig").Index;
const FileInfo = @import("index.zig").FileInfo;
const RotationConfig = @import("index.zig").RotationConfig;
const generateFilename = @import("index.zig").generateFilename;

pub const WriterError = error{
    LogDirNotFound,
    CannotCreateLogDir,
    CannotCreateFile,
    CannotWriteFile,
    CannotWriteIndex,
    CannotOpenFile,
    FileCorrupted,
    OutOfMemory,
    InvalidPath,
} || std.fs.File.OpenError || std.fs.File.WriteError || std.posix.RealPathError;

/// Log file writer
pub const Writer = struct {
    allocator: Allocator,
    /// Log directory path
    log_dir: []const u8,
    /// Current log file handle
    current_file: ?std.fs.File,
    /// Current file path
    current_path: ?[]const u8,
    /// Index tracking all files
    index: Index,
    /// Current file's byte count
    current_bytes: u64,
    /// Current file's entry count
    current_entries: u64,
    /// Last hash written
    last_hash: [64]u8,
    /// Current sequence number
    current_seq: u64,

    const Self = @This();

    /// Initialize writer with a log directory
    pub fn init(allocator: Allocator, log_dir: []const u8, db_name: []const u8) !Self {
        // Create log directory if it doesn't exist
        std.fs.makeDirAbsolute(log_dir) catch |err| switch (err) {
            error.PathAlreadyExists => {},
            else => return WriterError.CannotCreateLogDir,
        };

        // Try to load existing index or create new one
        const index_path = try std.fs.path.join(allocator, &.{ log_dir, "index.json" });
        defer allocator.free(index_path);

        var index: Index = undefined;
        var last_hash: [64]u8 = [_]u8{'0'} ** 64;
        var current_seq: u64 = 0;

        if (std.fs.cwd().readFileAlloc(allocator, index_path, 10 * 1024 * 1024)) |data| {
            defer allocator.free(data);
            index = try Index.load(allocator, data);
            last_hash = index.getLastHash();
            current_seq = index.getLastSeq();
        } else |_| {
            index = try Index.init(allocator, db_name);
        }

        return Self{
            .allocator = allocator,
            .log_dir = try allocator.dupe(u8, log_dir),
            .current_file = null,
            .current_path = null,
            .index = index,
            .current_bytes = 0,
            .current_entries = 0,
            .last_hash = last_hash,
            .current_seq = current_seq,
        };
    }

    pub fn deinit(self: *Self) void {
        if (self.current_file) |f| {
            f.close();
        }
        if (self.current_path) |p| {
            self.allocator.free(p);
        }
        self.index.deinit();
        self.allocator.free(self.log_dir);
        self.* = undefined;
    }

    /// Write an entry to the log
    pub fn write(self: *Self, entry: *const Entry) !void {
        // Ensure we have an open file
        try self.ensureFile();

        // Serialize entry to JSON
        const json = try entry.toJson(self.allocator);
        defer self.allocator.free(json);

        // Write JSON line
        const file = self.current_file.?;
        try file.writeAll(json);
        try file.writeAll("\n");

        // Update counters
        self.current_bytes += json.len + 1;
        self.current_entries += 1;
        self.last_hash = entry.hash;
        self.current_seq = entry.seq;

        // Update index
        self.index.updateCurrentFile(
            entry.seq,
            entry.timestamp_us,
            self.current_entries,
            self.current_bytes,
            entry.hash,
        );

        // Check if rotation is needed
        if (self.shouldRotate()) {
            try self.rotate();
        }
    }

    /// Get the next sequence number
    pub fn nextSeq(self: *Self) u64 {
        return self.current_seq + 1;
    }

    /// Get the last hash for chain continuity
    pub fn getLastHash(self: *const Self) [64]u8 {
        return self.last_hash;
    }

    /// Force rotation
    pub fn rotate(self: *Self) !void {
        // Close current file
        if (self.current_file) |f| {
            f.close();
            self.current_file = null;
        }

        // Mark current file as closed in index
        self.index.closeCurrentFile();

        // Save index
        try self.saveIndex();

        // Reset counters (will create new file on next write)
        self.current_bytes = 0;
        self.current_entries = 0;
        if (self.current_path) |p| {
            self.allocator.free(p);
            self.current_path = null;
        }
    }

    /// Flush any buffered data
    pub fn flush(self: *Self) !void {
        if (self.current_file) |f| {
            try f.sync();
        }
        try self.saveIndex();
    }

    /// Get statistics
    pub fn getStats(self: *const Self) Stats {
        var total_entries: u64 = 0;
        var total_bytes: u64 = 0;

        for (self.index.files.items) |*file| {
            total_entries += file.entries;
            total_bytes += file.bytes;
        }

        return Stats{
            .total_entries = total_entries,
            .total_bytes = total_bytes,
            .file_count = self.index.files.items.len,
            .current_file_entries = self.current_entries,
            .current_file_bytes = self.current_bytes,
        };
    }

    // Private methods

    fn ensureFile(self: *Self) !void {
        if (self.current_file != null) return;

        // Create new file
        const now = std.time.microTimestamp();
        const file_id = self.index.getNextFileId();
        const filename = try generateFilename(self.allocator, file_id, now);
        defer self.allocator.free(filename);

        const file_path = try std.fs.path.join(self.allocator, &.{ self.log_dir, filename });

        // Create and open file
        const file = std.fs.cwd().createFile(file_path, .{ .exclusive = true }) catch {
            self.allocator.free(file_path);
            return WriterError.CannotCreateFile;
        };

        self.current_file = file;
        self.current_path = file_path;
        self.current_bytes = 0;
        self.current_entries = 0;

        // Add to index
        const file_info = FileInfo{
            .id = file_id,
            .filename = try self.allocator.dupe(u8, filename),
            .seq_start = self.current_seq + 1,
            .seq_end = null,
            .ts_start = now,
            .ts_end = null,
            .entries = 0,
            .bytes = 0,
            .first_hash = self.last_hash,
            .last_hash = self.last_hash,
            .closed = false,
        };
        defer self.allocator.free(file_info.filename);

        try self.index.addFile(file_info);
    }

    fn shouldRotate(self: *const Self) bool {
        // Check size
        if (self.current_bytes >= self.index.rotation.max_bytes) {
            return true;
        }

        // Check age (if we have entries)
        if (self.current_entries > 0) {
            if (self.index.getCurrentFile()) |file| {
                const now = std.time.microTimestamp();
                const age_us = now - file.ts_start;
                const max_age_us: i64 = @as(i64, @intCast(self.index.rotation.max_age_days)) * 24 * 60 * 60 * 1_000_000;
                if (age_us >= max_age_us) {
                    return true;
                }
            }
        }

        return false;
    }

    fn saveIndex(self: *Self) !void {
        const json = try self.index.toJson();
        defer self.allocator.free(json);

        const index_path = try std.fs.path.join(self.allocator, &.{ self.log_dir, "index.json" });
        defer self.allocator.free(index_path);

        const file = std.fs.cwd().createFile(index_path, .{}) catch {
            return WriterError.CannotWriteIndex;
        };
        defer file.close();

        file.writeAll(json) catch {
            return WriterError.CannotWriteIndex;
        };
    }
};

/// Writer statistics
pub const Stats = struct {
    total_entries: u64,
    total_bytes: u64,
    file_count: usize,
    current_file_entries: u64,
    current_file_bytes: u64,
};

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

    // Use temp directory
    const tmp_dir = "/tmp/zcatsql_audit_test";
    defer std.fs.cwd().deleteTree(tmp_dir) catch {};

    var writer = try Writer.init(allocator, tmp_dir, "test.db");
    defer writer.deinit();

    // Create and write an entry
    const EntryBuilder = @import("entry.zig").EntryBuilder;

    var builder = EntryBuilder.init(allocator);
    _ = builder.setSeq(writer.nextSeq());
    _ = builder.setTimestampNow();
    _ = builder.setTxId(1);
    _ = builder.setOp(.INSERT);
    _ = try builder.setTable("test_table");
    _ = builder.setRowid(1);
    _ = builder.setPrevHash(writer.getLastHash());

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

    try writer.write(&entry);

    // Check stats
    const stats = writer.getStats();
    try std.testing.expectEqual(@as(u64, 1), stats.total_entries);
    try std.testing.expect(stats.total_bytes > 0);
    try std.testing.expectEqual(@as(usize, 1), stats.file_count);

    // Flush and verify file exists
    try writer.flush();

    const index_exists = std.fs.cwd().access(tmp_dir ++ "/index.json", .{});
    try std.testing.expect(index_exists != error.FileNotFound);
}

test "Writer rotation" {
    const allocator = std.testing.allocator;

    const tmp_dir = "/tmp/zcatsql_audit_rotation_test";
    defer std.fs.cwd().deleteTree(tmp_dir) catch {};

    var writer = try Writer.init(allocator, tmp_dir, "test.db");
    defer writer.deinit();

    // Set very small rotation threshold
    writer.index.rotation.max_bytes = 100;

    const EntryBuilder = @import("entry.zig").EntryBuilder;

    // Write entries until rotation
    var i: u32 = 0;
    while (i < 10) : (i += 1) {
        var builder = EntryBuilder.init(allocator);
        _ = builder.setSeq(writer.nextSeq());
        _ = builder.setTimestampNow();
        _ = builder.setTxId(1);
        _ = builder.setOp(.INSERT);
        _ = try builder.setTable("test_table");
        _ = builder.setRowid(@intCast(i));
        _ = try builder.setAfter("{\"value\":12345678901234567890}");
        _ = builder.setPrevHash(writer.getLastHash());

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

        try writer.write(&entry);
    }

    try writer.flush();

    // Should have multiple files due to rotation
    const stats = writer.getStats();
    try std.testing.expect(stats.file_count > 1);
}
