//! mgf-hrb `dispatch` (wire layer) — request orchestration, extracted from
//! `server.zig` (1A-iii/iv of the SOLID redesign). This is the
//! parse → rate-limit → dispatch → format → envelope pipeline:
//!
//!   `handle(facade, peer, peer_key, line, out)` is the single entry the
//!   transport shell calls per request line. It runs the trust gate
//!   (`request.parse`), charges the per-tenant rate limit, routes the
//!   typed `Request` to the right `format.fmt*`, then splices the
//!   envelope fields (fidelity on read-only success, correlation echo)
//!   and pushes the audit record.
//!
//! `server.zig` is left as pure transport + assembly: accept loop,
//! per-connection recv/slowloris/fd-pass, and startup wiring. No socket
//! or accept logic lives here; no parse/dispatch/format logic lives there.
//!
//! NOTE ON THE "DISPATCH TABLE" (plan 1A-iii): the plan considered
//! replacing the hand-written `dispatch` switch + `isReadOnlyOp` with an
//! `Op → {handler, read_only}` table to guard against forgetting to wire a
//! new op. On inspection that risk is ALREADY compile-enforced — Zig's
//! switch over the `request.Op` tagged union is exhaustive, so adding an
//! `Op` variant fails the build until every switch handles it (verified
//! when `attenuate`/`recover` were added). A table would only colocate the
//! read-only flag with the handler (minor) at the cost of ~20 uniform
//! adapter wrappers for heterogeneous formatter signatures. Not high-value
//! given the exhaustiveness guarantee; the explicit switch stays.

const std = @import("std");
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const cap = @import("mgf_hrb_cap");
const facademod = @import("mgf_hrb_facade");
const request = @import("mgf_hrb_request");
const audit = @import("mgf_hrb_audit");
const admission = @import("mgf_hrb_admission");
const format = @import("mgf_hrb_format");
const shm = @import("mgf_hrb_shm");
const ring = @import("mgf_hrb_ring");

const Urn = model.Urn;
const Err = model.Err;

/// A dispatched response: the JSON line + optionally an fd that should be
/// SCM_RIGHTS-passed alongside it. When `fd_to_send >= 0`, the transport
/// shell sends the fd after writing the response, then closes its dup. The
/// kernel duplicates the fd into the client's table — the client's copy
/// outlives ours (the survivability dividend, Part 7.4).
pub const DispatchResult = struct {
    resp: []const u8,
    fd_to_send: i32 = -1,
};

