//! mgf-hrb-audit — Part 13 §5.6.1 broker self-awareness ring.
//!
//! Every wire op the broker handles records one `AuditRecord` into
//! a fixed-size ring (default 4096 entries). The ring is purely
//! in-memory + process-local — no I/O on the hot path, no
//! dependency on `mgf.common.observability`. Consumers query it
//! via the `audit_tail(n)` wire op (§5.6.4) for retrospective
//! diagnostic queries.
//!
//! ## Why a module singleton
//!
//! Same reasoning as `subscription.zig`: the ring is broker-wide
//! state, every wire op needs to push into it, and `server.handle`
//! is where the push happens. Threading a `*AuditRing` through
//! every formatter would be a big diff for no value. The ring has
//! its own SpinMutex; the push path is short (no I/O, no
//! allocator) so contention is bounded.
//!
//! ## Memory layout
//!
//! Each record is fixed-size — bounded strings (op name, urn,
//! err code, correlation id) live in inline buffers so the ring
//! is one flat allocation. At 4096 records × ~400 bytes ≈ 1.6 MB
//! per broker — within the budget but heftier than the spec's
//! ~80 bytes estimate. The savings would come from interning op
//! names (closed set of ~15 strings) and arena-allocating URNs;
//! deferred until pressure surfaces.
//!
//! ## Outcomes
//!
//! `Outcome` is a closed set of 4 values:
//!   * `ok` — response carried `"ok":true`
//!   * `err` — response carried `"ok":false`
//!   * `timeout` — reserved (the request-parse path raises BadRequest
//!     for synthetic timeouts today; once true timeout-propagation
//!     lands, this surfaces those)
//!   * `conflict` — reserved (claim-lease conflicts today surface
//!     as `err`; future tightening separates them)

const std = @import("std");
const kabi = @import("mgf_hrb_kabi");

pub const MAX_RING: usize = 4096;
pub const MAX_OP_NAME: usize = 32;
pub const MAX_URN_BYTES: usize = 128;
pub const MAX_ERR_CODE: usize = 32;
pub const MAX_CORRELATION_ID: usize = 128;

pub const Outcome = enum {
    ok,
    err,
    timeout,
    conflict,

    pub fn name(self: Outcome) []const u8 {
        return @tagName(self);
    }
};

pub const AuditRecord = struct {
    /// Monotonic per broker; ++ on every record (NOT on overflow —
    /// overflow rotates the head; seq keeps growing). Consumers can
    /// detect gaps by comparing successive seqs.
    seq: u64,
    /// Broker-monotonic ms (matches `kabi.nowMs`).
    ts_ms: u64,
    /// Wall-clock from request-parse to response-emit, in
    /// microseconds. Capped at u32 (≈71 minutes); anything longer
    /// is a different bug.
    duration_us: u32,
    /// The peer's SO_PEERCRED uid + pid at accept-time.
    peer_uid: u32,
    peer_pid: u32,
    /// The capability id the wire request carried (claim / raw /
    /// control extract it from the body; other ops don't). 0
    /// means "no cap presented for this op".
    cap_id: u64,
    /// The op name as a bounded inline buffer.
    op_buf: [MAX_OP_NAME]u8 = undefined,
    op_len: u8 = 0,
    /// The URN, when the op carries one. `has_urn=false` for ops
    /// without (capabilities, state_snapshot, audit_tail).
    has_urn: bool = false,
    urn_buf: [MAX_URN_BYTES]u8 = undefined,
    urn_len: u8 = 0,
    /// Closed outcome enum.
    outcome: Outcome,
    /// Error code as a string (e.g. "NotFound", "BadRequest"); only
    /// meaningful when outcome != .ok.
    has_err_code: bool = false,
    err_code_buf: [MAX_ERR_CODE]u8 = undefined,
    err_code_len: u8 = 0,
    /// Correlation id the caller passed in (Part 13 §5.6.3 / §7.1).
    /// Echoes into the audit record so a multi-op trace can be
    /// reconstructed by consumers.
    has_correlation_id: bool = false,
    correlation_id_buf: [MAX_CORRELATION_ID]u8 = undefined,
    correlation_id_len: u8 = 0,

    pub fn op(self: *const AuditRecord) []const u8 {
        return self.op_buf[0..self.op_len];
    }
    pub fn urn(self: *const AuditRecord) ?[]const u8 {
        return if (self.has_urn) self.urn_buf[0..self.urn_len] else null;
    }
    pub fn errCode(self: *const AuditRecord) ?[]const u8 {
        return if (self.has_err_code) self.err_code_buf[0..self.err_code_len] else null;
    }
    pub fn correlationId(self: *const AuditRecord) ?[]const u8 {
        return if (self.has_correlation_id) self.correlation_id_buf[0..self.correlation_id_len] else null;
    }
};

