//! mgf-hrb-server (trust gate) — the S1 hardened decode/parse layer (Part 12
//! §3.2). The ONE place raw client bytes turn into typed requests. Everything
//! beyond this gate sees a validated `Request`, never a `std.json.Value`.
//!
//! THE CONTRACT (S1 / U1 / U2):
//!   • Every JSON access goes through a typed extractor that returns
//!     `Err.BadRequest` on any mismatch (missing key, wrong type, oversized
//!     string, out-of-range integer, unknown op, unknown enum). Direct
//!     `.string`/`.integer`/`.object` access on `std.json.Value` is FORBIDDEN
//!     in this file — those panic on type confusion, and a client-triggered
//!     panic is the most basic S1 violation.
//!   • Input length is bounded BEFORE parsing (S5 back-pressure, made
//!     concrete: a 4 GB request line cannot be buffered into memory and
//!     parsed).
//!   • A fuzzer throwing random/malformed bytes at `parse()` must produce
//!     ONLY `Err.BadRequest` or a valid `Request` — never a panic, never
//!     undefined behaviour. The S1 conformance test in `request_test.zig`
//!     pins this property.
//!
//! Future hardenings stacked on this gate (separate Tier-0 items; not in
//! this commit):
//!   • Unforgeable + connection-bound capabilities (Part 12 §3.3) — replaces
//!     the bare `u64` `cap_id` with an fd-backed reference + `SO_PEERCRED`
//!     binding.
//!   • Asymmetric shared-memory trust (§3.5) — when the data-plane ring is
//!     wired in.
//!   • Per-tenant rate limits + per-connection progress deadline (§3.7).

const std = @import("std");
const model = @import("mgf_hrb_model");
const cap = @import("mgf_hrb_cap");

const Urn = model.Urn;
const ClaimMode = model.ClaimMode;
const ClientId = model.ClientId;
const LeaseId = model.LeaseId;
const Err = model.Err;

/// The maximum size of one request line. Anything larger is rejected at the
/// gate before parsing — a giant input cannot exhaust the broker's memory
/// (S5). The figure is intentionally modest; the control plane is cold and
/// requests are small (Part 5.1).
pub const MAX_LINE_BYTES: usize = 4096;

/// Maximum length of a URN string in a request. The model's `Urn` carries a
/// 128-byte fixed buffer; we reject before truncation rather than silently
/// trim (G2: never lie about what we received).
pub const MAX_URN_BYTES: usize = 128;

/// Maximum length of an op verb or claim-mode string. Anything legitimate is
/// well under this.
pub const MAX_VERB_BYTES: usize = 32;

/// Upper bound on a caller-supplied `deadline_ms`. Roughly an hour — anything
/// longer is either a typo or a use case that should bypass deadlines via
/// the explicit "no deadline" sentinel (0 / absent field). Rejecting at the
/// gate keeps a single garbage number from spawning an effectively-never-
/// expiring deadline that a wedged op could ride forever.
pub const MAX_DEADLINE_MS: i64 = 60 * 60 * 1000;

/// The op tag. Closed enum; an unknown op at the wire becomes
/// `Err.BadRequest` at the gate.
pub const Op = enum { list, claim, release, raw, inspect, capabilities, drain_events, control, list_leases, dump_all, inspect_full, dump_chain, subscribe, unsubscribe, driver_info, audit_tail, state_snapshot, revoke, attenuate, recover, open_shm_channel, drain_shm_channel, start_shm_drain, reconcile, irq_scan };

/// Max byte length for a control-verb `value` field (the sysfs payload).
/// Driver names cap around 28 chars; autosuspend modes are ≤4; bools are 1.
/// 64 is generous and keeps a noisy / malicious request bounded at the gate.
pub const MAX_CONTROL_VALUE_BYTES: usize = 64;

pub const ListReq = struct {
    scope: Urn,
    /// R-T1-11A (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (closes U11 / S3 isolation gap, survivability
    /// review §3.6). When non-zero, the broker intersects the
    /// requested scope with the cap's scope before emitting results;
    /// URNs outside the cap's scope are silently filtered out (the
    /// caller can't even *observe the existence* of resources beyond
    /// its grant). Default 0 preserves legacy behavior (no filter)
    /// for clients that don't yet present a cap on `list`.
    cap_id: u64 = 0,
};

/// Empty payload — `capabilities` returns broker-level info (which workers
/// exist, what each one's backend reports). Per FEEDBACK TOOLS-01 / Part 6.2.1.
pub const CapabilitiesReq = struct {};

/// Open a shared-memory data-plane channel (Part 5.3). Slice-1 carries no
/// params beyond the (future) capability id — the broker allocates a
/// private region for the connection's uid and fd-passes it. `cap_id` is
/// parsed now for the eventual S3 right-to-open gating.
pub const OpenShmChannelReq = struct { cap_id: u64 = 0 };

/// Drain a shared-memory channel's ring (Part 5.3). `channel` is the
/// region index returned by `open_shm_channel`; the broker owner-checks it
/// against the connection's uid. `cap_id` is parsed for future S3 gating.
pub const DrainShmChannelReq = struct { channel: u32 = 0, cap_id: u64 = 0 };

/// Start a background drain thread on a shared-memory channel (Part 5.3,
/// slice 4 — push-mode). `channel` is the region index from
/// `open_shm_channel`; the broker owner-checks it against the connection's
/// uid. After this, `drain_shm_channel` returns the background thread's
/// accumulated stats instead of pulling synchronously. `cap_id` is parsed for
/// future S3 gating, same as the other channel ops.
pub const StartShmDrainReq = struct { channel: u32 = 0, cap_id: u64 = 0 };