pub fn handle(
    facade: *facademod.Facade,
    peer: cap.PeerCred,
    peer_key: u64,
    line: []const u8,
    out: []u8,
) DispatchResult {
    const parsed = request.parse(line, std.heap.page_allocator) catch |e| {
        // Part 13 §5.6.1 — even parse failures land in the audit
        // ring so consumers diagnosing "why are my requests
        // rejecting" can correlate. Op name unknown at this point;
        // record as "(parse_error)".
        var b = audit.Builder.start("(parse_error)", peer.uid, @intCast(peer.pid));
        b.setOutcome(.err);
        b.setErrCode(@errorName(e));
        audit.push(&b);
        return .{ .resp = format.fmtErr(out, format.errToWireCode(e)) };
    };
    // R-T1-9D — per-tenant rate limit. Charged AFTER the (cheap) parse so
    // the audit record carries the real op name, and BEFORE dispatch so a
    // shed op skips the expensive backend work. Over-rate → Err.Overloaded
    // (back-pressure; maps to OverloadedError in the SDK).
    if (!admission.rlAllow(peer.uid, kabi.nowMs())) {
        var b = audit.Builder.start(@tagName(parsed.req), peer.uid, @intCast(peer.pid));
        b.setOutcome(.err);
        b.setErrCode("Overloaded");
        audit.push(&b);
        return .{ .resp = format.fmtErr(out, "Overloaded") };
    }
    var result = dispatch(facade, peer, peer_key, parsed.req, line, out);
    // Part 13 §7.2 — top-level `fidelity` field on read-only success
    // responses. Spliced FIRST (so correlation_id, when present, lands
    // last for trace-readability). For v1 every read-only response is
    // `"live"` because the broker has no caching layer yet; a future
    // pass overrides per-op when meaningful (e.g. `dump_all` returning
    // `"cached"` when some URNs fell back). Write ops + error responses
    // do not carry it: an error means we couldn't compute anything to
    // tag a fidelity for; a write's outcome isn't a fidelity question.
    if (isReadOnlyOp(parsed.req) and isOkResponse(result.resp)) {
        // Skip the auto-splice when the formatter already emitted a
        // TOP-LEVEL `"fidelity":...` — e.g. `driver_info` honestly
        // reports `"inferred"` because the curated-registry half
        // ships in a later sub-phase (3B).
        //
        // We check only the trailing tail of the response (last 64
        // bytes) so the check sees the response's outermost-level
        // fidelity field, not a per-URN one nested inside
        // `resource` / `items` (which the existing `inspect` /
        // `dump_all` formatters emit). Top-level fidelity is always
        // the last field at the top before the closing `}\n`, so
        // a tail match is conservative + correct.
        const tail_start = if (result.resp.len > 64) result.resp.len - 64 else 0;
        const tail = result.resp[tail_start..];
        if (std.mem.indexOf(u8, tail, "\"fidelity\":\"") == null) {
            result.resp = spliceField(out, result.resp, "fidelity", "live");
        }
    }

    // Part 13 §5.6.3 / §7.1 — correlation_id echo. When the request
    // carried one, splice `,"correlation_id":"<id>"` into the response
    // just before the closing `}`. Charset was enforced at the gate
    // (alphanumeric + -_.) so no JSON escaping is needed on emit.
    // Echoes on BOTH success and error responses — every response shape
    // ends with `}\n` and the splice handles them uniformly.
    if (parsed.correlation_id_len > 0) {
        result.resp = spliceField(
            out,
            result.resp,
            "correlation_id",
            parsed.correlationId(),
        );
    }

    // Part 13 §5.6.1 — push the audit record AFTER all envelope
    // splicing so the recorded duration covers everything that
    // affects the wire response.
    pushAuditRecord(peer, &parsed, &result.resp);

    return result;
}

/// Build the audit record for a successfully-parsed wire op. The
/// op name + URN extraction is a small switch over the typed
/// Request union — no string parsing, no allocation.
fn pushAuditRecord(
    peer: cap.PeerCred,
    parsed: *const request.Parsed,
    resp: *const []const u8,
) void {
    const op_name = @tagName(@as(request.Op, parsed.req));
    var b = audit.Builder.start(op_name, peer.uid, @intCast(peer.pid));
    // Per-op URN + cap_id extraction. Ops without a URN simply
    // skip setUrn (audit record carries has_urn=false).
    switch (parsed.req) {
        .claim => |r| {
            b.setUrn(r.urn.slice());
            b.setCapId(r.cap_id);
        },
        .release => |r| b.setUrn(r.urn.slice()),
        .raw => |r| {
            b.setUrn(r.urn.slice());
            b.setCapId(r.cap_id);
        },
        .inspect => |r| b.setUrn(r.urn.slice()),
        .control => |r| {
            b.setUrn(r.urn.slice());
            b.setCapId(r.cap_id);
        },
        .inspect_full => |r| b.setUrn(r.urn.slice()),
        .dump_chain => |r| b.setUrn(r.urn.slice()),
        .revoke => |r| {
            b.setUrn(r.urn.slice());
            b.setCapId(r.cap_id);
        },
        else => {},
    }
    if (parsed.correlation_id_len > 0) {
        b.setCorrelationId(parsed.correlationId());
    }
    // Outcome / error code from the response's leading bytes.
    if (isOkResponse(resp.*)) {
        b.setOutcome(.ok);
    } else {
        b.setOutcome(.err);
        // Extract the err code: format is `{"ok":false,"err":"..."}`.
        // Slice between the two `"` after `"err":`.
        if (extractErrCode(resp.*)) |code| b.setErrCode(code);
    }
    audit.push(&b);
}

/// Extract the `err` field from a `{"ok":false,"err":"..."}`
/// response. Returns null if the shape doesn't match (defensive).
fn extractErrCode(resp: []const u8) ?[]const u8 {
    const marker = "\"err\":\"";
    const start = std.mem.indexOf(u8, resp, marker) orelse return null;
    const after = start + marker.len;
    const end = std.mem.indexOfScalarPos(u8, resp, after, '"') orelse return null;
    return resp[after..end];
}

