//! mgf-hrb-core — the arbitration core (Part 3). The lease engine that owns the
//! one fact nothing else may own: who holds what, right now, truthfully, even
//! across failure. Depends on model + the ABSTRACT kabi seam (DIP) — never on a
//! concrete backend, never on hardware. Testable against the fake.
//!
//! DEADLINES (D-029 / Part 12 §2.2). Every method that touches the backend
//! takes a `Deadline` and propagates it. A grant whose hardware-acquire
//! step exceeds its deadline returns `Err.Timeout` from `claim`; no lease
//! is committed (D-012 atomicity holds — the timeout path is just another
//! way for hardware-acquire to fail, and the `errdefer`-shaped logic leaves
//! no residue). For the upward boundary (a dead holder), the deadline on
//! the internal release-during-reclamation is `Deadline.none` — we accept
//! whatever wait the backend needs to safe-reset; the holder is already
//! gone, so latency to the caller doesn't matter.

const std = @import("std");
const QuarantineReason = @import("mgf_hrb_model").QuarantineReason;

/// Per-URN quarantine record (Part 2.7.1 / Part 12 §2.4). The Core keeps
/// a small list of currently-quarantined resources; entries are added
/// on Err.Timeout from a backend acquire and cleared by a verified
/// clean reset. A `claim` against a quarantined URN returns
/// `Err.Unavailable` honestly rather than re-trying the broken hardware.
const Quarantined = struct {
    urn: @import("mgf_hrb_model").Urn,
    reason: QuarantineReason,
    since_ms: u64,
};

/// Outcome of the reset-escalation ladder (`recoverByReset`, Tier-1 #12).
pub const RecoveryOutcome = enum {
    /// The URN wasn't quarantined — recovery is a no-op (a healthy
    /// resource in service is never force-reset).
    not_quarantined,
    /// Function-level reset (FLR) succeeded + verified — back to Free.
    recovered_flr,
    /// FLR failed; the bus-level rung succeeded + verified — back to Free.
    recovered_bus,
    /// Every rung failed; the resource stays quarantined (escalated to
    /// `reset_unverified`) for a later policy pass / operator action.
    still_quarantined,
};
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const cap = @import("mgf_hrb_cap");
const fairness_mod = @import("mgf_hrb_fairness");

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

/// G7 (review wave 2) — upper bound on the `revoked` lease-id set.
///
/// `Core.revoked` is the "this lease id was preempted" disclosure
/// log: when a holder calls `raw()`/`release()` against an id that
/// is no longer in the live lease table, the engine checks
/// `revoked` to disambiguate `Err.Revoked` (taken from you) from
/// `Err.InvalidToken` (never existed / typo'd by client).
///
/// The set was unbounded pre-G7, which under sustained churn
/// (CI loops doing claim+revoke; pathological client patterns; a
/// long-uptime broker with periodic policy preemption) grew until
/// OOM. The cap closes that leak with graceful degradation: when
/// a new revoke would overflow, we FIFO-evict the OLDEST id.
/// That id's `Err.Revoked` window closes — subsequent raw/release
/// against it returns `Err.InvalidToken` instead. Honest, just
/// less specific. Newer revocations keep their distinct signal.
///
/// 256 is sized comfortably above any realistic live-lease count
/// per Core (typical: dozens). A bigger value here costs ~8 KB
/// of broker memory (u64 × 256 × 4 workers ≈ 8 KB total); a
/// smaller value risks the eviction edge firing on normal load.
/// Sized with comfortable headroom above `MAX_LEASES` (the live-lease
/// ceiling below) so the FIFO eviction edge stays rare even under
/// sustained claim+revoke churn.
const MAX_REVOKED_IDS: usize = 256;

/// S5 / Tier-1 #9 — aggregate live-lease ceiling PER Core (per bus
/// worker). The lease table is the broker's ownership ledger; without
/// a count bound a single flood of non-conflicting (observe-mode)
/// claims grows it toward OOM (U8/I5). At the ceiling, `claimImpl`
/// sheds new claims with `Err.Overloaded` — back-pressure, not
/// unbounded growth (the S5 discipline). Per-Core, because each bus
/// worker owns a separate table and the write path is already sharded
/// by bus; the aggregate broker bound is `MAX_LEASES × worker count`.
pub const MAX_LEASES: usize = 128;

/// S5 / Tier-1 #9 — per-tenant live-lease ceiling, keyed on the
/// SO_PEERCRED uid (unforgeable, kernel-attested — NOT the client-
/// supplied `holder` id, which a hostile client can vary at will).
/// Bounds how much of one Core's table any single tenant can occupy,
/// so no tenant can starve others out of the ownership ledger even
/// below the aggregate ceiling. Must stay below `MAX_LEASES` so a
/// capped tenant never blocks a second tenant's claims. Only enforced
/// when a peer uid is present; in-process / test claims (null peer)
/// are bounded by the aggregate ceiling alone.
pub const MAX_LEASES_PER_UID: usize = 64;

/// A lease — the atom of ownership (Part 3.2). Exists only while its holder is
/// provably alive. Not a lock you keep; a continuously-renewed truth.
pub const Lease = struct {
    id: LeaseId,
    urn: Urn,
    holder: ClientId,
    mode: ClaimMode,
    token: kabi.AcquireToken, // the backend acquisition this lease wraps
    /// Monotonic ms at commit time (Part 5.5 / `kabi.nowMs`). Used by
    /// `listLeases` for the GUI's "since when" column (FEEDBACK SDK-03);
    /// also useful internally for lease-age policy (preemption, stale-
    /// holder detection). Set once at commit; never updated.
    granted_at_ms: u64,
    /// S5 / Tier-1 #9 — the SO_PEERCRED uid of the connection that made
    /// this claim, used to enforce `MAX_LEASES_PER_UID`. `null` for
    /// in-process / test claims that carry no connection identity (those
    /// are bounded by the aggregate ceiling alone). Kernel-attested, so
    /// the per-tenant bound can't be evaded by varying the client-chosen
    /// `holder` id.
    peer_uid: ?u32 = null,
};