/// Broker-wide restart reconcile (Tier-1 #8, R-T1-8-RESTART-WIRING). After a
/// restart, re-derive truth from hardware + live clients and quarantine the
/// orphans. `scope` (a glob, default the whole `hw:/**` namespace) bounds which
/// workers are reconciled; the op is steal-gated on `scope` (a stranger can't
/// reconcile a namespace it has no authority over). `cap_id`/`deadline_ms` as
/// usual.
pub const ReconcileReq = struct { scope: Urn = Urn.from("hw:/**"), cap_id: u64 = 0, deadline_ms: u64 = 0 };

/// Broker-wide interrupt-rate scan (D7, Tier-2). Samples every granted
/// device's interrupt counter and revoke+quarantines any that is flooding.
/// No scope — it sweeps all live leases; steal-gated on `hw:/**`.
pub const IrqScanReq = struct { cap_id: u64 = 0, deadline_ms: u64 = 0 };

/// Drain pending watch events from each worker whose subtree intersects
/// `scope` (USB-worker §5.3.4). Returns events as JSON array; the client
/// re-issues the call to poll. Future hardening adds true push via a
/// long-lived response stream + multi-client poll loop in the broker.
pub const DrainEventsReq = struct {
    scope: Urn,
    /// Part 13 §5.4.2 — optional subscription id. When present, the
    /// dispatcher drains from THAT subscription's bounded queue
    /// (events already filtered by the subscription's predicate);
    /// `scope` is ignored. When absent, the legacy behavior runs:
    /// pull every backend-buffered event whose worker scope
    /// intersects `scope`, fan them out to all subscriptions for
    /// background filtering, and return the raw stream.
    sub_id_buf: [MAX_SUBSCRIPTION_ID_BYTES]u8 = [_]u8{0} ** MAX_SUBSCRIPTION_ID_BYTES,
    sub_id_len: u8 = 0,

    pub fn subscriptionId(self: *const DrainEventsReq) []const u8 {
        return self.sub_id_buf[0..self.sub_id_len];
    }
    pub fn hasSubscriptionId(self: *const DrainEventsReq) bool {
        return self.sub_id_len > 0;
    }
};

/// List live leases whose URN matches `scope` (FEEDBACK SDK-03). Pure
/// read; no lease state is touched. Same scope grammar as `list` /
/// `drain_events`.
pub const ListLeasesReq = struct {
    scope: Urn,
    /// R-T1-11B (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (Tier-1 #11 / S3 isolation). Same semantics as
    /// `ListReq.cap_id`: when non-zero the broker drops lease records
    /// whose URN is outside the cap's scope before emission. Default 0
    /// preserves legacy unfiltered behavior.
    cap_id: u64 = 0,
};

/// Walk every URN matching `scope`; for each, call inspect and assemble
/// the full set as a single response (Part 13 §5.5.3). Read-only,
/// `observe`-right gated. Honest emptiness: empty scope → `items: []`.
///
/// The response buffer is bounded (~64 KB today); a scope matching more
/// resources than fit returns the partial set with `truncated: true` so
/// the consumer can re-issue against a narrower scope.
pub const DumpAllReq = struct {
    scope: Urn,
    /// R-T1-11B (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (Tier-1 #11 / S3 isolation). When non-zero, URNs
    /// outside the cap's scope are dropped during pass-1 enumeration so
    /// they never reach the per-URN inspect or the emitted set. Default
    /// 0 preserves legacy unfiltered behavior.
    cap_id: u64 = 0,
};

/// Walk the URN's substrate subtree (Part 13 §5.5.1). Returns the
/// nested tree of attrs + symlinks the backend's substrate exposes
/// (sysfs for the Linux backends). `depth` selects shallow (one
/// level) vs full recursive walk. Read-only, `observe`-right gated.
pub const InspectFullReq = struct {
    urn: Urn,
    /// Default is full recursive walk per §5.5.1 (depth: "all").
    depth: model.InspectDepth = .all,
    /// R-T1-11B (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (Tier-1 #11 / S3 isolation). Unlike the scope-shaped
    /// ops, `inspect_full` targets a SINGLE root URN; when non-zero and
    /// that URN is outside the cap's scope, the broker returns NotFound
    /// — the caller can't distinguish an out-of-scope URN from a
    /// nonexistent one (existence itself is not observable beyond the
    /// grant). Default 0 preserves legacy unfiltered behavior.
    cap_id: u64 = 0,
};

/// Walk the URN's parent chain upward (Part 13 §5.5.2). Returns the
/// linear topology trace as `(urn, level, summary)` triples — leaf
/// first, substrate root last. Read-only, `observe`-right gated.
pub const DumpChainReq = struct {
    urn: Urn,
    /// R-T1-11D (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (Tier-1 #11 / S3 isolation). Single-URN op: when
    /// non-zero and the URN is outside the cap's scope, the broker
    /// returns NotFound. The parent-chain walk above the URN is
    /// substrate topology, not separately-claimable broker URNs, so
    /// the S3 check is on the leaf URN only. Default 0 preserves legacy
    /// behavior.
    cap_id: u64 = 0,
};

/// Cap on a subscribe request's `id` string (Part 13 §5.4.2). 64 bytes
/// matches the registry's `MAX_SUBSCRIPTION_ID_LEN`.
pub const MAX_SUBSCRIPTION_ID_BYTES: usize = 64;