/// Insert `,"<key>":"<value>"` into the response just before the
/// closing `}` (Part 13 §5.6.3 / §7.1 / §7.2). Returns the updated
/// slice; the underlying memory lives in `out`. No-op (returns `resp`
/// unchanged) if the buffer can't fit the extra bytes — the splice is
/// best-effort, never blocking the response itself.
///
/// Pre-conditions checked defensively: the response must end in `}\n`
/// (every formatter in this file does, including fmtErr). Anything else
/// returns `resp` unchanged — the splice never corrupts a well-formed
/// JSON response by emitting a partial insert.
///
/// CALLER GUARANTEE: `key` and `value` MUST be JSON-safe (no characters
/// requiring escape). Today's call sites pass literal-or-charset-vetted
/// strings — `"fidelity"` + a closed set of values, or a `correlation_id`
/// already filtered through `isValidCorrelationIdChar`. Adding a new
/// call site with un-vetted bytes would break this contract.
fn spliceField(out: []u8, resp: []const u8, key: []const u8, value: []const u8) []const u8 {
    if (resp.len < 2) return resp;
    if (resp[resp.len - 1] != '\n' or resp[resp.len - 2] != '}') return resp;

    // ',"<key>":"<value>"' — three literal bytes (comma, quote, colon,
    // open-quote, close-quote) plus the key + value bytes themselves.
    const extra = 1 + 1 + key.len + 1 + 1 + 1 + value.len + 1;
    //              ,    "    key    "   :   "    value    "

    // Bounds: is there room in `out` for the spliced response?
    if (resp.len + extra > out.len) return resp;

    const close_pos = resp.len - 2; // points at `}`; `resp[close_pos+1]` is `\n`
    const new_len = resp.len + extra;

    // Shift the trailing "}\n" right by `extra` bytes. copyBackwards is
    // the right primitive for an overlapping copy where dest > source.
    std.mem.copyBackwards(
        u8,
        out[close_pos + extra ..][0..2],
        out[close_pos..][0..2],
    );
    // Write `,"<key>":"<value>"` at close_pos.
    var p = close_pos;
    out[p] = ',';
    p += 1;
    out[p] = '"';
    p += 1;
    @memcpy(out[p..][0..key.len], key);
    p += key.len;
    out[p] = '"';
    p += 1;
    out[p] = ':';
    p += 1;
    out[p] = '"';
    p += 1;
    @memcpy(out[p..][0..value.len], value);
    p += value.len;
    out[p] = '"';

    return out[0..new_len];
}

/// Is this op a read-only observer? Determines whether the response
/// carries a top-level `fidelity` field (Part 13 §7.2).
///
/// Read-only: observers + introspection — `list`, `inspect`,
/// `capabilities`, `drain_events`, `list_leases`, `dump_all`.
/// Write: `claim`, `release`, `raw` (returns an fd handle but its
/// outcome isn't a fidelity question), `control` (mutates hardware).
fn isReadOnlyOp(req: request.Request) bool {
    return switch (req) {
        .list, .inspect, .capabilities, .drain_events, .list_leases, .dump_all, .inspect_full, .dump_chain, .subscribe, .unsubscribe, .driver_info, .audit_tail, .state_snapshot => true,
        // `attenuate` mints a sub-cap; `recover` resets hardware + mutates
        // quarantine state — both writes, not fidelity-bearing reads.
        .claim, .release, .raw, .control, .revoke, .attenuate, .recover, .open_shm_channel, .drain_shm_channel, .start_shm_drain, .reconcile, .irq_scan => false,
    };
}

/// Does this response carry `"ok":true`? Used to gate fidelity-splicing
/// to successful responses only — an error response has no fidelity to
/// report (we couldn't compute anything).
fn isOkResponse(resp: []const u8) bool {
    const ok_prefix = "{\"ok\":true";
    return resp.len >= ok_prefix.len and
        std.mem.eql(u8, resp[0..ok_prefix.len], ok_prefix);
}

