//! Canonical Magogi `LogRecord` — Zig reference emitter (Wave 1.5).
//!
//! The Zig (`hrb-*`) half of decision D1 (all runtimes emit the one wire
//! shape). The Python source of truth is `mgf.common.observability.LogRecord`;
//! this is a hand-port pinned to the same golden fixture (vendored at
//! `testdata/log_record_fixture.jsonl`, regenerated by mgf-common's
//! `tools/gen_log_record_schema.py`). Emitted lines are **byte-identical** to
//! Python's `LogRecord.to_jsonl_line()`:
//!
//!   - keys in exactly the canonical emit order, then any `extra` (unknown)
//!     keys appended;
//!   - compact (no spaces), raw UTF-8, JSON string escaping matching Python's
//!     `json.dumps(ensure_ascii=false)` (`\"`, `\\`, `\b`, `\f`, `\n`, `\r`,
//!     `\t`, and `\u00xx` for other control bytes);
//!   - core keys always present; optional keys omitted when empty/null/zero.
//!
//! NOTE: mgf-common's CI has no Zig toolchain, so this file is compiled +
//! tested in **mgf-hrb** CI (which vendors this module + the fixture). The
//! conformance test below (`zig test log_record.zig`) asserts the emitter
//! reproduces every golden line.
//!
//! Structured `fields` / `labels` and forward-compat `extra` are passed as
//! pre-serialised compact-JSON fragments (`fields_json` / `labels_json` /
//! `extra_json`) — a Zig producer renders its own map once and hands the
//! fragment here, keeping this emitter allocation-light and byte-exact.

const std = @import("std");

pub const LOG_SCHEMA_VERSION = "1.0.0";

pub const LogRecord = struct {
    // core (always emitted)
    ts: []const u8,
    level: []const u8,
    level_no: i64,
    logger: []const u8,
    msg: []const u8,
    app: []const u8 = "",
    lang: []const u8 = "python",
    schema_version: []const u8 = LOG_SCHEMA_VERSION,

    // optional typed (omitted when null / 0)
    module: ?[]const u8 = null,
    func: ?[]const u8 = null,
    line: i64 = 0,
    thread: ?[]const u8 = null,
    process: i64 = 0,
    host: ?[]const u8 = null,
    app_version: ?[]const u8 = null,
    env: ?[]const u8 = null,
    trace_id: ?[]const u8 = null,
    span_id: ?[]const u8 = null,
    request_id: ?[]const u8 = null,
    exception: ?[]const u8 = null,
    stack: ?[]const u8 = null,

    // structured context + tags + forward-compat (pre-serialised fragments)
    fields_json: ?[]const u8 = null, // e.g. `{"k":1}`
    labels_json: ?[]const u8 = null, // e.g. `{"region":"eu"}`
    extra_json: ?[]const u8 = null, // inner pairs, e.g. `"severity_text":"notice"`
};

fn writeEscaped(w: anytype, s: []const u8) !void {
    for (s) |c| {
        switch (c) {
            '"' => try w.writeAll("\\\""),
            '\\' => try w.writeAll("\\\\"),
            0x08 => try w.writeAll("\\b"),
            0x0C => try w.writeAll("\\f"),
            '\n' => try w.writeAll("\\n"),
            '\r' => try w.writeAll("\\r"),
            '\t' => try w.writeAll("\\t"),
            else => {
                if (c < 0x20) {
                    try w.print("\\u{x:0>4}", .{c});
                } else {
                    try w.writeByte(c);
                }
            },
        }
    }
}

fn writeKey(w: anytype, first: *bool, key: []const u8) !void {
    if (!first.*) try w.writeByte(',');
    first.* = false;
    try w.writeByte('"');
    try w.writeAll(key); // keys are ASCII — no escaping needed
    try w.writeAll("\":");
}

fn strField(w: anytype, first: *bool, key: []const u8, val: []const u8) !void {
    try writeKey(w, first, key);
    try w.writeByte('"');
    try writeEscaped(w, val);
    try w.writeByte('"');
}

fn intField(w: anytype, first: *bool, key: []const u8, val: i64) !void {
    try writeKey(w, first, key);
    try w.print("{d}", .{val});
}

fn rawField(w: anytype, first: *bool, key: []const u8, raw: []const u8) !void {
    try writeKey(w, first, key);
    try w.writeAll(raw);
}