/// Register a server-side event filter (Part 13 §5.4.2). The
/// per-client `id` is echoed back as the subscription handle the
/// consumer passes to `drain_events` to drain only their queue.
/// `predicate` is OPTIONAL — when absent, every event in scope
/// matches.
///
/// Implementation note: the predicate sub-object is NOT carried on
/// this struct. The wire-op gate validates that the field, when
/// present, is a JSON object; the server's `fmtSubscribe` re-parses
/// the original line to materialise the predicate AST against a
/// fresh per-subscription arena. Double-parse cost is one std.json
/// pass over a sub-2 KiB input, well under the predicate parser's
/// per-call budget. Keeps the request types allocation-free.
pub const SubscribeReq = struct {
    id_buf: [MAX_SUBSCRIPTION_ID_BYTES]u8 = [_]u8{0} ** MAX_SUBSCRIPTION_ID_BYTES,
    id_len: u8 = 0,
    scope: Urn,
    /// True when the request carried a `predicate` field. The
    /// formatter uses this to decide whether to re-parse for the
    /// AST or use the no-op `matches_all` predicate.
    has_predicate: bool = false,

    pub fn id(self: *const SubscribeReq) []const u8 {
        return self.id_buf[0..self.id_len];
    }
};

/// Tier-0 #4 — broker-initiated revocation of `urn`'s claim
/// (survivability §3.4). Tears down the backend's kernel-side
/// claim + marks any holding lease as revoked; subsequent uses
/// of the lease return `Err.Revoked`. Cap-gated by the `steal`
/// right (taking the device away from another holder is a
/// stricter authority than `claim`).
pub const RevokeReq = struct {
    urn: Urn,
    cap_id: u64 = 0,
    /// Optional caller-supplied budget in ms. 0 → no deadline.
    deadline_ms: u64 = 0,
};

/// Tier-1 #12 — run the reset-escalation ladder on a quarantined URN to
/// recover it back to `Free` (`FLR → bus → quarantine`, each verified).
/// `steal`-right gated (a forceful hardware repair, like `revoke`). A
/// no-op on a non-quarantined URN.
pub const RecoverReq = struct {
    urn: Urn,
    cap_id: u64 = 0,
    /// Optional aggregate budget (ms) for the ladder; 0 → no deadline.
    deadline_ms: u64 = 0,
};

/// R-CAP-ATTEN-WIRE — mint an attenuated sub-capability from the
/// connection's cap. `cap_id` is the PARENT to narrow from (required,
/// non-zero; must carry the `grant` right and match the connection
/// binding — both enforced by `CapTable.attenuateFromPeer`). The three
/// narrowings mirror `cap.Restriction` and are each OPTIONAL: an axis
/// that's absent is inherited from the parent unchanged. Every present
/// axis can only SHRINK authority — a widen is refused at the cap engine
/// (so this wire op cannot be used to escalate).
pub const AttenuateReq = struct {
    cap_id: u64,
    /// scope narrowing (a glob that must be ⊆ the parent's scope);
    /// `scope_len == 0` ⇒ no scope narrowing.
    scope_buf: [MAX_URN_BYTES]u8 = [_]u8{0} ** MAX_URN_BYTES,
    scope_len: u16 = 0,
    /// rights narrowing (must be ⊆ the parent's rights). `has_rights`
    /// distinguishes "narrow to this set" (including the empty set —
    /// drop everything) from "don't narrow rights".
    has_rights: bool = false,
    rights: cap.RightSet = .{},
    /// claim-mode ceiling narrowing (must be ≤ the parent's ceiling).
    has_mode_ceiling: bool = false,
    mode_ceiling: ClaimMode = .exclusive,

    pub fn scope(self: *const AttenuateReq) []const u8 {
        return self.scope_buf[0..self.scope_len];
    }

    /// Project to the cap engine's `Restriction` (slices borrow this
    /// struct; valid for the dispatch call, where the engine copies the
    /// scope into a `Urn` immediately).
    pub fn restriction(self: *const AttenuateReq) cap.Restriction {
        return .{
            .scope = if (self.scope_len > 0) self.scope() else null,
            .rights = if (self.has_rights) self.rights else null,
            .mode_ceiling = if (self.has_mode_ceiling) self.mode_ceiling else null,
        };
    }
};

/// Retrospective audit-ring query (Part 13 §5.6.4). Reads up to
/// `n` most-recent `AuditRecord`s from the broker's ring; clamped
/// at the ring's capacity (4096 today). Read-only; gated by the
/// `audit_read` right (Part 13 §8) — same cap_id presentation
/// shape as claim/raw/control.
pub const AuditTailReq = struct {
    /// Default 100 — a reasonable "last interactive minute" window.
    /// Clamped to MAX_AUDIT_TAIL_N at the gate.
    n: u32 = 100,
    /// Wire cap id (Part 12 §3.3 Tier-0 #2). 0 = "no cap
    /// presented"; the dispatcher rejects with NotPermitted.
    cap_id: u64 = 0,
};

pub const MAX_AUDIT_TAIL_N: u32 = 4096;

/// Broker-wide self-state snapshot (Part 13 §5.6.2). Read-only;
/// gated by the `audit_read` right.
pub const StateSnapshotReq = struct {
    cap_id: u64 = 0,
};

/// Cap on a kernel-driver name in a `driver_info` request (Part 13
/// §5.2.2). 64 bytes is generous — Linux's KMOD_NAME_LEN is 64 too
/// (modules.h MODULE_NAME_LEN); driver names match modules in
/// practice. Strict cap defends the parse + the sysfs path-build.
pub const MAX_DRIVER_INFO_NAME_BYTES: usize = 64;