/// Dispatch a validated typed request. NO `std.json.Value` access here — the
/// gate already validated everything.
fn dispatch(
    facade: *facademod.Facade,
    peer: cap.PeerCred,
    peer_key: u64,
    req: request.Request,
    line: []const u8,
    out: []u8,
) DispatchResult {
    return switch (req) {
        .list => |r| .{ .resp = format.fmtList(facade, r.scope.slice(), r.cap_id, out) },
        .claim => |r| blk: {
            const lease = if (r.detach) facade.claimDetachFromPeer(
                r.cap_id,
                peer,
                &r.urn,
                r.mode,
                r.holder,
                format.deadlineFrom(r.deadline_ms),
            ) catch |e| break :blk .{ .resp = format.fmtErr(out, format.errToWireCode(e)) } else facade.claimFromPeer(
                r.cap_id,
                peer,
                &r.urn,
                r.mode,
                r.holder,
                format.deadlineFrom(r.deadline_ms),
            ) catch |e| break :blk .{ .resp = format.fmtErr(out, format.errToWireCode(e)) };
            const resp = std.fmt.bufPrint(
                out,
                "{{\"ok\":true,\"lease_id\":{d}}}\n",
                .{lease},
            ) catch format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp };
        },
        .release => |r| blk: {
            // J1 (review wave 2 — Cluster J): propagate the typed
            // error instead of silently emitting ok:true. The
            // route-miss case (unrouted URN) now surfaces as
            // NotFound on the wire; consumers' typed
            // `except NotFoundError` finally covers the "I tried
            // to release a URN no worker owns" path.
            facade.release(&r.urn, r.lease, format.deadlineFrom(r.deadline_ms)) catch |e|
                break :blk .{ .resp = format.fmtErr(out, format.errToWireCode(e)) };
            const resp = std.fmt.bufPrint(out, "{{\"ok\":true}}\n", .{}) catch
                format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp };
        },
        .raw => |r| blk: {
            _ = facade.rawFromPeer(
                r.cap_id,
                peer,
                &r.urn,
                r.lease,
                r.caller,
                format.deadlineFrom(r.deadline_ms),
            ) catch |e| break :blk .{ .resp = format.fmtErr(out, format.errToWireCode(e)) };

            // The raw call succeeded — the lease holder has the right to a
            // raw channel for this resource. Ask the routed worker's
            // backend for a DUPED fd (M2 / review wave 3) — pre-M2 this
            // returned a borrowed fd and we dup'd it here, but that
            // opened a UAF race (concurrent vRelease could close+recycle
            // between fdForLease returning and our fcntl). The backend
            // now does the dup under its claims_lock, atomic with the
            // find. The returned fd is OWNED — we close it after the
            // SCM_RIGHTS send, no broker-side dup needed.
            const fd_to_send = facade.fdForLease(&r.urn, r.lease);
            const has_fd = fd_to_send >= 0;
            const resp = std.fmt.bufPrint(
                out,
                "{{\"ok\":true,\"raw\":true,\"fd\":{}}}\n",
                .{has_fd},
            ) catch format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp, .fd_to_send = fd_to_send };
        },
        .inspect => |r| .{ .resp = format.fmtInspect(facade, &r.urn, r.cap_id, out) },
        .capabilities => .{ .resp = format.fmtCapabilities(facade, out) },
        .drain_events => |r| .{ .resp = format.fmtDrainEvents(facade, peer_key, &r, out) },
        .control => |r| .{ .resp = format.fmtControl(facade, &r, out) },
        .list_leases => |r| .{ .resp = format.fmtListLeases(facade, r.scope.slice(), r.cap_id, out) },
        .dump_all => |r| .{ .resp = format.fmtDumpAll(facade, r.scope.slice(), r.cap_id, out) },
        .inspect_full => |r| .{ .resp = format.fmtInspectFull(facade, &r.urn, r.depth, r.cap_id, out) },
        .dump_chain => |r| .{ .resp = format.fmtDumpChain(facade, &r.urn, r.cap_id, out) },
        .subscribe => |r| .{ .resp = format.fmtSubscribe(peer_key, &r, line, out) },
        .unsubscribe => |r| .{ .resp = format.fmtUnsubscribe(peer_key, &r, out) },
        .driver_info => |r| .{ .resp = format.fmtDriverInfo(r.name(), out) },
        .audit_tail => |r| .{ .resp = format.fmtAuditTail(facade, peer, r.cap_id, r.n, out) },
        .state_snapshot => |r| .{ .resp = format.fmtStateSnapshot(facade, peer, r.cap_id, out) },
        .revoke => |r| .{ .resp = format.fmtRevoke(facade, peer, &r, out) },
        .attenuate => |r| .{ .resp = format.fmtAttenuate(facade, peer, &r, out) },
        .recover => |r| .{ .resp = format.fmtRecover(facade, peer, &r, out) },
        .reconcile => |r| .{ .resp = format.fmtReconcile(facade, peer, &r, out) },
        .irq_scan => |r| .{ .resp = format.fmtIrqScan(facade, peer, &r, out) },
        .open_shm_channel => blk: {
            // Allocate a private memfd-backed region for this connection's
            // uid, lay out the ring header in it, and fd-pass the memfd.
            // The transport SCM_RIGHTS-sends `fd_to_send` and closes the
            // broker's dup; the client's mapping survives.
            const pool = shm.globalPool();
            // creator = peer_key (the cfd) → the region's lifecycle is bound to
            // THIS connection, freed only when it disconnects (releaseAllForConn),
            // not when any same-uid sibling connection does.
            const idx = pool.acquire(peer.uid, peer_key) orelse
                break :blk .{ .resp = format.fmtErr(out, "Overloaded") }; // S5 shed
            // view/fdFor can only miss if a concurrent same-uid disconnect
            // (releaseAllForUid) yanked the region between calls — rare;
            // degrade to a typed error rather than panic.
            const region_view = pool.view(idx, peer.uid) orelse
                break :blk .{ .resp = format.fmtErr(out, "Unavailable") };
            _ = ring.HostedRing(u64).layout(region_view, shm.CHANNEL_SLOTS);
            const fd = pool.fdFor(idx, peer.uid) orelse
                break :blk .{ .resp = format.fmtErr(out, "Unavailable") };
            // Descriptor: the client validates these against the header it
            // reads from the mmap. Emitted inline (same idiom as .raw).
            const resp = std.fmt.bufPrint(
                out,
                "{{\"ok\":true,\"shm\":true,\"fd\":true,\"channel\":{d},\"slot_count\":{d}," ++
                    "\"region_bytes\":{d},\"payload_size\":{d},\"slots_offset\":{d}," ++
                    "\"head_offset\":{d},\"tail_offset\":{d},\"dropped_offset\":{d},\"version\":{d}}}\n",
                .{ idx, shm.CHANNEL_SLOTS, shm.REGION_BYTES, @sizeOf(u64), ring.SHM_SLOTS_OFFSET, ring.SHM_HEAD_OFFSET, ring.SHM_TAIL_OFFSET, ring.SHM_DROPPED_OFFSET, ring.SHM_VERSION },
            ) catch format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp, .fd_to_send = fd };
        },
        .start_shm_drain => |r| blk: {
            // Slice 4 — opt into push-mode: spawn a background thread that
            // drains this channel's ring continuously. Owner-checked inside
            // startDrain (a stranger can't drain another tenant's channel);
            // refuses if already draining or on spawn/alloc failure (shed).
            const pool = shm.globalPool();
            if (!pool.startDrain(r.channel, peer.uid))
                break :blk .{ .resp = format.fmtErr(out, "Unavailable") };
            const resp = std.fmt.bufPrint(
                out,
                "{{\"ok\":true,\"draining\":true,\"channel\":{d}}}\n",
                .{r.channel},
            ) catch format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp };
        },
        .drain_shm_channel => |r| blk: {
            // Push-mode: if a background drainer owns this channel, return its
            // accumulated stats rather than racing it with a synchronous pull.
            const pool = shm.globalPool();
            if (pool.drainerStats(r.channel, peer.uid)) |s| {
                const resp = std.fmt.bufPrint(
                    out,
                    "{{\"ok\":true,\"drained\":{d},\"checksum\":{d},\"last_seq\":{d},\"dropped\":{d}}}\n",
                    .{ s.drained, s.checksum, s.last_seq, s.dropped },
                ) catch format.fmtErr(out, "fmt");
                break :blk .{ .resp = resp };
            }
            // Pull-mode (default): drain the client-produced ring as the
            // asymmetric-trust CONSUMER. The owner-check (view by peer.uid)
            // means a stranger can't drain another tenant's channel.
            const region_view = pool.view(r.channel, peer.uid) orelse
                break :blk .{ .resp = format.fmtErr(out, "NotFound") };
            var hr = ring.HostedRing(u64).overlay(region_view, shm.CHANNEL_SLOTS);
            var drained: u64 = 0;
            var checksum: u64 = 0;
            var last_seq: u64 = 0;
            // BOUND the loop by capacity: a hostile producer-written `head`
            // can't make the broker spin. popUntrustedProducer is bounds-
            // safe per pop; this cap bounds the WORK. A ring of N slots can
            // hold at most N live items, so N pops drains a well-behaved
            // producer fully and a malicious one harmlessly.
            var i: usize = 0;
            while (i < shm.CHANNEL_SLOTS) : (i += 1) {
                const item = hr.popUntrustedProducer() orelse break;
                drained +%= 1;
                checksum ^= item.payload;
                last_seq = item.seq;
            }
            const resp = std.fmt.bufPrint(
                out,
                "{{\"ok\":true,\"drained\":{d},\"checksum\":{d},\"last_seq\":{d},\"dropped\":{d}}}\n",
                .{ drained, checksum, last_seq, hr.droppedCount() },
            ) catch format.fmtErr(out, "fmt");
            break :blk .{ .resp = resp };
        },
    };
}

