//! Audit Log Verification
//!
//! Verifies the integrity of audit logs by checking hash chains.

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

/// Verification result
pub const VerifyResult = struct {
    /// Whether the entire chain is valid
    valid: bool,
    /// Total entries verified
    entries_verified: u64,
    /// Total files verified
    files_verified: usize,
    /// First invalid entry (if any)
    first_invalid_seq: ?u64,
    /// Error message (if any)
    error_message: ?[]const u8,

    pub fn deinit(self: *VerifyResult, allocator: Allocator) void {
        if (self.error_message) |msg| {
            allocator.free(msg);
        }
        self.* = undefined;
    }
};

/// Verify the integrity of audit logs in a directory
pub fn verifyChain(allocator: Allocator, log_dir: []const u8) !VerifyResult {
    // Load index
    const index_path = try std.fs.path.join(allocator, &.{ log_dir, "index.json" });
    defer allocator.free(index_path);

    const index_data = std.fs.cwd().readFileAlloc(allocator, index_path, 10 * 1024 * 1024) catch |err| {
        return VerifyResult{
            .valid = false,
            .entries_verified = 0,
            .files_verified = 0,
            .first_invalid_seq = null,
            .error_message = try std.fmt.allocPrint(allocator, "Cannot read index.json: {}", .{err}),
        };
    };
    defer allocator.free(index_data);

    var index = Index.load(allocator, index_data) catch |err| {
        return VerifyResult{
            .valid = false,
            .entries_verified = 0,
            .files_verified = 0,
            .first_invalid_seq = null,
            .error_message = try std.fmt.allocPrint(allocator, "Invalid index.json: {}", .{err}),
        };
    };
    defer index.deinit();

    var total_entries: u64 = 0;
    var expected_prev_hash: [64]u8 = [_]u8{'0'} ** 64; // Genesis

    // Verify each file
    for (index.files.items, 0..) |*file_info, file_idx| {
        // Check chain continuity between files
        if (file_idx > 0) {
            if (!std.mem.eql(u8, &file_info.first_hash, &expected_prev_hash)) {
                return VerifyResult{
                    .valid = false,
                    .entries_verified = total_entries,
                    .files_verified = file_idx,
                    .first_invalid_seq = file_info.seq_start,
                    .error_message = try std.fmt.allocPrint(
                        allocator,
                        "File chain broken at file {d}: expected prev_hash {s}, got {s}",
                        .{ file_info.id, expected_prev_hash, file_info.first_hash },
                    ),
                };
            }
        }

        // Verify entries in file
        const file_path = try std.fs.path.join(allocator, &.{ log_dir, file_info.filename });
        defer allocator.free(file_path);

        const result = try verifyFile(allocator, file_path, expected_prev_hash);
        if (!result.valid) {
            return VerifyResult{
                .valid = false,
                .entries_verified = total_entries + result.entries_verified,
                .files_verified = file_idx,
                .first_invalid_seq = result.first_invalid_seq,
                .error_message = result.error_message,
            };
        }

        total_entries += result.entries_verified;
        expected_prev_hash = result.last_hash;

        // Verify file's recorded last_hash matches
        if (!std.mem.eql(u8, &file_info.last_hash, &result.last_hash)) {
            return VerifyResult{
                .valid = false,
                .entries_verified = total_entries,
                .files_verified = file_idx + 1,
                .first_invalid_seq = null,
                .error_message = try std.fmt.allocPrint(
                    allocator,
                    "File {d} last_hash mismatch: index says {s}, actual {s}",
                    .{ file_info.id, file_info.last_hash, result.last_hash },
                ),
            };
        }
    }

    return VerifyResult{
        .valid = true,
        .entries_verified = total_entries,
        .files_verified = index.files.items.len,
        .first_invalid_seq = null,
        .error_message = null,
    };
}

/// Result from verifying a single file
const FileVerifyResult = struct {
    valid: bool,
    entries_verified: u64,
    first_invalid_seq: ?u64,
    last_hash: [64]u8,
    error_message: ?[]const u8,
};