/// Report the known interface of a kernel driver (Part 13 §5.2.2).
/// Read-only; broker-global (not URN-scoped). Returns the driver's
/// load state + bound-device count from /sys, plus curated-registry
/// fields (sub-phase 3B; absent in v1 = null/empty).
pub const DriverInfoReq = struct {
    name_buf: [MAX_DRIVER_INFO_NAME_BYTES]u8 = [_]u8{0} ** MAX_DRIVER_INFO_NAME_BYTES,
    name_len: u8 = 0,

    pub fn name(self: *const DriverInfoReq) []const u8 {
        return self.name_buf[0..self.name_len];
    }
};

/// Tear down a subscription (Part 13 §5.4.2). The peer identity that
/// owned the subscribe is enforced at dispatch time — a client
/// cannot unsubscribe another peer's subscription.
pub const UnsubscribeReq = struct {
    id_buf: [MAX_SUBSCRIPTION_ID_BYTES]u8 = [_]u8{0} ** MAX_SUBSCRIPTION_ID_BYTES,
    id_len: u8 = 0,

    pub fn id(self: *const UnsubscribeReq) []const u8 {
        return self.id_buf[0..self.id_len];
    }
};

pub const ClaimReq = struct {
    urn: Urn,
    mode: ClaimMode,
    holder: ClientId,
    /// Optional caller-supplied budget in ms. 0 → no deadline (`Deadline.none`).
    /// Plumbed to `Core.claim` which forwards to the backend acquire (D-029).
    deadline_ms: u64 = 0,
    /// Opt-in: route through the backend's atomic disconnect-then-claim
    /// primitive (USBDEVFS_DISCONNECT_CLAIM on USB). Cap-gated by the
    /// `detach_driver` right (USB-worker §5.10). When false (the
    /// default), a bound kernel driver makes claim return Err.Conflict
    /// honestly — the broker never silently steals devices.
    detach: bool = false,
    /// Capability id the client is presenting (Part 12 §3.3 Tier-0
    /// #2). The broker resolves this against its CapTable + the
    /// connection peer's SO_PEERCRED. 0 means "no cap presented" —
    /// the broker rejects with NotPermitted rather than falling back
    /// to a default (the U3 closure: a request without a valid cap
    /// has no authority, no matter where it came from).
    cap_id: u64 = 0,
};

pub const ReleaseReq = struct {
    urn: Urn,
    lease: LeaseId,
    deadline_ms: u64 = 0,
};

pub const RawReq = struct {
    urn: Urn,
    lease: LeaseId,
    caller: ClientId,
    deadline_ms: u64 = 0,
    cap_id: u64 = 0, // same shape + semantic as ClaimReq.cap_id (Tier-0 #2)
};

pub const InspectReq = struct {
    urn: Urn,
    /// R-T1-11D (2026-05-27) — optional cap_id for capability-filtered
    /// enumeration (Tier-1 #11 / S3 isolation). Single-URN op: when
    /// non-zero and the URN is outside the cap's scope, the broker
    /// returns NotFound (indistinguishable from a nonexistent URN —
    /// existence isn't observable beyond the grant). Same shape as
    /// `InspectFullReq.cap_id`. Default 0 preserves legacy behavior.
    cap_id: u64 = 0,
};

/// Typed control verb (USB-worker §5.10). `verb` names which sysfs
/// write to issue; `value` is the verb-specific payload (interpretation
/// deferred to the backend). The value is stored as a fixed-size
/// buffer to keep the request struct allocation-free.
pub const ControlReq = struct {
    urn: Urn,
    verb: model.ControlVerb,
    value_buf: [MAX_CONTROL_VALUE_BYTES]u8 = [_]u8{0} ** MAX_CONTROL_VALUE_BYTES,
    value_len: u8 = 0,
    /// The writer's capability id (Part 12 §3.3 Tier-0 #2). Same
    /// shape + semantic as `ClaimReq.cap_id`. Sub-phase 4B uses
    /// this as the `last_writer_cap_id` field in the
    /// per-(urn,verb) serialiser's state — so the next writer
    /// learns who wrote before them. 0 means "no cap presented";
    /// today's broker uses a shared root cap so every writer
    /// currently looks the same on the wire, but the shape
    /// supports per-client caps when those land.
    cap_id: u64 = 0,

    pub fn value(self: *const ControlReq) []const u8 {
        return self.value_buf[0..self.value_len];
    }
};

/// Cap on the correlation-id length (Part 13 §5.6.3 / §7.1). 128 bytes is
/// generous for any sane id shape (UUIDv4 string = 36 chars; a tagged
/// `vmgr-claim-001` = ~17 chars). The cap bounds the response splice and
/// keeps a single hostile request from forcing a giant memmove.
pub const MAX_CORRELATION_ID_BYTES: usize = 128;

/// Allowed charset for `correlation_id`. ASCII alphanumeric + `-_.` only.
/// Why constrained: the broker SPLICES the value into the response JSON
/// without escaping. Allowing arbitrary string bytes would force per-call
/// JSON escaping in the splice path — solvable but adds cost to every op.
/// The cap-set is enough for UUIDs, dotted-tag ids, hyphenated client ids;
/// anything richer is rejected at the gate with BadRequest.
fn isValidCorrelationIdChar(c: u8) bool {
    return (c >= 'a' and c <= 'z') or
        (c >= 'A' and c <= 'Z') or
        (c >= '0' and c <= '9') or
        c == '-' or c == '_' or c == '.';
}