/// One row in the result of `Core.listLeases` — a flat, copyable view
/// of a live lease. The internal `Lease` struct carries the backend
/// `token` which is broker-private; this projection drops it and keeps
/// only the consumer-facing fields. Fixed-shape so callers can collect
/// without allocation.
pub const LeaseRecord = struct {
    urn: Urn,
    lease_id: LeaseId,
    holder: ClientId,
    mode: ClaimMode,
    granted_at_ms: u64,
};

/// Push-style sink for `listLeases` — same shape as `FactSink` /
/// `EventSink`. Lets the facade fan out across workers and merge
/// into one collector without intermediate allocation.
pub const LeaseSink = struct {
    ctx: *anyopaque,
    push: *const fn (ctx: *anyopaque, record: LeaseRecord) void,
};

/// The claim-mode lattice compatibility check (Part 3.3). Family-A only
/// (Part 10: Family B uses reservation strength instead).
/// Returns true if `requested` may be granted given an existing `held` mode.
fn compatible(held: ClaimMode, requested: ClaimMode) bool {
    return switch (requested) {
        // Observe never conflicts with anything — unlimited concurrent readers.
        .observe => true,
        // Shared(rw) is compatible with observe and other shared, not exclusive.
        .shared_rw => held == .observe or held == .shared_rw,
        // Exclusive is compatible only with nothing-or-observers.
        .exclusive => held == .observe,
    };
}

/// Tier-1 #8 (S4 §4.4) — pure lease-table integrity check. The table is
/// the broker's single source of ownership truth; these invariants must
/// ALWAYS hold by construction of the lease engine, so a violation means
/// a deep bug or memory corruption — the safe response is a controlled
/// fault (quarantine + alert), never a silent bad grant. Returns the
/// INDEX of the first offending lease, or null when the table is sound.
///
/// Invariants:
///   1. every lease has a real id (≠ 0) and holder (≠ 0);
///   2. lease ids are unique;
///   3. no two MUTUALLY-INCOMPATIBLE leases share a URN — the
///      double-grant guard (e.g. two `exclusive` on one resource is the
///      worst possible reliability lie). Observe coexists with anything;
///      the `compatible` lattice defines the rest.
///
/// Pure (no allocation, no lock) so it's unit-testable over a hand-built
/// slice and cheap to run at a table transition.
pub fn verifyLeaseInvariants(leases: []const Lease) ?usize {
    for (leases, 0..) |l, i| {
        if (l.id == 0) return i;
        if (l.holder == 0) return i;
        for (leases[0..i]) |prev| {
            if (prev.id == l.id) return i; // duplicate lease id
            if (prev.urn.eql(&l.urn) and !compatible(prev.mode, l.mode)) return i;
        }
    }
    return null;
}