/// Verify a single log file
fn verifyFile(allocator: Allocator, file_path: []const u8, expected_prev_hash: [64]u8) !FileVerifyResult {
    const file_contents = std.fs.cwd().readFileAlloc(allocator, file_path, 100 * 1024 * 1024) catch |err| {
        return FileVerifyResult{
            .valid = false,
            .entries_verified = 0,
            .first_invalid_seq = null,
            .last_hash = expected_prev_hash,
            .error_message = try std.fmt.allocPrint(allocator, "Cannot read file: {}", .{err}),
        };
    };
    defer allocator.free(file_contents);

    var entries_verified: u64 = 0;
    var current_prev_hash = expected_prev_hash;
    var last_hash = expected_prev_hash;

    var lines = std.mem.splitScalar(u8, file_contents, '\n');
    while (lines.next()) |line| {
        if (line.len == 0) continue;

        // Parse JSON to extract hash fields and seq
        const parse_result = parseEntryHashes(allocator, line) catch |err| {
            return FileVerifyResult{
                .valid = false,
                .entries_verified = entries_verified,
                .first_invalid_seq = null,
                .last_hash = last_hash,
                .error_message = try std.fmt.allocPrint(allocator, "JSON parse error: {}", .{err}),
            };
        };
        defer {
            if (parse_result.error_message) |msg| allocator.free(msg);
        }

        if (!parse_result.valid) {
            return FileVerifyResult{
                .valid = false,
                .entries_verified = entries_verified,
                .first_invalid_seq = parse_result.seq,
                .last_hash = last_hash,
                .error_message = parse_result.error_message,
            };
        }

        // Verify prev_hash chain
        if (!std.mem.eql(u8, &parse_result.prev_hash, &current_prev_hash)) {
            return FileVerifyResult{
                .valid = false,
                .entries_verified = entries_verified,
                .first_invalid_seq = parse_result.seq,
                .last_hash = last_hash,
                .error_message = try std.fmt.allocPrint(
                    allocator,
                    "Chain broken at seq {?d}: expected prev {s}, got {s}",
                    .{ parse_result.seq, current_prev_hash, parse_result.prev_hash },
                ),
            };
        }

        // Verify hash
        const computed_hash = try computeEntryHash(allocator, line);

        if (!std.mem.eql(u8, &computed_hash, &parse_result.hash)) {
            return FileVerifyResult{
                .valid = false,
                .entries_verified = entries_verified,
                .first_invalid_seq = parse_result.seq,
                .last_hash = last_hash,
                .error_message = try std.fmt.allocPrint(
                    allocator,
                    "Hash mismatch at seq {?d}: computed {s}, stored {s}",
                    .{ parse_result.seq, computed_hash, parse_result.hash },
                ),
            };
        }

        current_prev_hash = parse_result.hash;
        last_hash = parse_result.hash;
        entries_verified += 1;
    }

    return FileVerifyResult{
        .valid = true,
        .entries_verified = entries_verified,
        .first_invalid_seq = null,
        .last_hash = last_hash,
        .error_message = null,
    };
}

/// Result from parsing entry hashes
const ParseHashResult = struct {
    valid: bool,
    seq: ?u64,
    prev_hash: [64]u8,
    hash: [64]u8,
    error_message: ?[]const u8,
};

/// Parse entry to extract hash fields
fn parseEntryHashes(allocator: Allocator, json_line: []const u8) !ParseHashResult {
    const parsed = std.json.parseFromSlice(std.json.Value, allocator, json_line, .{}) catch |err| {
        return ParseHashResult{
            .valid = false,
            .seq = null,
            .prev_hash = undefined,
            .hash = undefined,
            .error_message = try std.fmt.allocPrint(allocator, "JSON parse failed: {}", .{err}),
        };
    };
    defer parsed.deinit();

    const root = parsed.value.object;

    // Get seq
    const seq: u64 = @intCast(root.get("seq").?.integer);

    // Get hashes
    const prev_hash_str = root.get("prev_hash").?.string;
    const hash_str = root.get("hash").?.string;

    if (prev_hash_str.len != 64 or hash_str.len != 64) {
        return ParseHashResult{
            .valid = false,
            .seq = seq,
            .prev_hash = undefined,
            .hash = undefined,
            .error_message = try std.fmt.allocPrint(allocator, "Invalid hash length", .{}),
        };
    }

    var prev_hash: [64]u8 = undefined;
    var hash: [64]u8 = undefined;
    @memcpy(&prev_hash, prev_hash_str[0..64]);
    @memcpy(&hash, hash_str[0..64]);

    return ParseHashResult{
        .valid = true,
        .seq = seq,
        .prev_hash = prev_hash,
        .hash = hash,
        .error_message = null,
    };
}