/// A parsed, validated, typed request. After `parse()` returns one of these,
/// the server's dispatcher operates on this — never on raw bytes or
/// `std.json.Value`.
pub const Request = union(Op) {
    list: ListReq,
    claim: ClaimReq,
    release: ReleaseReq,
    raw: RawReq,
    inspect: InspectReq,
    capabilities: CapabilitiesReq,
    drain_events: DrainEventsReq,
    control: ControlReq,
    list_leases: ListLeasesReq,
    dump_all: DumpAllReq,
    inspect_full: InspectFullReq,
    dump_chain: DumpChainReq,
    subscribe: SubscribeReq,
    unsubscribe: UnsubscribeReq,
    driver_info: DriverInfoReq,
    audit_tail: AuditTailReq,
    state_snapshot: StateSnapshotReq,
    revoke: RevokeReq,
    attenuate: AttenuateReq,
    recover: RecoverReq,
    open_shm_channel: OpenShmChannelReq,
    drain_shm_channel: DrainShmChannelReq,
    start_shm_drain: StartShmDrainReq,
    reconcile: ReconcileReq,
    irq_scan: IrqScanReq,
};

/// What `parse()` returns: the typed `Request` PLUS any envelope-level
/// metadata. Today: `correlation_id` (Part 13 §5.6.3 / §7.1) — an
/// optional caller-supplied string the broker echoes verbatim in the
/// response. Future envelope fields (deadline propagation, tenancy tag,
/// trace headers) land here without re-shaping every formatter.
pub const Parsed = struct {
    req: Request,
    /// Inline buffer; empty when the request didn't carry a
    /// `correlation_id` field. Capped at MAX_CORRELATION_ID_BYTES; the
    /// parser rejects oversized + charset-violating values at the gate.
    correlation_id_buf: [MAX_CORRELATION_ID_BYTES]u8 = [_]u8{0} ** MAX_CORRELATION_ID_BYTES,
    correlation_id_len: u8 = 0,

    pub fn correlationId(self: *const Parsed) []const u8 {
        return self.correlation_id_buf[0..self.correlation_id_len];
    }
};

// ---------------------------------------------------------------------------
// Typed extractors. Each returns `Err.BadRequest` on any deviation from
// "field is present, correctly typed, within bounds." NEVER unwraps a
// `std.json.Value` directly — switch-on-tag in every access.
// ---------------------------------------------------------------------------

fn getObject(v: std.json.Value) Err!std.json.ObjectMap {
    return switch (v) {
        .object => |o| o,
        else => Err.BadRequest,
    };
}

fn getString(obj: std.json.ObjectMap, key: []const u8, max_len: usize) Err![]const u8 {
    const v = obj.get(key) orelse return Err.BadRequest;
    return switch (v) {
        .string => |s| if (s.len > 0 and s.len <= max_len) s else Err.BadRequest,
        else => Err.BadRequest,
    };
}

fn getInt(obj: std.json.ObjectMap, key: []const u8, min: i64, max: i64) Err!i64 {
    const v = obj.get(key) orelse return Err.BadRequest;
    return switch (v) {
        .integer => |n| if (n >= min and n <= max) n else Err.BadRequest,
        else => Err.BadRequest,
    };
}

/// Optional integer extractor — missing key returns the default; present but
/// wrong type / out-of-range is still `BadRequest`. The deadline lives here.
fn getOptionalInt(
    obj: std.json.ObjectMap,
    key: []const u8,
    min: i64,
    max: i64,
    default: i64,
) Err!i64 {
    const v = obj.get(key) orelse return default;
    return switch (v) {
        .integer => |n| if (n >= min and n <= max) n else Err.BadRequest,
        else => Err.BadRequest,
    };
}

/// Optional boolean: returns `default` if the key isn't present,
/// otherwise REQUIRES a real bool (a string "true" / "false" or an
/// int 0/1 is BadRequest at the gate — typed extractor discipline).
fn getOptionalBool(obj: std.json.ObjectMap, key: []const u8, default: bool) Err!bool {
    const v = obj.get(key) orelse return default;
    return switch (v) {
        .bool => |b| b,
        else => Err.BadRequest,
    };
}

fn parseMode(s: []const u8) Err!ClaimMode {
    if (std.mem.eql(u8, s, "observe")) return .observe;
    if (std.mem.eql(u8, s, "shared_rw")) return .shared_rw;
    if (std.mem.eql(u8, s, "exclusive")) return .exclusive;
    return Err.BadRequest;
}

fn parseOp(s: []const u8) Err!Op {
    if (std.mem.eql(u8, s, "list")) return .list;
    if (std.mem.eql(u8, s, "claim")) return .claim;
    if (std.mem.eql(u8, s, "release")) return .release;
    if (std.mem.eql(u8, s, "raw")) return .raw;
    if (std.mem.eql(u8, s, "inspect")) return .inspect;
    if (std.mem.eql(u8, s, "capabilities")) return .capabilities;
    if (std.mem.eql(u8, s, "drain_events")) return .drain_events;
    if (std.mem.eql(u8, s, "control")) return .control;
    if (std.mem.eql(u8, s, "list_leases")) return .list_leases;
    if (std.mem.eql(u8, s, "dump_all")) return .dump_all;
    if (std.mem.eql(u8, s, "inspect_full")) return .inspect_full;
    if (std.mem.eql(u8, s, "dump_chain")) return .dump_chain;
    if (std.mem.eql(u8, s, "subscribe")) return .subscribe;
    if (std.mem.eql(u8, s, "unsubscribe")) return .unsubscribe;
    if (std.mem.eql(u8, s, "driver_info")) return .driver_info;
    if (std.mem.eql(u8, s, "audit_tail")) return .audit_tail;
    if (std.mem.eql(u8, s, "state_snapshot")) return .state_snapshot;
    if (std.mem.eql(u8, s, "revoke")) return .revoke;
    if (std.mem.eql(u8, s, "attenuate")) return .attenuate;
    if (std.mem.eql(u8, s, "recover")) return .recover;
    if (std.mem.eql(u8, s, "open_shm_channel")) return .open_shm_channel;
    if (std.mem.eql(u8, s, "drain_shm_channel")) return .drain_shm_channel;
    if (std.mem.eql(u8, s, "start_shm_drain")) return .start_shm_drain;
    if (std.mem.eql(u8, s, "reconcile")) return .reconcile;
    if (std.mem.eql(u8, s, "irq_scan")) return .irq_scan;
    return Err.BadRequest;
}