pub const Core = struct {
    backend: kabi.KernelBackend,
    leases: std.ArrayListUnmanaged(Lease) = .empty,
    /// Tier-0 #4 — lease ids that have been revoked (the broker
    /// forcibly ended them via `revoke()`). Subsequent `raw()` /
    /// `release()` on a revoked id return `Err.Revoked`. The set
    /// is append-only within a Core's lifetime; entries can be
    /// garbage-collected after a TTL (not v1).
    revoked: std.ArrayListUnmanaged(LeaseId) = .empty,
    /// Currently-quarantined URNs (Part 2.7.1). Small list; linear scan
    /// is fine for the hundreds-of-resources scale the broker targets.
    /// Entries land here on Err.Timeout from acquire; cleared by
    /// `clearQuarantine(urn)` after a verified clean reset.
    quarantined: std.ArrayListUnmanaged(Quarantined) = .empty,
    alloc: std.mem.Allocator,
    next_lease: LeaseId = 1,
    /// Tier-1 #10 (C2/C4) — anti-starvation contention policy. Consulted at the
    /// claim conflict + release seams so a contended resource is handed to its
    /// longest-waiting tenant (FIFO reservation), never indefinitely to a
    /// spammer. Touched only under `self.lock`.
    fairness: fairness_mod.Fairness,
    /// Optional capability table (Part 4). When present, claims must be
    /// authorized by a presented capability (DIP: the core depends on the
    /// verifier, not on how authority is represented). When null, the core is
    /// running unguarded (Phase-2 mode / trusted single-tenant).
    verifier: ?*cap.CapTable = null,
    /// Mutex guarding the lease + quarantine tables (Part 7.1 multi-
    /// client constellation). Every public method takes this lock at
    /// entry; held across the entire op (including the backend call
    /// during claim) so two threads claiming the same URN can't both
    /// see an empty lease list and both proceed to acquire. The
    /// lock is per-Core, so different workers (PCI, USB, …)
    /// parallelize naturally; serializing within one worker matches
    /// the kernel-side ioctl serialization anyway.
    lock: kabi.SpinMutex = .{},

    pub fn init(alloc: std.mem.Allocator, backend: kabi.KernelBackend) Core {
        return .{ .backend = backend, .alloc = alloc, .fairness = fairness_mod.Fairness.init(alloc) };
    }
    pub fn initWithCaps(alloc: std.mem.Allocator, backend: kabi.KernelBackend, verifier: *cap.CapTable) Core {
        return .{ .backend = backend, .alloc = alloc, .verifier = verifier, .fairness = fairness_mod.Fairness.init(alloc) };
    }
    pub fn deinit(self: *Core) void {
        self.leases.deinit(self.alloc);
        self.revoked.deinit(self.alloc);
        self.quarantined.deinit(self.alloc);
        self.fairness.deinit();
    }

    /// Tier-0 #4 — mark every lease for `urn` as revoked. The
    /// backend's claim teardown has already happened (Facade
    /// orders the steps); this method just updates the lease
    /// engine's view. Idempotent: if no lease exists for the
    /// URN, no-op.
    ///
    /// Future `raw()` / `release()` calls against a revoked
    /// lease return `Err.Revoked` — the typed signal the client
    /// learns its access was preempted, not just lost. The
    /// `Err.InvalidToken` path (lease never existed) remains
    /// distinct from `Err.Revoked` (lease existed, was taken).
    ///
    /// G7 (review wave 2) — bounds the `revoked` set. The
    /// pre-G7 code appended forever, so a long-running broker
    /// under sustained revoke churn (think: a CI loop or a
    /// hostile pattern of repeated claim+revoke) would grow
    /// `revoked` without limit until OOM. The eviction policy
    /// is FIFO: when at MAX_REVOKED_IDS, we drop the OLDEST id.
    /// Consequence: that id's `Err.Revoked` disclosure window
    /// closes — subsequent raw/release against it surfaces
    /// `Err.InvalidToken` instead. That's a *graceful*
    /// degradation: the holder still gets an honest error, just
    /// the less-specific one. Newer revocations keep their
    /// distinct `Err.Revoked` signal. 256 is sized for "every
    /// active lease the broker could realistically hold at once"
    /// with comfortable headroom — bigger than `MAX_LEASES` ×
    /// reasonable churn.
    pub fn markRevokedByUrn(self: *Core, urn: *const Urn) void {
        self.lock.lock();
        defer self.lock.unlock();
        // Walk leases backward so swapRemove doesn't shift indices
        // we're about to inspect.
        var i: usize = self.leases.items.len;
        while (i > 0) {
            i -= 1;
            if (self.leases.items[i].urn.eql(urn)) {
                const id = self.leases.items[i].id;
                // G7 — FIFO eviction at the cap. orderedRemove(0)
                // is O(N) but N ≤ 256; cost is bounded and we only
                // fire on the cap-hit edge (rare in any realistic
                // workload).
                if (self.revoked.items.len >= MAX_REVOKED_IDS) {
                    _ = self.revoked.orderedRemove(0);
                }
                self.revoked.append(self.alloc, id) catch {};
                _ = self.leases.swapRemove(i);
            }
        }
    }

    /// True iff `id` was revoked at some point in this Core's
    /// lifetime. Used by `raw()` / `release()` to return
    /// `Err.Revoked` instead of `Err.InvalidToken`.
    fn isRevoked(self: *Core, id: LeaseId) bool {
        for (self.revoked.items) |rid| if (rid == id) return true;
        return false;
    }

    /// Lookup helper. Returns the Quarantined record for `urn` or null.
    /// Used by the claim path (refuse) and by `inspect` (report).
    pub fn quarantineStatus(self: *Core, urn: *const Urn) ?Quarantined {
        self.lock.lock();
        defer self.lock.unlock();
        return self._quarantineStatusLocked(urn);
    }

    /// Internal version of `quarantineStatus` that assumes the caller
    /// already holds `self.lock`. Used by `claimImpl` so we don't
    /// reentrantly try to take the lock (Zig's `Mutex` is not
    /// reentrant — that would deadlock).
    fn _quarantineStatusLocked(self: *Core, urn: *const Urn) ?Quarantined {
        for (self.quarantined.items) |q| {
            if (q.urn.eql(urn)) return q;
        }
        return null;
    }

    /// Mark a URN quarantined. Idempotent: if it's already in the set,
    /// the existing record stays (preserves the original `since_ms`).
    /// Internal helper — callers MUST hold `self.lock`.
    fn markQuarantined(self: *Core, urn: *const Urn, reason: QuarantineReason) void {
        if (self._quarantineStatusLocked(urn) != null) return;
        self.quarantined.append(self.alloc, .{
            .urn = urn.*,
            .reason = reason,
            .since_ms = kabi.nowMs(),
        }) catch {}; // best effort — on OOM we just don't track; claim will retry
    }

    /// R-T1-8 residual (I3) — quarantine on a helper-fault from a device op.
    /// A helper-touching op (`raw`, `control`, …) that returns `Err.Timeout` did
    /// NOT complete cleanly: the Tier-0 #3 supervisor may have killed the
    /// privileged helper mid-op, so the device's state is UNKNOWN. Quarantine the
    /// URN (`op_timeout`) so it's never silently re-grantable until an operator
    /// recovers it via the #12 reset ladder — the same response the acquire path
    /// already gives a mid-acquire timeout. A clean error (e.g. `HardwareFault`,
    /// `Unsupported`) leaves the device in a KNOWN state and is NOT quarantined.
    /// Takes the lock itself (callers are post-backend-call, lock dropped).
    fn quarantineOnHelperFault(self: *Core, urn: *const Urn, err: Err) void {
        if (err != Err.Timeout) return;
        self.lock.lock();
        defer self.lock.unlock();
        self.markQuarantined(urn, .op_timeout);
    }

    /// Clear a URN from the quarantine set. Called after a verified
    /// clean reset (Part 2.7.1 recovery path). Returns true if the
    /// URN was quarantined; false if it wasn't (no-op).
    pub fn clearQuarantine(self: *Core, urn: *const Urn) bool {
        self.lock.lock();
        defer self.lock.unlock();
        for (self.quarantined.items, 0..) |q, i| {
            if (q.urn.eql(urn)) {
                _ = self.quarantined.swapRemove(i);
                return true;
            }
        }
        return false;
    }

    /// Tier-1 #12 — the reset-escalation ladder (survivability §2.5):
    /// `FLR → (verify) → bus reset → (verify) → quarantine`. Run to
    /// recover a quarantined resource back to the `Free` pool.
    ///
    /// Each rung is deadline-bounded and VERIFIED BY THE BACKEND — e.g.
    /// the USB backend's `verifyResetSucceeded` post-condition (the
    /// device-lie guard, D6); a backend that can't verify returns an
    /// error, which the ladder treats as a failed rung and escalates.
    /// The device can never block the broker: each rung is abandonable
    /// (the deadline bounds it), and the ladder ends in `Free` (recovered)
    /// or `Quarantined` (escalated) — never in a hang.
    ///
    /// Recovery acts ONLY on a quarantined resource — a healthy device in
    /// service is never force-reset (a non-quarantined URN returns
    /// `.not_quarantined`, a no-op). A quarantined URN has no live lease by
    /// construction (quarantine reclaims the holder + refuses new claims),
    /// so there's no holder to disrupt. Backend resets run OUTSIDE
    /// `self.lock` (they may block on a privileged-helper IPC, same A4/M3
    /// discipline as acquire).
    pub fn recoverByReset(self: *Core, urn: *const Urn, deadline: Deadline) RecoveryOutcome {
        if (self.quarantineStatus(urn) == null) return .not_quarantined;

        // Rung 1 — function-level reset (the common case; cheapest).
        if (self.backend.reset(urn, .function_level, deadline)) |_| {
            _ = self.clearQuarantine(urn);
            return .recovered_flr;
        } else |_| {}

        // Rung 2 — escalate to bus-level reset.
        if (self.backend.reset(urn, .bus_level, deadline)) |_| {
            _ = self.clearQuarantine(urn);
            return .recovered_bus;
        } else |_| {}

        // Both rungs failed → stay quarantined, escalating the reason to
        // `reset_unverified` so the record reflects "FLR+bus tried, device
        // still not provably clean" (operator-actionable) rather than the
        // original trigger. Clear-then-mark updates the reason + stamps the
        // recovery-attempt time.
        _ = self.clearQuarantine(urn);
        self.lock.lock();
        self.markQuarantined(urn, .reset_unverified);
        self.lock.unlock();
        return .still_quarantined;
    }

    /// Tier-1 #8 (S4 §4.4) — run the lease-table integrity check; on a
    /// violation, quarantine the offending resource (`fault_maybe_dirty`)
    /// and alert loudly rather than continue with a corrupt ownership
    /// table. Returns the violating URN, or null when the table is sound.
    /// A controlled fault, never a silent bad grant. Callable as a
    /// safety-net audit (run at major transitions, e.g. after
    /// `holderDied`); in correct operation it never fires.
    pub fn auditInvariants(self: *Core) ?Urn {
        self.lock.lock();
        defer self.lock.unlock();
        return self.auditInvariantsLocked();
    }

    /// `auditInvariants` assuming `self.lock` is already held.
    fn auditInvariantsLocked(self: *Core) ?Urn {
        const idx = verifyLeaseInvariants(self.leases.items) orelse return null;
        const urn = self.leases.items[idx].urn;
        self.markQuarantined(&urn, .fault_maybe_dirty);
        std.debug.print(
            "error: core lease-table invariant violated on {s} (lease idx {d}) — " ++
                "quarantining (S4 controlled fault, not a silent bad grant)\n",
            .{ urn.slice(), idx },
        );
        return urn;
    }

    /// Tier-1 #8 (S4 §4.3) — re-derivation recovery contract, as a
    /// callable. After a worker/helper/broker restart the broker must
    /// re-derive truth from TWO authoritative sources — the hardware
    /// (what's actually bound) and the live clients (who holds a lease) —
    /// never from lost in-memory state. Anything bound in hardware but
    /// unclaimed by any live client is `Quarantined`
    /// (`unclaimed_after_restart`), then safe-reset + freed via the
    /// recovery ladder — never assumed free (may be dirty), never assumed
    /// owned (the owner is gone). This makes restart crash-consistent.
    ///
    /// `bound_urns` is the hardware-derived set the caller supplies (from
    /// the backend's enumeration on restart); the live-lease set is this
    /// Core's table. Returns the count newly quarantined. The restart
    /// PLUMBING (who calls this, with what enumeration, on which restart
    /// signal) is future — this formalises the CONTRACT as a tested
    /// mechanism so the recovery is a callable, not just prose.
    pub fn reconcileAfterRestart(self: *Core, bound_urns: []const Urn) usize {
        self.lock.lock();
        defer self.lock.unlock();
        var quarantined: usize = 0;
        for (bound_urns) |*burn| {
            var has_live_lease = false;
            for (self.leases.items) |l| {
                if (l.urn.eql(burn)) {
                    has_live_lease = true;
                    break;
                }
            }
            if (has_live_lease) continue; // owned by a live client — leave it
            if (self._quarantineStatusLocked(burn) != null) continue; // already quarantined
            // Bound in hardware, unclaimed by any live client → quarantine.
            self.markQuarantined(burn, .unclaimed_after_restart);
            quarantined += 1;
        }
        return quarantined;
    }

    /// D7 (survivability §2.7) interrupt-rate containment — tear a flooding
    /// device's grant down and quarantine it. Called when the interrupt-rate
    /// monitor judges a granted device to be abusing the host with a runaway
    /// IRQ rate. The shape mirrors `Facade.revoke` (revoke BELOW the fd — the
    /// IOMMU mapping, not just the descriptor — so a held fd faults on next use)
    /// but additionally QUARANTINES: a flooding device is in an uncertain/unsafe
    /// state and must not be silently re-grantable until an operator recovers it
    /// via the #12 reset-escalation ladder (`recoverByReset`). `fault_maybe_dirty`
    /// is the reuse-existing reason that fits (a fault left the device unsafe).
    ///
    /// `backend.revoke` runs with the lock DROPPED (it may block on the
    /// privileged-helper IPC — same A4/M3 discipline as `claim`); the lease drop
    /// + quarantine then run under the lock.
    pub fn containByFlooding(self: *Core, urn: *const Urn, deadline: Deadline) Err!void {
        try self.backend.revoke(urn, deadline); // tear-down first, lock dropped
        self.markRevokedByUrn(urn); // locks internally — drop every lease on it
        self.lock.lock();
        defer self.lock.unlock();
        self.markQuarantined(urn, .fault_maybe_dirty);
    }

    /// Tier-1 #10 §5.4 — execute any graceful holder-preemptions whose grace
    /// window has expired. For each, if the marked holder is STILL holding (it
    /// didn't release within grace — a release would have cancelled it via
    /// `onRelease`), force the resource away from it: revoke BELOW the fd (the
    /// §3.4 teardown — `backend.revoke` renders the holder's delegated fd inert,
    /// then `markRevokedByUrn` drops the lease so its later `raw`/`release`
    /// returns `Err.Revoked`, not a silent loss), and leave the resource FREE and
    /// reserved for the higher-priority waiter. UNLIKE `containByFlooding` it does
    /// NOT quarantine — preemption is policy, not fault, so the resource stays
    /// immediately re-grantable (by the waiter the reservation favours). Returns
    /// the number of holds actually torn down.
    ///
    /// Operator/loop-driven (no implicit broker timer — same posture as
    /// `reconcileAfterRestart`); a wire trigger is the deferred follow-on.
    /// `backend.revoke` runs with `Core.lock` DROPPED (it may block on the
    /// privileged-helper IPC — the same A4/M3 discipline as `claim`).
    pub fn runDuePreemptions(self: *Core, now_ms: u64, deadline: Deadline) usize {
        var torn_down: usize = 0;
        while (true) {
            // Take one due preemption AND confirm the marked holder still holds —
            // both under the lock, so a holder that released in between (its
            // preemption already cancelled) is never torn down, and a DIFFERENT
            // tenant that has since claimed the URN is never preempted by mistake.
            self.lock.lock();
            const due = self.fairness.takeDuePreemption(now_ms) orelse {
                self.lock.unlock();
                break;
            };
            const still_held = blk: {
                var held: [16]*Lease = undefined;
                const n = self.leasesOn(&due.urn, &held);
                for (held[0..n]) |l| {
                    if (l.token.id == 0) continue; // placeholder — not a committed holder
                    if (l.peer_uid) |hu| {
                        if (hu == due.holder_uid) break :blk true;
                    }
                }
                break :blk false;
            };
            self.lock.unlock();
            if (!still_held) continue; // holder gone or replaced — nothing to preempt

            // Forced teardown — lock dropped. If the backend can't revoke, leave
            // the lease intact (never claim a teardown that didn't happen, G2).
            self.backend.revoke(&due.urn, deadline) catch continue;
            self.markRevokedByUrn(&due.urn); // locks internally — drops the lease(s)
            // The resource is now free → reserve it for the higher-priority waiter
            // (still queued from its denied claim) so a spammer can't grab it first.
            self.lock.lock();
            self.fairness.onRelease(&due.urn, now_ms);
            self.lock.unlock();
            torn_down += 1;
        }
        return torn_down;
    }

    /// All live leases on a given resource.
    fn leasesOn(self: *Core, urn: *const Urn, out: *[16]*Lease) usize {
        var n: usize = 0;
        for (self.leases.items) |*l| {
            if (l.urn.eql(urn) and n < out.len) {
                out[n] = l;
                n += 1;
            }
        }
        return n;
    }

    /// Capability-checked claim (Part 3.4 step 1 + Part 4). Verifies the
    /// presented capability permits this claim BEFORE any lattice check or
    /// hardware touch.
    ///
    /// `peer` is the connection identity (SO_PEERCRED) when the caller has
    /// one: a non-null peer additionally re-checks the cap's `bound_to`
    /// binding here (Part 12 §3.3 Tier-0 #2), so an attacker who reached
    /// this layer with the wrong peer still fails. `null` is the legacy
    /// in-process path — no connection identity to bind against.
    pub fn claimWithCap(
        self: *Core,
        cap_id: u64,
        peer: ?cap.PeerCred,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const v = self.verifier orelse return Err.NotPermitted; // caps required but none configured
        const ok = if (peer) |p|
            v.permitsClaimFromPeer(cap_id, urn, mode, p)
        else
            v.permitsClaim(cap_id, urn, mode);
        if (!ok) return Err.NotPermitted;
        return self.claimImpl(urn, mode, holder, deadline, if (peer) |p| p.uid else null, false);
    }

    /// Capability-checked claim WITH auto-detach (USB-worker §5.10).
    /// Two layers of authorization required: the base claim right AND
    /// the `detach_driver` right (the broker never silently steals
    /// devices — only authorized clients can take an interface away
    /// from a bound kernel driver). Backend must support the
    /// `acquire_detach` vtable slot or this returns `Err.Unsupported`.
    /// `peer` carries the same optional binding-recheck semantics as
    /// `claimWithCap` (non-null ⇒ check the cap's `bound_to`).
    pub fn claimDetachWithCap(
        self: *Core,
        cap_id: u64,
        peer: ?cap.PeerCred,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const v = self.verifier orelse return Err.NotPermitted;
        const claim_ok = if (peer) |p|
            v.permitsClaimFromPeer(cap_id, urn, mode, p)
        else
            v.permitsClaim(cap_id, urn, mode);
        if (!claim_ok) return Err.NotPermitted;
        const detach_ok = if (peer) |p|
            v.permitsDetachFromPeer(cap_id, urn, p)
        else
            v.permitsDetach(cap_id, urn);
        if (!detach_ok) return Err.NotPermitted;
        return self.claimImpl(urn, mode, holder, deadline, if (peer) |p| p.uid else null, true);
    }

    /// Non-cap-checked claim with detach. Same atomic-grant dance as
    /// `claim`, just calls `backend.acquireDetach` instead of
    /// `backend.acquire`. Useful for tests / future facades that do
    /// their own authorization.
    pub fn claimDetach(
        self: *Core,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        return self.claimImpl(urn, mode, holder, deadline, null, true);
    }

    /// Capability-checked raw-channel grant (Part 3.8 + Part 4.3 `raw` right).
    /// Requires BOTH lease ownership AND the `raw` right — two different trust
    /// levels (controlling a device ≠ poking its registers directly).
    ///
    /// Locking: validate under `lock`, then DROP the lock before calling
    /// `backend.rawChannel`. The backend op may block on a privileged
    /// helper IPC roundtrip (up to ~5s on a wedged helper); holding
    /// `Core.lock` across that window would freeze every other op on
    /// this worker (claim, release, list_leases, …) for the same
    /// duration. The lease can in principle be revoked between the
    /// unlock and the backend call — that's fine: revoke is async
    /// by design, the backend will surface ENODEV/EPIPE on the
    /// underlying fd, and `Err.Revoked` is observable via the next
    /// `raw`/`release` once the table catches up. (See A4 in the
    /// 2026-05-26 review.)
    /// `peer` carries the same optional binding-recheck semantics as
    /// `claimWithCap` (non-null ⇒ verify the cap's `bound_to` against the
    /// connection peer; null ⇒ legacy in-process path).
    pub fn rawWithCap(
        self: *Core,
        cap_id: u64,
        peer: ?cap.PeerCred,
        id: LeaseId,
        caller: ClientId,
        deadline: Deadline,
    ) Err!kabi.RawHandle {
        const token = blk: {
            self.lock.lock();
            defer self.lock.unlock();
            const idx = self.findLease(id) orelse {
                // Tier-0 #4 parity with plain raw(): distinguish
                // "never existed" (InvalidToken) from "was
                // revoked" (Revoked). The lease engine moves
                // revoked ids into self.revoked on markRevokedByUrn;
                // surface that signal honestly through every
                // raw entry point, not just the unverifier path.
                if (self.isRevoked(id)) return Err.Revoked;
                return Err.InvalidToken;
            };
            const l = &self.leases.items[idx];
            if (l.holder != caller) return Err.NotPermitted;
            const v = self.verifier orelse return Err.NotPermitted;
            const ok = if (peer) |p|
                v.permitsRawFromPeer(cap_id, &l.urn, p)
            else
                v.permitsRaw(cap_id, &l.urn); // has lease but lacks `raw` right
            if (!ok) return Err.NotPermitted;
            break :blk l.token;
        };
        return self.backend.rawChannel(token, deadline);
    }

    /// Claim a resource (Part 3.4). Atomic, partial-failure-safe, touches
    /// hardware LAST among the fallible steps, never commits a lie.
    pub fn claim(
        self: *Core,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        return self.claimImpl(urn, mode, holder, deadline, null, false);
    }

    /// Shared atomic-grant dance for `claim` / `claimDetach`. The only
    /// difference is which backend op is invoked at the slow step.
    ///
    /// A4 (review wave 2 — Cluster J, ROADMAP R2): the lock is dropped
    /// around `backend.acquire`. Pre-A4 the entire grant held
    /// `Core.lock`. When the backend was `UsbBackendViaIpc`, the
    /// privileged-helper IPC could take up to the helper-supervisor's
    /// 5 s deadline (Tier-0 #3). During that window EVERY other op
    /// on this Core blocked. Now: under-lock reservation (placeholder
    /// in the lease table with `token.id == 0`) → unlocked
    /// `backend.acquire` → under-lock install (replace placeholder
    /// with real token).
    ///
    /// **Race semantics.** Concurrent claimants on the same URN see
    /// the placeholder as a held lease in `leasesOn`'s compat check.
    /// Two `EXCLUSIVE` claims race → second sees the placeholder's
    /// EXCLUSIVE mode and returns `Conflict` immediately. Cluster-A4
    /// promise delivered.
    ///
    /// **Placeholder sentinel.** A `Lease` with `token.id == 0` is a
    /// claim-in-progress reservation. `holderDied` skips
    /// `backend.release` for placeholders (no real token to
    /// release). `release` / `raw` against a placeholder lease_id
    /// would surface as `InvalidToken` (the holder doesn't have the
    /// id yet — `claimImpl` only returns it on success).
    fn claimImpl(
        self: *Core,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
        peer_uid: ?u32,
        detach: bool,
    ) Err!LeaseId {
        // ---- Phase 1: under-lock reservation. ----
        self.lock.lock();
        // Step 0: refuse early if the URN is quarantined (Part 2.7.1).
        if (self._quarantineStatusLocked(urn) != null) {
            self.lock.unlock();
            return Err.Unavailable;
        }
        // Step 1: Tier-1 #10 (C2/C4) fairness. If this resource was just freed
        // and is reserved for a longer-waiting tenant, deny so that tenant gets
        // its turn — a spammer can't jump the reservation. Enqueue this tenant as
        // a waiter (so a later release can reserve for it in turn).
        if (peer_uid) |uid| {
            if (self.fairness.reservationVerdict(urn, uid, kabi.nowMs()) == .reserved_for_other) {
                self.fairness.onConflict(urn, uid, kabi.nowMs());
                self.lock.unlock();
                return Err.Conflict;
            }
        }
        // Step 2: compatibility against all current holders (incl. placeholders).
        var held: [16]*Lease = undefined;
        const n = self.leasesOn(urn, &held);
        for (held[0..n]) |l| {
            if (!compatible(l.mode, mode)) {
                // Denied by contention — record the waiter so the next release
                // reserves the resource for the longest-waiting tenant (#10).
                if (peer_uid) |uid| {
                    self.fairness.onConflict(urn, uid, kabi.nowMs());
                    // Tier-1 #10 §5.4 — if this denied claimant STRICTLY outranks
                    // the live committed holder, mark the holder for graceful
                    // preemption: a grace window opens now; if it doesn't release,
                    // `runDuePreemptions` forces the teardown. No-op unless a
                    // `PrioritySource` ranks the waiter above the holder (FIFO
                    // default never marks). Placeholders (mid-claim, token.id == 0)
                    // are skipped — only a committed holder is preemptible.
                    if (l.token.id != 0) {
                        if (l.peer_uid) |holder_uid| {
                            if (self.fairness.shouldPreempt(uid, holder_uid)) {
                                self.fairness.markPreemption(urn, holder_uid, uid, kabi.nowMs());
                            }
                        }
                    }
                }
                self.lock.unlock();
                return Err.Conflict;
            }
        }
        // Step 2.5: S5 / Tier-1 #9 — bound the lease table. Checked
        // AFTER the compat lattice (a conflicting claim still gets the
        // precise Err.Conflict) and BEFORE reserving the placeholder
        // (we only count a claim that's actually about to add a lease).
        // Both ceilings shed with Err.Overloaded — back-pressure, never
        // unbounded growth.
        if (self.leases.items.len >= MAX_LEASES) {
            self.lock.unlock();
            return Err.Overloaded;
        }
        if (peer_uid) |uid| {
            var held_by_uid: usize = 0;
            for (self.leases.items) |l| {
                if (l.peer_uid) |lu| {
                    if (lu == uid) held_by_uid += 1;
                }
            }
            if (held_by_uid >= MAX_LEASES_PER_UID) {
                self.lock.unlock();
                return Err.Overloaded;
            }
        }
        // Step 3: ensure capacity. OOM here is back-pressure (J3) +
        // forensic log (J5).
        self.leases.ensureUnusedCapacity(self.alloc, 1) catch |e| {
            std.debug.print(
                "warn: core.claim: lease-list ensureUnusedCapacity failed ({s}); shedding via Err.Overloaded; active_leases={d}\n",
                .{ @errorName(e), self.leases.items.len },
            );
            self.lock.unlock();
            return Err.Overloaded;
        };
        // Step 3.5: reserve placeholder. `token.id == 0` marks it as
        // not-yet-acquired. The placeholder ALREADY counts in
        // compat checks — concurrent claimants see it.
        const lease_id = self.next_lease;
        self.next_lease += 1;
        self.leases.appendAssumeCapacity(.{
            .id = lease_id,
            .urn = urn.*,
            .holder = holder,
            .mode = mode,
            .token = .{ .id = 0 },
            .granted_at_ms = kabi.nowMs(),
            .peer_uid = peer_uid,
        });
        // Tier-1 #10 — this tenant just won the contended resource: clear any
        // reservation it held and drop it from the wait queue.
        if (peer_uid) |uid| self.fairness.onGranted(urn, uid);
        self.lock.unlock();

        // ---- Phase 2: backend.acquire OUTSIDE the lock. ----
        // This is the slow path — helper IPC up to 5 s. Other Core
        // ops can proceed concurrently against unrelated URNs.
        // On Err.Timeout the resource transitions to `quarantined`
        // (Part 2.7.1 trigger #1).
        const token = (if (detach)
            self.backend.acquireDetach(urn, mode, deadline)
        else
            self.backend.acquire(urn, mode, deadline)) catch |e| {
            // Rollback: remove the placeholder.
            self.lock.lock();
            if (e == Err.Timeout) self.markQuarantined(urn, .op_timeout);
            if (self.findLease(lease_id)) |idx| {
                _ = self.leases.swapRemove(idx);
            }
            self.lock.unlock();
            return e;
        };

        // ---- Phase 3: install real token under lock. ----
        self.lock.lock();
        if (self.findLease(lease_id)) |idx| {
            // Placeholder still present — install the real token.
            self.leases.items[idx].token = token;
            self.lock.unlock();
            return lease_id;
        }
        self.lock.unlock();

        // Placeholder vanished between Phase 2 and Phase 3 — only
        // `holderDied` removes leases without going through this
        // function's own rollback path. The client must have
        // disconnected mid-claim. We OWN the just-acquired token;
        // release it OUTSIDE the lock so a slow backend.release
        // can't re-introduce the wedge A4 was solving.
        self.backend.release(token, Deadline.none);
        return Err.Conflict;
    }

    /// List live leases whose URN matches `scope` (FEEDBACK SDK-03).
    /// Push-style — the sink receives each matching record in turn
    /// (insertion order, which is also grant order since release
    /// uses swapRemove). Read-only; no lease state is touched.
    ///
    /// `scope = "hw:/**"` matches everything; a narrower glob filters
    /// at the Core level so the facade doesn't have to re-filter merged
    /// streams. The same trailing-`**` model as `enumerate` / `list`.
    pub fn listLeases(self: *Core, scope: []const u8, sink: *const LeaseSink) void {
        self.lock.lock();
        defer self.lock.unlock();
        for (self.leases.items) |l| {
            if (!l.urn.matches(scope)) continue;
            sink.push(sink.ctx, .{
                .urn = l.urn,
                .lease_id = l.id,
                .holder = l.holder,
                .mode = l.mode,
                .granted_at_ms = l.granted_at_ms,
            });
        }
    }

    fn findLease(self: *Core, id: LeaseId) ?usize {
        for (self.leases.items, 0..) |*l, i| {
            if (l.id == id) return i;
        }
        return null;
    }

    /// Release a held lease (Part 3.2). Total + idempotent: releasing an
    /// unknown lease is a no-op. Backend release always succeeds for the
    /// caller (Part 6.3); internal escalation/quarantine is the backend's
    /// concern, bounded by `deadline`.
    pub fn release(self: *Core, id: LeaseId, deadline: Deadline) void {
        // M3 (review wave 3) — drop Core.lock around backend.release.
        //
        // Pre-M3 this held `Core.lock` across `backend.release(token, deadline)`.
        // When the backend is UsbBackendViaIpc, that call is a privileged-
        // helper IPC roundtrip — bounded by the helper-supervisor's 5s
        // SO_RCVTIMEO. During that window, every other op on this Core
        // (claim / raw / list_leases / ...) blocked on the same lock.
        // Cluster A4 (commit f463527) fixed the same shape for `raw`
        // but explicitly deferred `release`/`claim` to a focused
        // commit because they need a placeholder/re-find protocol.
        // M3 ships the placeholder protocol for release: the lease's
        // SLOT is removed atomically under the lock (so concurrent
        // claims see it gone immediately), the token is captured by
        // value, then the backend.release runs OUTSIDE the lock with
        // the captured value. If a second `release(id)` races in
        // during the unlocked window, its findLease returns null and
        // it returns silently (idempotency contract holds).
        //
        // A revoked lease is already torn down at the backend
        // level (Tier-0 #4 — Facade.revoke ran the teardown
        // before marking revoked). release() on it is a no-op:
        // the client's release should still succeed for the
        // total + idempotent contract (§6.3), but there's
        // nothing for the backend to release.
        var token_opt: ?kabi.AcquireToken = null;
        {
            self.lock.lock();
            defer self.lock.unlock();
            const idx = self.findLease(id) orelse return; // idempotent
            token_opt = self.leases.items[idx].token;
            const urn = self.leases.items[idx].urn; // capture before removal (for #10)
            // Remove the lease entry FIRST so concurrent ops see it
            // gone; release-races become no-op rather than racing
            // backend.release calls.
            _ = self.leases.swapRemove(idx);
            // Tier-1 #10 (C2/C4) — if the resource is now fully free and tenants
            // are waiting, reserve it for the longest-waiting one (FIFO) so a
            // spammer can't immediately re-grab it ahead of the fair waiter.
            var held: [16]*Lease = undefined;
            if (self.leasesOn(&urn, &held) == 0) self.fairness.onRelease(&urn, kabi.nowMs());
        }
        if (token_opt) |token| {
            self.backend.release(token, deadline); // safe-resets the resource
        }
    }

    /// Obtain the raw direct channel for a held lease (Part 3.8). Verifies the
    /// lease is live and held by the caller, then delegates to the backend and
    /// steps OUT — usage does not touch the core (G4).
    ///
    /// Lock-release before backend call: see `rawWithCap` for the rationale
    /// (avoid freezing the whole worker on a wedged helper IPC).
    pub fn raw(self: *Core, id: LeaseId, caller: ClientId, deadline: Deadline) Err!kabi.RawHandle {
        var urn: Urn = undefined; // captured under lock for quarantine-on-fault
        const token = blk: {
            self.lock.lock();
            defer self.lock.unlock();
            const idx = self.findLease(id) orelse {
                // Tier-0 #4 — distinguish "never existed" from "was
                // revoked". `Err.Revoked` tells the holder they LOST
                // access (preemption / policy), not that they never
                // had it (typo / forged id).
                if (self.isRevoked(id)) return Err.Revoked;
                return Err.InvalidToken;
            };
            const l = &self.leases.items[idx];
            if (l.holder != caller) return Err.NotPermitted;
            urn = l.urn;
            break :blk l.token;
        };
        // R-T1-8 residual (I3) — a helper killed mid-`raw_channel` leaves the
        // device's state unknown; quarantine on the timeout so it isn't silently
        // re-grantable.
        return self.backend.rawChannel(token, deadline) catch |e| {
            self.quarantineOnHelperFault(&urn, e);
            return e;
        };
    }

    /// Typed device-state mutation (Part 13 §5.1.2). Delegates to the backend's
    /// `control` op — which on the USB worker runs in the privileged helper — and
    /// quarantines the URN if a helper-fault (timeout) leaves its state unknown
    /// (R-T1-8 residual I3). The facade routes `control` through here (not
    /// straight to the backend) so this safety net applies.
    pub fn control(
        self: *Core,
        urn: *const Urn,
        verb: model.ControlVerb,
        value: []const u8,
        cap_id: u64,
        deadline: Deadline,
    ) Err!kabi.ControlOutcome {
        return self.backend.control(urn, verb, value, cap_id, deadline) catch |e| {
            self.quarantineOnHelperFault(urn, e);
            return e;
        };
    }

    /// Reverse-lookup: the backend `AcquireToken` for a given lease. Used by
    /// the broker server when it needs to ask the backend for the underlying
    /// fd (kabi.KernelBackend.fdForToken) to SCM_RIGHTS-pass on a raw call.
    /// Returns null if the lease doesn't exist; the caller may still need
    /// to verify holder identity separately (this is broker-internal use
    /// after a successful raw() call already validated the caller).
    pub fn tokenForLease(self: *Core, id: LeaseId) ?kabi.AcquireToken {
        self.lock.lock();
        defer self.lock.unlock();
        const idx = self.findLease(id) orelse return null;
        return self.leases.items[idx].token;
    }

    /// Liveness signal (Part 3.6 / 5.5): a client died. The lease engine
    /// reclaims EVERY lease that client held — death is just lease expiry, so
    /// ghost ownership is structurally impossible (G3). Returns count reclaimed.
    ///
    /// We pass `Deadline.none` to the backend release: the holder is already
    /// gone, so latency to the caller doesn't matter; a wedged release will
    /// quarantine internally and the worker watchdog (separate work) will
    /// kill+respawn if necessary.
    pub fn holderDied(self: *Core, who: ClientId) usize {
        // M3 (review wave 3) — drop Core.lock around backend.release.
        //
        // Pre-M3 this held Core.lock across N backend.release calls
        // (one per reclaimed lease). One dead client holding N leases
        // froze the worker for up to N × 5s (helper IPC deadline) —
        // which can lock up the whole worker for ~30s on a typical
        // crashed client holding ~6 leases. Same wedge cluster A4
        // fixed for raw, never fixed for the holder-death path.
        //
        // Fix shape: under the lock, harvest matching leases' tokens
        // into a stack array AND swap-remove every matching entry.
        // After releasing the lock, call backend.release for each
        // harvested token. The cap (MAX_RECLAIM_BATCH=64) bounds the
        // stack frame; the bound holds because the per-tenant lease
        // ceiling (MAX_LEASES_PER_UID = 64) caps how many leases one
        // connection can hold, and a dead connection's leases all
        // share its uid.
        const MAX_RECLAIM_BATCH = 64;
        var tokens: [MAX_RECLAIM_BATCH]kabi.AcquireToken = undefined;
        var token_count: usize = 0;
        {
            self.lock.lock();
            defer self.lock.unlock();
            var i: usize = 0;
            while (i < self.leases.items.len) {
                if (self.leases.items[i].holder == who) {
                    // A4 (Cluster J, ROADMAP R2): skip placeholders
                    // — `token.id == 0` means no real backend
                    // acquire was committed (the claim is still
                    // in-flight). Removing the lease entry is
                    // correct (the claimer's Phase 3 will see
                    // it gone + release the token they're about
                    // to receive). Calling `backend.release` on
                    // a zero-id token would either be a no-op
                    // (FakeBackend) or attempt to release a
                    // nonexistent claim_id on a real backend.
                    if (self.leases.items[i].token.id != 0) {
                        if (token_count < MAX_RECLAIM_BATCH) {
                            tokens[token_count] = self.leases.items[i].token;
                            token_count += 1;
                        }
                    }
                    _ = self.leases.swapRemove(i);
                    // do not advance i — swapRemove moved a new element into i
                } else {
                    i += 1;
                }
            }
        }
        // Release outside the lock. Deadline.none because the holder
        // is already gone — caller-side latency doesn't matter; a
        // wedged release will trip the Tier-0 #3 supervisor watchdog
        // on the helper side.
        var k: usize = 0;
        while (k < token_count) : (k += 1) {
            self.backend.release(tokens[k], Deadline.none);
        }
        // Tier-1 #8 — bulk-reclaim is a major table transition; run the
        // integrity safety-net audit. In correct operation it never fires;
        // if it ever does, the offending resource is quarantined (a
        // controlled fault) rather than left as a silent corruption.
        _ = self.auditInvariants();
        return token_count;
    }

    /// Current live state of a resource, derived from the lease table (Part 2.7).
    pub fn stateOf(self: *Core, urn: *const Urn) model.State {
        self.lock.lock();
        defer self.lock.unlock();
        for (self.leases.items) |*l| {
            if (l.urn.eql(urn)) {
                return .{ .claimed = .{ .owner = l.holder, .mode = l.mode, .lease = l.id } };
            }
        }
        return .free;
    }

    pub fn liveLeaseCount(self: *Core) usize {
        self.lock.lock();
        defer self.lock.unlock();
        return self.leases.items.len;
    }
};