// ===========================================================================
// Unit tests — the envelope helpers were previously exercised ONLY at the
// wire (via broker-spawning contract tests). Now that dispatch is its own
// module they're directly unit-testable (1A-iii + WS-B).
// ===========================================================================

const testing = std.testing;

test "isOkResponse: only `{\"ok\":true` prefixes are success" {
    try testing.expect(isOkResponse("{\"ok\":true,\"resources\":[]}\n"));
    try testing.expect(isOkResponse("{\"ok\":true}\n"));
    try testing.expect(!isOkResponse("{\"ok\":false,\"err\":\"NotFound\"}\n"));
    try testing.expect(!isOkResponse("{}")); // too short / no prefix
    try testing.expect(!isOkResponse("")); // empty
}

test "extractErrCode: pulls the err code, null on a non-error shape" {
    try testing.expectEqualStrings(
        "NotFound",
        extractErrCode("{\"ok\":false,\"err\":\"NotFound\"}\n").?,
    );
    try testing.expectEqualStrings(
        "Overloaded",
        extractErrCode("{\"ok\":false,\"err\":\"Overloaded\"}\n").?,
    );
    try testing.expect(extractErrCode("{\"ok\":true,\"resources\":[]}\n") == null);
    try testing.expect(extractErrCode("garbage") == null);
}