/// R-CAP-ATTEN-WIRE — parse a single `rights` array entry (a Right enum
/// name) into the `cap.Right`. Closed set at the gate; unknown names
/// reject with BadRequest (a client can't request a right that doesn't
/// exist, and the typo can't silently widen/narrow).
fn parseRightName(s: []const u8) Err!cap.Right {
    if (std.mem.eql(u8, s, "observe")) return .observe;
    if (std.mem.eql(u8, s, "claim_shared")) return .claim_shared;
    if (std.mem.eql(u8, s, "claim_exclusive")) return .claim_exclusive;
    if (std.mem.eql(u8, s, "steal")) return .steal;
    if (std.mem.eql(u8, s, "reserve")) return .reserve;
    if (std.mem.eql(u8, s, "raw")) return .raw;
    if (std.mem.eql(u8, s, "grant")) return .grant;
    if (std.mem.eql(u8, s, "detach_driver")) return .detach_driver;
    if (std.mem.eql(u8, s, "audit_read")) return .audit_read;
    return Err.BadRequest;
}

/// Part 13 §5.5.1 — parses the `depth` enum from its wire string.
/// Closed enum at the gate; unknown values reject with BadRequest.
fn parseInspectDepth(s: []const u8) Err!model.InspectDepth {
    if (std.mem.eql(u8, s, "shallow")) return .shallow;
    if (std.mem.eql(u8, s, "all")) return .all;
    return Err.BadRequest;
}

/// Extract the optional `cap_id` field from a request object.
/// Defaults to 0 ("no cap presented") when absent. Reuses the
/// same parse-and-validate rules as claim/raw/control: integer
/// only, non-negative; non-integer + negative reject with
/// BadRequest at the gate.
fn parseCapId(obj: std.json.ObjectMap) Err!u64 {
    const v = obj.get("cap_id") orelse return 0;
    return switch (v) {
        .integer => |i| if (i < 0) Err.BadRequest else @intCast(i),
        else => Err.BadRequest,
    };
}

fn parseControlVerb(s: []const u8) Err!model.ControlVerb {
    if (std.mem.eql(u8, s, "set_authorized")) return .set_authorized;
    if (std.mem.eql(u8, s, "set_autosuspend")) return .set_autosuspend;
    if (std.mem.eql(u8, s, "set_port_power")) return .set_port_power;
    if (std.mem.eql(u8, s, "bind_driver")) return .bind_driver;
    if (std.mem.eql(u8, s, "unbind_driver")) return .unbind_driver;
    return Err.BadRequest;
}

// ---------------------------------------------------------------------------
// The gate itself.
//
// `parse(line, alloc)` is the ONLY function in this module callers use. It
// returns either a fully validated `Request` or `Err.BadRequest`. The caller
// owns nothing afterward: all strings have been copied into the `Request`'s
// fixed-size buffers (via `Urn.from`), so the parsed JSON's allocator-owned
// memory is released before this function returns.
// ---------------------------------------------------------------------------