/// The module-level singleton ring + its own lock. The lock is
/// held briefly (one record copy in / out); contention is bounded
/// by the wire op rate.
const Ring = struct {
    lock: kabi.SpinMutex = .{},
    items: [MAX_RING]AuditRecord = undefined,
    head: usize = 0, // index of oldest
    len: usize = 0,
    next_seq: u64 = 1,
    /// Process-start timestamp (ms). Captured at `init()`; used by
    /// `state_snapshot` to compute uptime_ms. 0 means uninitialised.
    start_ms: u64 = 0,
    /// Tracks `ops_total` independently of the ring (the ring's
    /// `next_seq - 1` would suffice, but a separate field keeps
    /// state_snapshot's read decoupled).
    ops_total: u64 = 0,
    /// Active client connections (incremented at server accept,
    /// decremented at handleClient teardown). Read by state_snapshot.
    active_connections: std.atomic.Value(u32) = .init(0),
    /// Lifetime count of records evicted by overflow (B3 — surface
    /// audit loss honestly). Without this counter, a busy broker
    /// quietly rewrites the oldest 4096 records' worth of security
    /// history every time `MAX_RING` is exceeded. Surfaced via
    /// `state_snapshot.audit_dropped_total`; a non-zero delta
    /// between snapshots tells operators the ring is too small for
    /// the current op rate.
    dropped_total: u64 = 0,
};

var ring: Ring = .{};

/// Initialise the ring. Idempotent — calling twice updates start_ms
/// only on the first call (it's a one-shot timestamp).
pub fn init() void {
    ring.lock.lock();
    defer ring.lock.unlock();
    if (ring.start_ms == 0) ring.start_ms = kabi.nowMs();
}

/// Builder for a single record. The server constructs one, fills
/// what it knows pre-dispatch + post-dispatch, then calls `push`
/// to consume the builder into the ring.
pub const Builder = struct {
    rec: AuditRecord,
    start_ms: u64,

    pub fn start(op_name: []const u8, peer_uid: u32, peer_pid: u32) Builder {
        // N4 (review wave 3) — capture nowMs ONCE per builder.
        //
        // Pre-N4 this called nowMs() twice (once for ts_ms, once for
        // start_ms) microseconds apart, then push() called it a third
        // time for duration_us. Effect: ts_ms ≠ start_ms by a few µs,
        // and `ts_ms + duration_us` did not equal "response emit time"
        // (it referenced three different clock samples). Audit
        // analyses that try to correlate "ops in the same ms" got
        // small but systematic skew.
        //
        // Single capture aligns ts_ms = start_ms = "op start time"
        // exactly, and push() then computes duration_us against THE
        // SAME start instant.
        const now = kabi.nowMs();
        var b = Builder{
            .rec = .{
                .seq = 0,
                .ts_ms = now,
                .duration_us = 0,
                .peer_uid = peer_uid,
                .peer_pid = peer_pid,
                .cap_id = 0,
                .outcome = .ok,
            },
            .start_ms = now,
        };
        const n = @min(op_name.len, MAX_OP_NAME);
        @memcpy(b.rec.op_buf[0..n], op_name[0..n]);
        b.rec.op_len = @intCast(n);
        return b;
    }

    pub fn setUrn(self: *Builder, urn: []const u8) void {
        const n = @min(urn.len, MAX_URN_BYTES);
        @memcpy(self.rec.urn_buf[0..n], urn[0..n]);
        self.rec.urn_len = @intCast(n);
        self.rec.has_urn = true;
    }

    pub fn setCapId(self: *Builder, cap_id: u64) void {
        self.rec.cap_id = cap_id;
    }

    pub fn setCorrelationId(self: *Builder, cid: []const u8) void {
        if (cid.len == 0) return;
        const n = @min(cid.len, MAX_CORRELATION_ID);
        @memcpy(self.rec.correlation_id_buf[0..n], cid[0..n]);
        self.rec.correlation_id_len = @intCast(n);
        self.rec.has_correlation_id = true;
    }

    pub fn setOutcome(self: *Builder, outcome: Outcome) void {
        self.rec.outcome = outcome;
    }

    pub fn setErrCode(self: *Builder, code: []const u8) void {
        const n = @min(code.len, MAX_ERR_CODE);
        @memcpy(self.rec.err_code_buf[0..n], code[0..n]);
        self.rec.err_code_len = @intCast(n);
        self.rec.has_err_code = true;
    }
};

