//! The tagged kernel log ring — a circular byte buffer of framed records, each
//! stamped by the writer (the kernel) with the sender's pid, task name, level,
//! per-boot sequence number, and monotonic timestamp. Pure code over an
//! embedded buffer — no architecture or lock imports — so it host-tests
//! alongside the other pure kernel pieces (`zig build test`).
//!
//! `head` and `tail` are free-running u64 positions in a logical byte stream;
//! the physical wrap is invisible to readers (all copies are modulo the
//! buffer), so a record never splits logically and no padding records exist.
//! Reclaim happens record by record: the writer parses the header at `tail`
//! (which it wrote itself) and advances until the new record fits — `tail`
//! always sits on a record boundary, and sequence-number gaps tell a reader
//! exactly how many records it lost.
//!
//! Locking is the caller's job (log.zig holds its log lock around every call);
//! the ring itself is single-writer, snapshot-reader.

const std = @import("std");
const abi = @import("abi");

pub fn Ring(comptime capacity: usize) type {
    comptime std.debug.assert(std.math.isPowerOfTwo(capacity));
    return struct {
        const Self = @This();

        buffer: [capacity]u8 = undefined,
        head: u64 = 0,
        tail: u64 = 0,
        next_sequence: u64 = 0,

        /// Append one record; returns its sequence number. `name` and `message`
        /// are clamped to their ABI caps (the syscall clamps earlier too — the
        /// clamp here makes the ring safe in isolation).
        pub fn append(
            self: *Self,
            pid: u32,
            name: []const u8,
            level: abi.KlogLevel,
            timestamp_ns: u64,
            message: []const u8,
            truncated: bool,
        ) u64 {
            const name_len: usize = @min(name.len, abi.maximum_process_name);
            const message_len: usize = @min(message.len, abi.klog_maximum_message);
            const record_len = recordLength(name_len, message_len);

            // Reclaim whole records until the new one fits.
            while (self.head + record_len - self.tail > capacity) self.reclaimOne();

            const sequence = self.next_sequence;
            self.next_sequence += 1;

            const header = abi.KlogRecordHeader{
                .magic = abi.klog_record_magic,
                .level = level,
                .name_len = @intCast(name_len),
                .pid = pid,
                .sequence = sequence,
                .timestamp_ns = timestamp_ns,
                .message_len = @intCast(message_len),
                .flags = if (truncated) abi.klog_flag_truncated else 0,
                ._reserved = @splat(0),
            };
            self.put(self.head, std.mem.asBytes(&header));
            self.put(self.head + abi.klog_record_header_size, name[0..name_len]);
            self.put(self.head + abi.klog_record_header_size + name_len, message[0..message_len]);
            // The alignment pad is dead space; zero it so raw dumps stay tidy.
            var pad = abi.klog_record_header_size + name_len + message_len;
            while (pad < record_len) : (pad += 1)
                self.buffer[@intCast((self.head + pad) % capacity)] = 0;
            self.head += record_len;
            return sequence;
        }

        /// Copy stream bytes beginning at `offset` into `out`. Returns null if
        /// `offset` fell behind `tail` (overwritten) or lies past `head` — the
        /// reader re-syncs from status(). 0 bytes means caught up.
        pub fn read(self: *const Self, offset: u64, out: []u8) ?usize {
            if (offset < self.tail or offset > self.head) return null;
            const n: usize = @intCast(@min(out.len, self.head - offset));
            self.get(offset, out[0..n]);
            return n;
        }

        /// Cursors for klog_status. boot_unix_seconds is the kernel wrapper's
        /// to fill — the ring knows nothing of wall clocks.
        pub fn status(self: *const Self) abi.KlogStatus {
            return .{
                .tail = self.tail,
                .head = self.head,
                .next_sequence = self.next_sequence,
                .boot_unix_seconds = 0,
            };
        }

        fn reclaimOne(self: *Self) void {
            var header_bytes: [abi.klog_record_header_size]u8 = undefined;
            self.get(self.tail, &header_bytes);
            const header = std.mem.bytesToValue(abi.KlogRecordHeader, &header_bytes);
            // The writer wrote this header itself: the assert guards against
            // memory corruption, not bad input.
            std.debug.assert(header.magic == abi.klog_record_magic);
            self.tail += recordLength(header.name_len, header.message_len);
        }

        fn recordLength(name_len: usize, message_len: usize) usize {
            return std.mem.alignForward(usize, abi.klog_record_header_size + name_len + message_len, abi.klog_record_alignment);
        }

        // Byte-at-a-time modulo copies keep the wrap logic obviously correct;
        // if they ever show in a profile, split into two @memcpy spans.
        fn put(self: *Self, offset: u64, bytes: []const u8) void {
            for (bytes, 0..) |b, i| self.buffer[@intCast((offset + i) % capacity)] = b;
        }

        fn get(self: *const Self, offset: u64, out: []u8) void {
            for (out, 0..) |*b, i| b.* = self.buffer[@intCast((offset + i) % capacity)];
        }
    };
}

// --- tests (host) -----------------------------------------------------------

const TestRing = Ring(4096);