pub fn parse(line: []const u8, alloc: std.mem.Allocator) Err!Parsed {
    // Bound input size at the gate (S5). A 4 GB request line cannot reach the
    // JSON parser.
    if (line.len == 0 or line.len > MAX_LINE_BYTES) return Err.BadRequest;

    var parsed = std.json.parseFromSlice(std.json.Value, alloc, line, .{}) catch
        return Err.BadRequest;
    defer parsed.deinit();

    const obj = try getObject(parsed.value);

    // Envelope: optional correlation_id (Part 13 §5.6.3). Extracted here
    // alongside the typed Request so a downstream splice can echo it
    // into the response. Charset-restricted so the splice doesn't need
    // to JSON-escape; oversized or out-of-charset values fail at the gate.
    var result = Parsed{ .req = undefined };
    if (obj.get("correlation_id")) |v| {
        switch (v) {
            .string => |s| {
                if (s.len == 0 or s.len > MAX_CORRELATION_ID_BYTES) return Err.BadRequest;
                for (s) |c| if (!isValidCorrelationIdChar(c)) return Err.BadRequest;
                @memcpy(result.correlation_id_buf[0..s.len], s);
                result.correlation_id_len = @intCast(s.len);
            },
            else => return Err.BadRequest,
        }
    }

    const op_str = try getString(obj, "op", MAX_VERB_BYTES);
    const op = try parseOp(op_str);

    result.req = switch (op) {
        .list => blk: {
            const scope = try getString(obj, "scope", MAX_URN_BYTES);
            // R-T1-11A — optional cap_id for capability-filtered
            // enumeration. Default 0 ⇒ legacy "no cap" path.
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .list = .{
                .scope = Urn.from(scope),
                .cap_id = @intCast(cap_id),
            } };
        },
        .claim => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const mode_str = try getString(obj, "mode", MAX_VERB_BYTES);
            const mode = try parseMode(mode_str);
            const holder = try getInt(obj, "holder", 1, std.math.maxInt(u32));
            const deadline_ms = try getOptionalInt(obj, "deadline_ms", 0, MAX_DEADLINE_MS, 0);
            const detach = try getOptionalBool(obj, "detach", false);
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .claim = .{
                .urn = Urn.from(urn),
                .mode = mode,
                .holder = @intCast(holder),
                .deadline_ms = @intCast(deadline_ms),
                .detach = detach,
                .cap_id = @intCast(cap_id),
            } };
        },
        .release => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const lease = try getInt(obj, "lease_id", 1, std.math.maxInt(u32));
            const deadline_ms = try getOptionalInt(obj, "deadline_ms", 0, MAX_DEADLINE_MS, 0);
            break :blk .{ .release = .{
                .urn = Urn.from(urn),
                .lease = @intCast(lease),
                .deadline_ms = @intCast(deadline_ms),
            } };
        },
        .raw => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const lease = try getInt(obj, "lease_id", 1, std.math.maxInt(u32));
            const caller = try getInt(obj, "caller", 1, std.math.maxInt(u32));
            const deadline_ms = try getOptionalInt(obj, "deadline_ms", 0, MAX_DEADLINE_MS, 0);
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .raw = .{
                .urn = Urn.from(urn),
                .lease = @intCast(lease),
                .caller = @intCast(caller),
                .deadline_ms = @intCast(deadline_ms),
                .cap_id = @intCast(cap_id),
            } };
        },
        .inspect => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            // R-T1-11D — optional cap_id for capability-filtered
            // enumeration. Default 0 ⇒ legacy "no cap" path.
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .inspect = .{
                .urn = Urn.from(urn),
                .cap_id = @intCast(cap_id),
            } };
        },
        .capabilities => .{ .capabilities = .{} },
        .drain_events => blk: {
            const scope = try getString(obj, "scope", MAX_URN_BYTES);
            var req = DrainEventsReq{ .scope = Urn.from(scope) };
            // Optional `subscription_id` (Part 13 §5.4.2). When set,
            // the dispatcher drains from the subscription's queue.
            if (obj.get("subscription_id")) |v| switch (v) {
                .string => |s| {
                    if (s.len == 0 or s.len > MAX_SUBSCRIPTION_ID_BYTES) {
                        return Err.BadRequest;
                    }
                    @memcpy(req.sub_id_buf[0..s.len], s);
                    req.sub_id_len = @intCast(s.len);
                },
                else => return Err.BadRequest,
            };
            break :blk .{ .drain_events = req };
        },
        .control => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const verb_str = try getString(obj, "verb", MAX_VERB_BYTES);
            const verb = try parseControlVerb(verb_str);
            const val = try getString(obj, "value", MAX_CONTROL_VALUE_BYTES);
            // Optional cap_id; defaults to 0 (= "no cap presented")
            // when omitted. Part 13 §5.1.2 / 4B uses it as the
            // writer-identity on the per-(urn,verb) serialiser.
            const cap_id: u64 = if (obj.get("cap_id")) |v| switch (v) {
                .integer => |i| if (i < 0) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            var req = ControlReq{
                .urn = Urn.from(urn),
                .verb = verb,
                .value_len = @intCast(val.len),
                .cap_id = cap_id,
            };
            @memcpy(req.value_buf[0..val.len], val);
            break :blk .{ .control = req };
        },
        .list_leases => blk: {
            const scope = try getString(obj, "scope", MAX_URN_BYTES);
            // R-T1-11B — optional cap_id for capability-filtered
            // enumeration. Default 0 ⇒ legacy "no cap" path.
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .list_leases = .{
                .scope = Urn.from(scope),
                .cap_id = @intCast(cap_id),
            } };
        },
        .dump_all => blk: {
            const scope = try getString(obj, "scope", MAX_URN_BYTES);
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .dump_all = .{
                .scope = Urn.from(scope),
                .cap_id = @intCast(cap_id),
            } };
        },
        .inspect_full => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            // `depth` is optional (default "all" per §5.5.1).
            const depth: model.InspectDepth = if (obj.get("depth")) |v| switch (v) {
                .string => |s| try parseInspectDepth(s),
                else => return Err.BadRequest,
            } else .all;
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .inspect_full = .{
                .urn = Urn.from(urn),
                .depth = depth,
                .cap_id = @intCast(cap_id),
            } };
        },
        .dump_chain => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const cap_id = try getOptionalInt(obj, "cap_id", 0, std.math.maxInt(i64), 0);
            break :blk .{ .dump_chain = .{
                .urn = Urn.from(urn),
                .cap_id = @intCast(cap_id),
            } };
        },
        .subscribe => blk: {
            const id_str = try getString(obj, "id", MAX_SUBSCRIPTION_ID_BYTES);
            if (id_str.len == 0) return Err.BadRequest;
            const scope_str = try getString(obj, "scope", MAX_URN_BYTES);
            var req = SubscribeReq{ .scope = Urn.from(scope_str) };
            @memcpy(req.id_buf[0..id_str.len], id_str);
            req.id_len = @intCast(id_str.len);
            // Validate `predicate` field shape at the gate when
            // present (must be a JSON object); the formatter
            // re-parses for the AST. Keeps the request struct
            // allocation-free.
            if (obj.get("predicate")) |v| {
                switch (v) {
                    .object => {},
                    else => return Err.BadRequest,
                }
                req.has_predicate = true;
            }
            break :blk .{ .subscribe = req };
        },
        .unsubscribe => blk: {
            const id_str = try getString(obj, "id", MAX_SUBSCRIPTION_ID_BYTES);
            if (id_str.len == 0) return Err.BadRequest;
            var req = UnsubscribeReq{};
            @memcpy(req.id_buf[0..id_str.len], id_str);
            req.id_len = @intCast(id_str.len);
            break :blk .{ .unsubscribe = req };
        },
        .audit_tail => blk: {
            // Optional `n` (default 100; clamped at the gate to
            // MAX_AUDIT_TAIL_N so a hostile request can't ask for
            // an unreasonable response size).
            var req = AuditTailReq{};
            if (obj.get("n")) |v| switch (v) {
                .integer => |i| {
                    if (i < 0) return Err.BadRequest;
                    const ui: u64 = @intCast(i);
                    if (ui > MAX_AUDIT_TAIL_N) return Err.BadRequest;
                    req.n = @intCast(ui);
                },
                else => return Err.BadRequest,
            };
            req.cap_id = try parseCapId(obj);
            break :blk .{ .audit_tail = req };
        },
        .state_snapshot => .{ .state_snapshot = .{ .cap_id = try parseCapId(obj) } },
        .revoke => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const deadline_ms: u64 = if (obj.get("deadline_ms")) |v| switch (v) {
                .integer => |i| if (i < 0) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .revoke = .{
                .urn = Urn.from(urn),
                .cap_id = try parseCapId(obj),
                .deadline_ms = deadline_ms,
            } };
        },
        .driver_info => blk: {
            const name = try getString(obj, "name", MAX_DRIVER_INFO_NAME_BYTES);
            if (name.len == 0) return Err.BadRequest;
            // Linux module / driver names are conventionally ASCII
            // [A-Za-z0-9_-]; the kernel rejects path-separators and
            // NULs at modprobe. We enforce the safer subset here so
            // a maliciously-shaped name can't walk out of the
            // /sys/module/<name> path-build at the syscall layer.
            for (name) |c| {
                const ok = (c >= 'a' and c <= 'z') or
                    (c >= 'A' and c <= 'Z') or
                    (c >= '0' and c <= '9') or
                    c == '_' or c == '-';
                if (!ok) return Err.BadRequest;
            }
            var req = DriverInfoReq{};
            @memcpy(req.name_buf[0..name.len], name);
            req.name_len = @intCast(name.len);
            break :blk .{ .driver_info = req };
        },
        .attenuate => blk: {
            // Parent cap is required + must be non-zero (id 0 is the
            // "no cap" sentinel — there's nothing to attenuate from).
            const parent = try parseCapId(obj);
            if (parent == 0) return Err.BadRequest;
            var req = AttenuateReq{ .cap_id = parent };
            // Optional scope narrowing.
            if (obj.get("scope")) |v| {
                const s = switch (v) {
                    .string => |str| str,
                    else => return Err.BadRequest,
                };
                if (s.len == 0 or s.len > MAX_URN_BYTES) return Err.BadRequest;
                @memcpy(req.scope_buf[0..s.len], s);
                req.scope_len = @intCast(s.len);
            }
            // Optional rights narrowing — an array of Right names. An
            // empty array is valid (narrow to no rights). Each entry is
            // a closed-set name; anything else rejects at the gate.
            if (obj.get("rights")) |v| {
                const arr = switch (v) {
                    .array => |a| a,
                    else => return Err.BadRequest,
                };
                var rs = cap.RightSet{};
                for (arr.items) |item| {
                    const name = switch (item) {
                        .string => |str| str,
                        else => return Err.BadRequest,
                    };
                    rs.bits |= (try parseRightName(name)).bit();
                }
                req.has_rights = true;
                req.rights = rs;
            }
            // Optional mode-ceiling narrowing.
            if (obj.get("mode_ceiling")) |v| {
                const s = switch (v) {
                    .string => |str| str,
                    else => return Err.BadRequest,
                };
                req.has_mode_ceiling = true;
                req.mode_ceiling = try parseMode(s);
            }
            break :blk .{ .attenuate = req };
        },
        .recover => blk: {
            const urn = try getString(obj, "urn", MAX_URN_BYTES);
            const deadline_ms: u64 = if (obj.get("deadline_ms")) |v| switch (v) {
                .integer => |i| if (i < 0) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .recover = .{
                .urn = Urn.from(urn),
                .cap_id = try parseCapId(obj),
                .deadline_ms = deadline_ms,
            } };
        },
        .open_shm_channel => .{ .open_shm_channel = .{ .cap_id = try parseCapId(obj) } },
        .drain_shm_channel => blk: {
            const channel: u32 = if (obj.get("channel")) |v| switch (v) {
                .integer => |i| if (i < 0 or i > std.math.maxInt(u32)) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .drain_shm_channel = .{ .channel = channel, .cap_id = try parseCapId(obj) } };
        },
        .start_shm_drain => blk: {
            const channel: u32 = if (obj.get("channel")) |v| switch (v) {
                .integer => |i| if (i < 0 or i > std.math.maxInt(u32)) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .start_shm_drain = .{ .channel = channel, .cap_id = try parseCapId(obj) } };
        },
        .reconcile => blk: {
            // `scope` is optional — absent means the whole hw:/** namespace.
            const scope: Urn = if (obj.get("scope")) |v| switch (v) {
                .string => |s| if (s.len > 0 and s.len <= MAX_URN_BYTES) Urn.from(s) else return Err.BadRequest,
                else => return Err.BadRequest,
            } else Urn.from("hw:/**");
            const deadline_ms: u64 = if (obj.get("deadline_ms")) |v| switch (v) {
                .integer => |i| if (i < 0) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .reconcile = .{
                .scope = scope,
                .cap_id = try parseCapId(obj),
                .deadline_ms = deadline_ms,
            } };
        },
        .irq_scan => blk: {
            const deadline_ms: u64 = if (obj.get("deadline_ms")) |v| switch (v) {
                .integer => |i| if (i < 0) return Err.BadRequest else @intCast(i),
                else => return Err.BadRequest,
            } else 0;
            break :blk .{ .irq_scan = .{ .cap_id = try parseCapId(obj), .deadline_ms = deadline_ms } };
        },
    };
    return result;
}