/// Push a completed record. Computes duration_us + assigns seq;
/// overflow is silent (head advances; seq keeps growing).
pub fn push(b: *Builder) void {
    const now = kabi.nowMs();
    const dur_ms = if (now > b.start_ms) now - b.start_ms else 0;
    // ms → us; saturate at u32 max (~71 min).
    const dur_us: u32 = if (dur_ms > std.math.maxInt(u32) / 1000)
        std.math.maxInt(u32)
    else
        @intCast(dur_ms * 1000);
    b.rec.duration_us = dur_us;

    ring.lock.lock();
    defer ring.lock.unlock();
    b.rec.seq = ring.next_seq;
    ring.next_seq += 1;
    ring.ops_total += 1;

    if (ring.len < MAX_RING) {
        const idx = (ring.head + ring.len) % MAX_RING;
        ring.items[idx] = b.rec;
        ring.len += 1;
    } else {
        // Overflow: drop oldest by advancing head; new at the tail.
        // Bump `dropped_total` so state_snapshot can surface the loss
        // — silent rewrite would erase security records (B3).
        const idx = ring.head;
        ring.head = (ring.head + 1) % MAX_RING;
        ring.items[idx] = b.rec;
        ring.dropped_total += 1;
    }
}

/// Snapshot up to `n` most-recent records into `out` (newest at
/// index 0, oldest at the end). Returns the number written.
pub fn snapshot(n: usize, out: []AuditRecord) usize {
    ring.lock.lock();
    defer ring.lock.unlock();
    const count = @min(n, @min(out.len, ring.len));
    var i: usize = 0;
    while (i < count) : (i += 1) {
        // ring layout: oldest at ring.head, walk forward len items.
        // We want NEWEST first: pick index (head + len - 1 - i).
        const idx = (ring.head + ring.len - 1 - i) % MAX_RING;
        out[i] = ring.items[idx];
    }
    return count;
}

/// Snapshot the broker-wide counters for `state_snapshot` (§5.6.2).
pub const Counters = struct {
    uptime_ms: u64,
    ops_total: u64,
    ring_used: u32,
    ring_capacity: u32,
    active_connections: u32,
    /// Lifetime count of records evicted by overflow. A non-zero
    /// delta between two snapshots tells the operator the ring is
    /// too small for the current op rate — audit history is being
    /// silently overwritten and `audit_tail` is losing detail.
    dropped_total: u64,
};

pub fn counters() Counters {
    ring.lock.lock();
    const used: u32 = @intCast(ring.len);
    const ops_total = ring.ops_total;
    const dropped_total = ring.dropped_total;
    const start = ring.start_ms;
    ring.lock.unlock();
    const now = kabi.nowMs();
    return .{
        .uptime_ms = if (now > start) now - start else 0,
        .ops_total = ops_total,
        .ring_used = used,
        .ring_capacity = MAX_RING,
        .active_connections = ring.active_connections.load(.seq_cst),
        .dropped_total = dropped_total,
    };
}

/// Count records whose `ts_ms` falls within the last `window_ms`
/// milliseconds (i.e. ts_ms >= now - window_ms). Used by
/// `state_snapshot` to compute `ops_per_second_1m`. Linear in
/// ring size (4096); cheap because state_snapshot is rare.
///
/// When the ring has fewer than 1 minute of data (e.g. fresh
/// broker), the function returns whatever it can — the consumer
/// can compare against `uptime_ms` to gauge how representative
/// the rate is.
pub fn opsInLastMs(window_ms: u64) u32 {
    const now = kabi.nowMs();
    const cutoff = if (now > window_ms) now - window_ms else 0;
    ring.lock.lock();
    defer ring.lock.unlock();
    var count: u32 = 0;
    var i: usize = 0;
    while (i < ring.len) : (i += 1) {
        const idx = (ring.head + i) % MAX_RING;
        if (ring.items[idx].ts_ms >= cutoff) count += 1;
    }
    return count;
}

pub fn incActiveConnections() void {
    _ = ring.active_connections.fetchAdd(1, .seq_cst);
}

pub fn decActiveConnections() void {
    _ = ring.active_connections.fetchSub(1, .seq_cst);
}

/// Test-only: reset the ring + counters. Production code never
/// calls this; tests use it to start with a known-empty state.
///
/// N3 (review wave 3) — re-arm `start_ms` to the current time.
///
/// Pre-N3 this zeroed `start_ms`, leaving the ring "uninitialized"
/// from `counters()`'s perspective. The next `counters()` call
/// then computed `uptime_ms = now - 0`, returning the broker's
/// monotonic-clock value (effectively "uptime since the epoch") —
/// garbage data into any test or dashboard that polled
/// state_snapshot post-reset. Tests calling `reset() + push() +
/// counters()` got nonsense uptime unless they also explicitly
/// called `init()` afterwards (which they typically don't —
/// reset is the test-isolation barrier).
///
/// Fix: re-arm `start_ms` to `nowMs()` so the post-reset broker
/// state is "fresh broker, just started" — the semantic tests
/// actually want.
pub fn reset() void {
    ring.lock.lock();
    defer ring.lock.unlock();
    ring.head = 0;
    ring.len = 0;
    ring.next_seq = 1;
    ring.ops_total = 0;
    ring.dropped_total = 0;
    ring.start_ms = kabi.nowMs();
    ring.active_connections.store(0, .seq_cst);
}