/// Parse the record at `offset` out of `ring`, returning the header plus name
/// and message copies — the same walk a userspace drainer performs.
const Parsed = struct {
    header: abi.KlogRecordHeader,
    name: [abi.maximum_process_name]u8 = undefined,
    message: [abi.klog_maximum_message]u8 = undefined,

    fn nameSlice(self: *const Parsed) []const u8 {
        return self.name[0..self.header.name_len];
    }
    fn messageSlice(self: *const Parsed) []const u8 {
        return self.message[0..self.header.message_len];
    }
    fn next(self: *const Parsed, offset: u64) u64 {
        return offset + std.mem.alignForward(usize, abi.klog_record_header_size + self.header.name_len + self.header.message_len, abi.klog_record_alignment);
    }
};

fn parseAt(ring: *const TestRing, offset: u64) Parsed {
    var p: Parsed = undefined;
    var header_bytes: [abi.klog_record_header_size]u8 = undefined;
    std.debug.assert(ring.read(offset, &header_bytes).? == header_bytes.len);
    p.header = std.mem.bytesToValue(abi.KlogRecordHeader, &header_bytes);
    std.debug.assert(p.header.magic == abi.klog_record_magic);
    _ = ring.read(offset + abi.klog_record_header_size, p.name[0..p.header.name_len]);
    _ = ring.read(offset + abi.klog_record_header_size + p.header.name_len, p.message[0..p.header.message_len]);
    return p;
}

test "header size is pinned" {
    try std.testing.expectEqual(abi.klog_record_header_size, @sizeOf(abi.KlogRecordHeader));
}

test "append/read round trip" {
    var ring = std.testing.allocator.create(TestRing) catch unreachable;
    defer std.testing.allocator.destroy(ring);
    ring.* = .{};

    _ = ring.append(7, "/system/services/fat", .info, 123, "mounted /mnt/usb", false);
    _ = ring.append(0, "kernel", .raw, 456, "wall clock online", false);

    const first = parseAt(ring, ring.tail);
    try std.testing.expectEqual(@as(u32, 7), first.header.pid);
    try std.testing.expectEqual(abi.KlogLevel.info, first.header.level);
    try std.testing.expectEqual(@as(u64, 123), first.header.timestamp_ns);
    try std.testing.expectEqualStrings("/system/services/fat", first.nameSlice());
    try std.testing.expectEqualStrings("mounted /mnt/usb", first.messageSlice());

    const second = parseAt(ring, first.next(ring.tail));
    try std.testing.expectEqual(@as(u32, 0), second.header.pid);
    try std.testing.expectEqualStrings("kernel", second.nameSlice());
    try std.testing.expectEqual(@as(u64, 1), second.header.sequence);
}

test "wrap reclaims whole records and keeps tail on a boundary" {
    var ring = std.testing.allocator.create(TestRing) catch unreachable;
    defer std.testing.allocator.destroy(ring);
    ring.* = .{};

    // Fill far past capacity so the ring wraps many times.
    var i: u32 = 0;
    while (i < 200) : (i += 1) {
        var message: [64]u8 = undefined;
        const m = std.fmt.bufPrint(&message, "line {d} padding padding padding", .{i}) catch unreachable;
        _ = ring.append(1, "/system/tests/writer", .info, i, m, false);
    }
    try std.testing.expect(ring.head - ring.tail <= 4096);

    // The record at tail parses cleanly (boundary held), and walking to head
    // yields consecutive sequence numbers.
    var offset = ring.tail;
    var previous: ?u64 = null;
    while (offset < ring.head) {
        const p = parseAt(ring, offset);
        if (previous) |q| try std.testing.expectEqual(q + 1, p.header.sequence);
        previous = p.header.sequence;
        offset = p.next(offset);
    }
    try std.testing.expectEqual(ring.head, offset);
    // Records were lost (sequence at tail > 0), and the count is the gap.
    try std.testing.expect(parseAt(ring, ring.tail).header.sequence > 0);
}

test "stale offset returns null; head offset reads zero bytes" {
    var ring = std.testing.allocator.create(TestRing) catch unreachable;
    defer std.testing.allocator.destroy(ring);
    ring.* = .{};

    var i: u32 = 0;
    while (i < 300) : (i += 1)
        _ = ring.append(1, "w", .info, i, "0123456789abcdef0123456789abcdef", false);

    var out: [16]u8 = undefined;
    try std.testing.expect(ring.read(0, &out) == null); // long overwritten
    try std.testing.expect(ring.read(ring.head + 1, &out) == null); // past the end
    try std.testing.expectEqual(@as(usize, 0), ring.read(ring.head, &out).?); // caught up
}

test "truncation flag and clamping" {
    var ring = std.testing.allocator.create(TestRing) catch unreachable;
    defer std.testing.allocator.destroy(ring);
    ring.* = .{};

    const long = "x" ** 300; // past klog_maximum_message
    _ = ring.append(2, "w", .warn, 0, long, true);
    const p = parseAt(ring, ring.tail);
    try std.testing.expectEqual(@as(u16, abi.klog_maximum_message), p.header.message_len);
    try std.testing.expect(p.header.flags & abi.klog_flag_truncated != 0);
}