test "spliceField: inserts `,\"k\":\"v\"` before the closing brace" {
    // `spliceField` edits IN PLACE — `resp` must alias `out` (as it does
    // in production: the formatter writes into `out` + returns a slice of
    // it). Mirror that: stage the input in `out`, then splice.
    var out: [128]u8 = undefined;
    const input = "{\"ok\":true}\n";
    @memcpy(out[0..input.len], input);
    const r = spliceField(&out, out[0..input.len], "fidelity", "live");
    try testing.expectEqualStrings("{\"ok\":true,\"fidelity\":\"live\"}\n", r);
}

test "spliceField: no-op (returns input) on a non `}\\n`-terminated response" {
    var out: [128]u8 = undefined;
    // Missing the trailing newline → not a well-formed response → unchanged.
    const bad = "{\"ok\":true}";
    try testing.expectEqualStrings(bad, spliceField(&out, bad, "k", "v"));
    try testing.expectEqualStrings("x", spliceField(&out, "x", "k", "v")); // too short
}

test "isReadOnlyOp: observers true, mutators false" {
    const ro: request.Request = .{ .list = .{ .scope = Urn.from("hw:/**") } };
    try testing.expect(isReadOnlyOp(ro));
    const rw: request.Request = .{ .revoke = .{ .urn = Urn.from("hw:/x") } };
    try testing.expect(!isReadOnlyOp(rw));
}