/// Compute hash for an entry (excluding the hash field)
fn computeEntryHash(allocator: Allocator, json_line: []const u8) ![64]u8 {
    // Parse, remove hash field, reserialize, hash
    const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_line, .{});
    defer parsed.deinit();

    // Build JSON without hash field
    var list: std.ArrayListUnmanaged(u8) = .empty;
    defer list.deinit(allocator);

    const writer = list.writer(allocator);
    try writer.writeByte('{');

    const root = parsed.value.object;
    var first = true;

    // Write fields in order, excluding "hash"
    const fields = [_][]const u8{ "seq", "ts", "tx_id", "ctx", "op", "table", "rowid", "sql", "before", "after", "prev_hash" };

    for (fields) |field_name| {
        if (root.get(field_name)) |value| {
            if (!first) try writer.writeByte(',');
            first = false;

            try writer.print("\"{s}\":", .{field_name});
            try writeJsonValue(writer, value);
        }
    }

    try writer.writeByte('}');

    // Compute SHA-256
    var hash: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(list.items, &hash, .{});

    return std.fmt.bytesToHex(hash, .lower);
}

/// Write a JSON value
fn writeJsonValue(writer: anytype, value: std.json.Value) !void {
    switch (value) {
        .null => try writer.writeAll("null"),
        .bool => |b| try writer.writeAll(if (b) "true" else "false"),
        .integer => |i| try writer.print("{d}", .{i}),
        .float => |f| try writer.print("{d}", .{f}),
        .string => |s| {
            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('"');
        },
        .array => |arr| {
            try writer.writeByte('[');
            for (arr.items, 0..) |item, i| {
                if (i > 0) try writer.writeByte(',');
                try writeJsonValue(writer, item);
            }
            try writer.writeByte(']');
        },
        .object => |obj| {
            try writer.writeByte('{');
            var iter = obj.iterator();
            var i: usize = 0;
            while (iter.next()) |entry| {
                if (i > 0) try writer.writeByte(',');
                try writer.print("\"{s}\":", .{entry.key_ptr.*});
                try writeJsonValue(writer, entry.value_ptr.*);
                i += 1;
            }
            try writer.writeByte('}');
        },
        .number_string => |s| try writer.writeAll(s),
    }
}

/// Quick integrity check (just checks chain continuity in index)
pub fn quickCheck(allocator: Allocator, log_dir: []const u8) !bool {
    const index_path = try std.fs.path.join(allocator, &.{ log_dir, "index.json" });
    defer allocator.free(index_path);

    const index_data = std.fs.cwd().readFileAlloc(allocator, index_path, 10 * 1024 * 1024) catch return false;
    defer allocator.free(index_data);

    var index = Index.load(allocator, index_data) catch return false;
    defer index.deinit();

    // Check file chain continuity in index
    var prev_hash: [64]u8 = [_]u8{'0'} ** 64;
    for (index.files.items) |*file| {
        if (!std.mem.eql(u8, &file.first_hash, &prev_hash)) {
            return false;
        }
        prev_hash = file.last_hash;
    }

    return true;
}

test "verify empty log" {
    const allocator = std.testing.allocator;

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

    // Create directory with just index
    std.fs.makeDirAbsolute(tmp_dir) catch {};

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

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

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

    const file = try std.fs.cwd().createFile(index_path, .{});
    defer file.close();
    try file.writeAll(json);

    // Verify
    var result = try verifyChain(allocator, tmp_dir);
    defer result.deinit(allocator);

    try std.testing.expect(result.valid);
    try std.testing.expectEqual(@as(u64, 0), result.entries_verified);
}

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

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

    std.fs.makeDirAbsolute(tmp_dir) catch {};

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

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

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

    const file = try std.fs.cwd().createFile(index_path, .{});
    defer file.close();
    try file.writeAll(json);

    const ok = try quickCheck(allocator, tmp_dir);
    try std.testing.expect(ok);
}