/// Write one canonical JSONL object (no trailing newline) to `w`.
pub fn writeJsonObject(rec: LogRecord, w: anytype) !void {
    var first = true;
    try w.writeByte('{');

    // core — always present, in order
    try strField(w, &first, "ts", rec.ts);
    try strField(w, &first, "level", rec.level);
    try intField(w, &first, "level_no", rec.level_no);
    try strField(w, &first, "logger", rec.logger);
    try strField(w, &first, "msg", rec.msg);
    try strField(w, &first, "app", rec.app);
    try strField(w, &first, "lang", rec.lang);
    try strField(w, &first, "schema_version", rec.schema_version);

    // optional typed — omit when empty/null/zero
    if (rec.module) |v| try strField(w, &first, "module", v);
    if (rec.func) |v| try strField(w, &first, "func", v);
    if (rec.line != 0) try intField(w, &first, "line", rec.line);
    if (rec.thread) |v| try strField(w, &first, "thread", v);
    if (rec.process != 0) try intField(w, &first, "process", rec.process);
    if (rec.host) |v| try strField(w, &first, "host", v);
    if (rec.app_version) |v| try strField(w, &first, "app_version", v);
    if (rec.env) |v| try strField(w, &first, "env", v);
    if (rec.trace_id) |v| try strField(w, &first, "trace_id", v);
    if (rec.span_id) |v| try strField(w, &first, "span_id", v);
    if (rec.request_id) |v| try strField(w, &first, "request_id", v);
    if (rec.exception) |v| try strField(w, &first, "exception", v);
    if (rec.stack) |v| try strField(w, &first, "stack", v);

    // structured maps
    if (rec.fields_json) |v| try rawField(w, &first, "fields", v);
    if (rec.labels_json) |v| try rawField(w, &first, "labels", v);

    // forward-compat: unknown keys appended verbatim
    if (rec.extra_json) |v| {
        if (!first) try w.writeByte(',');
        first = false;
        try w.writeAll(v);
    }

    try w.writeByte('}');
}

/// Render one JSONL line (object + trailing newline). Caller owns the memory.
pub fn toJsonlAlloc(allocator: std.mem.Allocator, rec: LogRecord) ![]u8 {
    var aw: std.Io.Writer.Allocating = .init(allocator);
    errdefer aw.deinit();
    try writeJsonObject(rec, &aw.writer);
    try aw.writer.writeByte('\n');
    return aw.toOwnedSlice();
}

// --------------------------------------------------------------------------- //
// Conformance test — emitter reproduces the canonical golden, byte-for-byte.
// --------------------------------------------------------------------------- //

const fixture = @embedFile("testdata/log_record_fixture.jsonl");

test "emitter reproduces every golden fixture line" {
    const recs = [_]LogRecord{
        // 1. minimal
        .{
            .ts = "2026-05-28T00:00:00+00:00",
            .level = "INFO",
            .level_no = 20,
            .logger = "app",
            .msg = "service started",
            .app = "demo",
            .lang = "python",
        },
        // 2. full
        .{
            .ts = "2026-05-28T00:00:01.500000+00:00",
            .level = "WARNING",
            .level_no = 30,
            .logger = "app.db",
            .msg = "slow query",
            .app = "demo",
            .lang = "python",
            .module = "pool",
            .func = "acquire",
            .line = 87,
            .thread = "MainThread",
            .process = 4321,
            .host = "host-1",
            .app_version = "1.2.3",
            .env = "staging",
            .trace_id = "0af7651916cd43dd8448eb211c80319c",
            .span_id = "b7ad6b7169203331",
            .request_id = "req-42",
            .fields_json = "{\"duration_ms\":1234,\"rows\":0,\"cached\":false}",
            .labels_json = "{\"region\":\"eu-west-1\",\"tier\":\"db\"}",
        },
        // 3. error (exception + stack)
        .{
            .ts = "2026-05-28T00:00:02+00:00",
            .level = "ERROR",
            .level_no = 40,
            .logger = "app.http",
            .msg = "request failed",
            .app = "demo",
            .lang = "python",
            .host = "host-1",
            .app_version = "1.2.3",
            .exception = "Traceback (most recent call last):\n  ...\nValueError: boom",
            .stack = "  File \"app/http.py\", line 12, in handle\n    raise ValueError",
        },
        // 4. zig runtime
        .{
            .ts = "2026-05-28T00:00:03+00:00",
            .level = "INFO",
            .level_no = 20,
            .logger = "hrb.kabi",
            .msg = "device attached",
            .app = "hrb-daemon",
            .lang = "zig",
            .host = "host-1",
            .app_version = "0.4.0",
            .fields_json = "{\"vid\":1133,\"pid\":49374}",
        },
        // 5. ts runtime
        .{
            .ts = "2026-05-28T00:00:04+00:00",
            .level = "DEBUG",
            .level_no = 10,
            .logger = "web.ssr",
            .msg = "render route",
            .app = "magogi-website",
            .lang = "ts",
            .host = "host-1",
            .app_version = "2026.5.0",
            .request_id = "req-99",
            .fields_json = "{\"route\":\"/dashboard\",\"ttfb_ms\":12}",
        },
        // 6. forward-compat (unknown key via extra)
        .{
            .ts = "2026-05-28T00:00:05+00:00",
            .level = "INFO",
            .level_no = 20,
            .logger = "app",
            .msg = "future field carried through",
            .app = "demo",
            .lang = "python",
            .extra_json = "\"severity_text\":\"notice\"",
        },
    };

    var it = std.mem.splitScalar(u8, fixture, '\n');
    for (recs) |rec| {
        const expected = it.next() orelse return error.FixtureTooShort;
        var obuf: [4096]u8 = undefined;
        var w = std.Io.Writer.fixed(&obuf);
        try writeJsonObject(rec, &w);
        try std.testing.expectEqualStrings(expected, w.buffered());
    }
}
