//! mgf-hrb-facade — the router + supervisor (Part 7). Composes the proven
//! pieces (core lease engine, cap verifier, liveness, backend) into one running
//! constellation. Owns the client connections and routes requests by URN scope
//! to the owning worker. Holds NO ownership table itself (Part 7.3) — that lives
//! in the per-bus workers' cores. Here, for the single-process skeleton, we model
//! one worker per bus subtree behind the facade's routing.
//!
//! DEADLINES. Every method that crosses the seam takes a `Deadline` and
//! propagates it. The facade itself adds no time — routing is constant-time
//! and never blocks — so the deadline arrives at the owning worker's core
//! with the full caller-supplied budget intact.

const std = @import("std");
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const cap = @import("mgf_hrb_cap");
const core_mod = @import("mgf_hrb_core");
const Core = core_mod.Core;
const liveness = @import("mgf_hrb_liveness");
const irqmod = @import("mgf_hrb_irq_monitor");
const lbound = @import("mgf_hrb_linux_bound");
const subscription = @import("mgf_hrb_subscription");

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

/// Per-worker cap on the restart-reconcile scan (S5 bound — a hostile/huge
/// hardware enumeration can't make reconcile allocate or scan without limit).
/// Overflow sets `truncated`; the operator re-runs (reconcile is idempotent) to
/// cover the remainder. 256 × `@sizeOf(Urn)` (~136 B) ≈ 35 KiB — a comfortable
/// stack frame on the connection thread.
const MAX_RECONCILE_URNS: usize = 256;

/// Aggregate result of a broker-wide restart reconcile (`reconcileAfterRestart`):
/// how many present-in-hardware URNs were scanned, how many were newly
/// quarantined (present but unclaimed), and whether the bounded scan truncated.
pub const ReconcileReport = struct { scanned: usize = 0, quarantined: usize = 0, truncated: bool = false };

/// Collects the present-in-hardware URNs a worker's backend enumerates, filtered
/// to the requested `scope` and bounded by `MAX_RECONCILE_URNS`, to feed
/// `Core.reconcileAfterRestart`. A pure value type (no allocator) — lives on the
/// reconcile caller's stack for the duration of one worker's scan.
const ReconcileCollector = struct {
    scope: []const u8,
    /// R-T1-8 (b) — when set, keep only URNs the source classifies as bound at
    /// the hardware level (genuine orphans), dropping idle-free / host-used ones.
    /// Null = keep the full present set (the prior behaviour).
    bound: ?lbound.BoundSource = null,
    urns: [MAX_RECONCILE_URNS]Urn = undefined,
    n: usize = 0,
    truncated: bool = false,

    fn push(ctx: *anyopaque, r: model.Resource) void {
        const self: *ReconcileCollector = @ptrCast(@alignCast(ctx));
        if (!r.urn.matches(self.scope)) return; // outside the requested scope
        if (self.bound) |b| {
            if (!b.is_bound(b.ctx, &r.urn)) return; // not a hardware-bound orphan → leave it alone
        }
        if (self.n >= self.urns.len) {
            self.truncated = true;
            return;
        }
        self.urns[self.n] = r.urn;
        self.n += 1;
    }
};

/// Collects the DISTINCT URNs of the live leases a worker reports, to drive the
/// interrupt-rate scan (D7). A URN held by several shared leases is sampled once.
/// Bounded by `MAX_RECONCILE_URNS` (the live-lease set is already bounded by the
/// #9 ceilings); overflow silently stops collecting (the next scan re-covers it).
const LeasedUrnCollector = struct {
    urns: [MAX_RECONCILE_URNS]Urn = undefined,
    n: usize = 0,

    fn push(ctx: *anyopaque, rec: core_mod.LeaseRecord) void {
        const self: *LeasedUrnCollector = @ptrCast(@alignCast(ctx));
        for (self.urns[0..self.n]) |u| {
            if (u.eql(&rec.urn)) return; // dedup shared-mode leases on one URN
        }
        if (self.n >= self.urns.len) return;
        self.urns[self.n] = rec.urn;
        self.n += 1;
    }
};

/// A worker: owns one bus subtree (its scope prefix) and the core/ownership for
/// it. In the full system this is a separate process (Part 7.1); here it's an
/// in-process unit behind the facade's routing, with the SAME boundaries.
pub const Worker = struct {
    scope_prefix: []const u8, // e.g. "hw:/pci/", "hw:/usb/"
    core: *Core,
    /// Human-readable backend identifier — surfaced by the `capabilities`
    /// op so `mgf-hrb doctor` can render "scope `hw:/pci/` → mgf-hrb-linux".
    backend_name: []const u8 = "unknown",

    fn owns(self: *const Worker, urn: *const Urn) bool {
        return std.mem.startsWith(u8, urn.slice(), self.scope_prefix);
    }
};

/// The facade. Routes by scope to the owning worker; supervises liveness;
/// holds the client connection set (the linchpin of worker crash-recovery,
/// Part 7.2). No ownership table here — routing only (Part 7.3).
pub const Facade = struct {
    workers: []Worker,
    verifier: ?*cap.CapTable = null,

    pub fn init(workers: []Worker) Facade {
        wireEventSinks(workers);
        return .{ .workers = workers };
    }
    pub fn initWithCaps(workers: []Worker, verifier: *cap.CapTable) Facade {
        wireEventSinks(workers);
        return .{ .workers = workers, .verifier = verifier };
    }

    /// Route a urn to its owning worker (Part 7.2). Returns null if no worker
    /// owns that subtree.
    fn route(self: *Facade, urn: *const Urn) ?*Worker {
        for (self.workers) |*w| {
            if (w.owns(urn)) return w;
        }
        return null;
    }

    // ---- the client-facing operations, routed to the owning worker ----

    pub fn claim(
        self: *Facade,
        cap_id: u64,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.claimWithCap(cap_id, null, urn, mode, holder, deadline);
        }
        return w.core.claim(urn, mode, holder, deadline);
    }

    /// Like `claim` but routes through the backend's `acquireDetach`
    /// primitive (USBDEVFS_DISCONNECT_CLAIM on USB). Cap-gated by the
    /// `detach_driver` right (see USB-worker §5.10 / FEEDBACK
    /// TOOLS-01). Returns `Err.Unsupported` when the routed backend
    /// has no detach primitive — e.g. PCI without the future VFIO
    /// rebind path, or FakeBackend.
    pub fn claimDetach(
        self: *Facade,
        cap_id: u64,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.claimDetachWithCap(cap_id, null, urn, mode, holder, deadline);
        }
        return w.core.claimDetach(urn, mode, holder, deadline);
    }

    /// Peer-aware variant of `claim` (Part 12 §3.3 Tier-0 #2). Routes
    /// to the same per-bus core but uses the peer-aware verifier path
    /// so a connection-bound cap is checked against the actual peer.
    /// Used by the broker server (which has the peer from
    /// `SO_PEERCRED` at accept time); in-process callers still use
    /// the legacy `claim`.
    pub fn claimFromPeer(
        self: *Facade,
        cap_id: u64,
        peer: ?cap.PeerCred,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.claimWithCap(cap_id, peer, urn, mode, holder, deadline);
        }
        return w.core.claim(urn, mode, holder, deadline);
    }

    pub fn claimDetachFromPeer(
        self: *Facade,
        cap_id: u64,
        peer: ?cap.PeerCred,
        urn: *const Urn,
        mode: ClaimMode,
        holder: ClientId,
        deadline: Deadline,
    ) Err!LeaseId {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.claimDetachWithCap(cap_id, peer, urn, mode, holder, deadline);
        }
        return w.core.claimDetach(urn, mode, holder, deadline);
    }

    /// Release a held lease. Total + idempotent for the BACKEND side
    /// (releasing an unknown lease within a routed worker is a no-op),
    /// but NotFound for an unrouted URN — J1 (review wave 2): pre-J1
    /// the routing miss was silently swallowed and the wire still
    /// reported `ok:true`, which made "I released the wrong URN" a
    /// silent failure for consumers. Now the caller sees the typed
    /// error.
    pub fn release(self: *Facade, urn: *const Urn, id: LeaseId, caller: ClientId, deadline: Deadline) Err!void {
        const w = self.route(urn) orelse return Err.NotFound;
        // CORE-02 — `caller` is the broker-assigned connection id (D-038),
        // so the holder it is compared against is unforgeable. Refuses
        // with NotPermitted when the lease belongs to another connection.
        return w.core.release(id, caller, deadline);
    }

    pub fn raw(
        self: *Facade,
        cap_id: u64,
        urn: *const Urn,
        id: LeaseId,
        caller: ClientId,
        deadline: Deadline,
    ) Err!kabi.RawHandle {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.rawWithCap(cap_id, null, id, caller, deadline);
        }
        return w.core.raw(id, caller, deadline);
    }

    /// Peer-aware variant of `raw`. Same lease-ownership check, but
    /// the cap check is binding-aware (Part 12 §3.3 Tier-0 #2).
    pub fn rawFromPeer(
        self: *Facade,
        cap_id: u64,
        peer: ?cap.PeerCred,
        urn: *const Urn,
        id: LeaseId,
        caller: ClientId,
        deadline: Deadline,
    ) Err!kabi.RawHandle {
        const w = self.route(urn) orelse return Err.NotFound;
        if (self.verifier) |_| {
            return w.core.rawWithCap(cap_id, peer, id, caller, deadline);
        }
        return w.core.raw(id, caller, deadline);
    }

    // M-B B2 — `drainEvents` is GONE from the facade, deliberately.
    //
    // It existed for exactly one caller: `format.fmtDrainEvents`, i.e. a
    // CLIENT reaching through the broker to pull the netlink socket. That
    // made hardware events a first-come resource two operators raced for.
    // The broker now drains its backends itself, from one thread, via
    // `event_pump.zig` — which holds the `{fd, drain-fn}` pair per worker
    // and never needs routing-by-scope. Leaving a facade-level drain in
    // place would leave a second door onto the same socket open; the
    // single-drainer property is only true while this method does not
    // exist. Worker enumeration for the pump lives in `server.zig`'s
    // startup wiring, where the constellation is built.

    /// Dispatch a typed control verb (USB-worker §5.10) to the owning
    /// worker's backend. The facade only routes — it does NOT enforce
    /// the cap gate (that's the server's job; the right needed depends
    /// on the verb). Returns `Err.Unsupported` honestly when the
    /// routed backend lacks the `control` vtable slot.
    pub fn control(
        self: *Facade,
        urn: *const Urn,
        verb: model.ControlVerb,
        value: []const u8,
        cap_id: u64,
        deadline: Deadline,
    ) Err!kabi.ControlOutcome {
        const w = self.route(urn) orelse return Err.NotFound;
        // Through core.control (not straight to the backend) so a helper-fault
        // mid-control quarantines the URN (R-T1-8 residual I3).
        return w.core.control(urn, verb, value, cap_id, deadline);
    }

    /// Inspect a single URN: route to the owning worker, ask its backend
    /// for per-resource detail (vid/pid/serial for USB; vendor/device for
    /// PCI; etc.). Returns Err.NotFound when no worker owns the scope.
    pub fn inspect(self: *Facade, urn: *const Urn, attrs: *const kabi.AttrSink, deadline: Deadline) Err!model.Resource {
        const w = self.route(urn) orelse return Err.NotFound;
        return w.core.backend.inspect(urn, attrs, deadline);
    }

    /// Part 13 §5.5.1 — bulk-introspect the URN's substrate subtree.
    /// Routes to the owning worker, delegates to the backend's
    /// `inspect_full` vtable method. Returns `Err.NotFound` for an
    /// unrouted scope; `Err.Unsupported` when the routed backend
    /// has no substrate-tree walker (Fake today).
    pub fn inspectFull(
        self: *Facade,
        urn: *const Urn,
        depth: model.InspectDepth,
        sink: *const kabi.TreeSink,
        deadline: Deadline,
    ) Err!model.Resource {
        const w = self.route(urn) orelse return Err.NotFound;
        return w.core.backend.inspectFull(urn, depth, sink, deadline);
    }

    /// Part 13 §5.5.2 — route a single-URN parent-chain walk to the
    /// worker that owns the URN, then call into the backend's
    /// `dump_chain` vtable method. Returns `Err.NotFound` for an
    /// unrouted URN; `Err.Unsupported` when the routed backend has
    /// no parent-chain walker (Fake today).
    pub fn dumpChain(
        self: *Facade,
        urn: *const Urn,
        sink: *const kabi.ChainSink,
        deadline: Deadline,
    ) Err!void {
        const w = self.route(urn) orelse return Err.NotFound;
        return w.core.backend.dumpChain(urn, sink, deadline);
    }

    /// Tier-0 #4 — revoke any held lease on `urn` AND tear down
    /// the backend's kernel-side claim. The two halves must
    /// happen together: dropping just the lease leaves the
    /// kernel-side claim in place (the holder's fd keeps
    /// working); tearing down just the claim leaves the lease
    /// engine thinking the lease is still alive (future
    /// inspect/list_leases lie). This wrapper does both,
    /// atomically from the consumer's perspective.
    pub fn revoke(
        self: *Facade,
        urn: *const Urn,
        deadline: Deadline,
    ) Err!void {
        const w = self.route(urn) orelse return Err.NotFound;
        // First tear down the backend's claim (the harder part —
        // may return Unsupported on a fake / VFIO-less backend).
        // Only on success do we move the lease into revoked state
        // so future ops by the prior holder see Err.Revoked.
        try w.core.backend.revoke(urn, deadline);
        w.core.markRevokedByUrn(urn);
    }

    /// Tier-1 #12 — run the reset-escalation ladder on a quarantined URN
    /// to recover it back to `Free` (routes to the owning worker's core).
    /// Returns `.not_quarantined` for an unrouted URN too — there's no
    /// resource to recover, so the no-op outcome is the honest answer
    /// (recovery never errors; the ladder always terminates).
    pub fn recoverByReset(
        self: *Facade,
        urn: *const Urn,
        deadline: Deadline,
    ) core_mod.RecoveryOutcome {
        const w = self.route(urn) orelse return .not_quarantined;
        return w.core.recoverByReset(urn, deadline);
    }

    /// Returns the underlying fd for a held raw channel, when the routed
    /// worker's backend has an fd-shaped raw channel (USBDEVFS today;
    /// VFIO when that lands). Returns -1 if the backend doesn't fd-pass,
    /// the lease doesn't exist, or routing fails. Used by the broker
    /// server to SCM_RIGHTS-pass the fd over the client socket per
    /// `recipes/claim-a-device-for-vfio-passthrough.md` §1.
    pub fn fdForLease(self: *Facade, urn: *const Urn, id: LeaseId) i32 {
        const w = self.route(urn) orelse return -1;
        const token = w.core.tokenForLease(id) orelse return -1;
        return w.core.backend.fdForToken(token);
    }

    /// Enumerate across ALL workers, merging results (Part 7.2: the facade
    /// merges results, never ownership decisions). A "hw:/**" query fans out;
    /// a scoped query reaches only the owning worker(s). The deadline is the
    /// AGGREGATE budget — each worker gets the same `Deadline`, so a slow
    /// worker can consume the whole budget. (A future hardening shards the
    /// deadline across workers; not in this commit.)
    pub fn enumerate(
        self: *Facade,
        scope: []const u8,
        sink: *const kabi.FactSink,
        deadline: Deadline,
    ) Err!void {
        for (self.workers) |*w| {
            // only query a worker whose subtree could intersect the scope
            if (scopeIntersects(scope, w.scope_prefix)) {
                try w.core.backend.enumerate(scope, sink, deadline);
            }
        }
    }

    /// Tier-1 #8 (S4 §4.3) restart-recovery WIRING — the broker-side caller of
    /// `Core.reconcileAfterRestart`. After a restart the broker re-derives truth
    /// from the two authoritative sources: the HARDWARE (what each worker's
    /// backend enumerates as present) and the LIVE CLIENTS (each core's lease
    /// table). For each worker intersecting `scope`, it enumerates the
    /// present-in-hardware set and hands it to that core, which quarantines
    /// anything present-but-unclaimed (`unclaimed_after_restart`) — never
    /// assumed free (may be dirty), never assumed owned (the owner is gone).
    /// Returns the aggregate `{scanned, quarantined, truncated}`.
    ///
    /// MUST be triggered only on an actual restart (the operator/supervisor
    /// knows; the broker has no self-restart-detection persistence yet) — run on
    /// a healthy broker it would quarantine every idle device. The `reconcile`
    /// wire op is steal-gated for exactly this reason.
    pub fn reconcileAfterRestart(self: *Facade, scope: []const u8, bound: ?lbound.BoundSource, deadline: Deadline) ReconcileReport {
        var report = ReconcileReport{};
        for (self.workers) |*w| {
            if (!scopeIntersects(scope, w.scope_prefix)) continue;
            // Collect the present-in-hardware URNs for this worker (bounded +
            // scope-filtered + optionally bound-filtered), then reconcile that
            // worker's core against them.
            var col = ReconcileCollector{ .scope = scope, .bound = bound };
            const sink = kabi.FactSink{ .ctx = &col, .push = ReconcileCollector.push };
            // An enumerate failure (timeout / backend fault) skips this worker
            // rather than aborting the whole sweep — reconcile is idempotent, so
            // an operator re-run picks up a worker that was transiently slow.
            w.core.backend.enumerate(scope, &sink, deadline) catch {
                report.truncated = true;
                continue;
            };
            if (col.truncated) {
                report.truncated = true;
                std.debug.print(
                    "reconcile: worker {s} enumerated > {d} URNs — bounded scan; " ++
                        "re-run reconcile to cover the remainder (idempotent)\n",
                    .{ w.scope_prefix, MAX_RECONCILE_URNS },
                );
            }
            report.scanned += col.n;
            report.quarantined += w.core.reconcileAfterRestart(col.urns[0..col.n]);
        }
        return report;
    }

    /// D7 (survivability §2.7) interrupt-rate containment scan — the broker-side
    /// driver of the IRQ monitor. Walks every currently-granted device (live
    /// leases, deduped per URN), pulls its interrupt counter from `source`, feeds
    /// it to `monitor`, and for any device the monitor judges to be FLOODING,
    /// tears the grant down + quarantines it (`Core.containByFlooding`) and drops
    /// its history so a future re-grant re-baselines. `now_ms` is injected so the
    /// scan is deterministic in tests (the wire path passes `kabi.nowMs()`).
    /// Returns `{scanned, contained}`. Cold path — runs only when the steal-gated
    /// `irq_scan` op is triggered, never on the hot use path.
    pub fn scanInterruptRates(
        self: *Facade,
        monitor: *irqmod.IrqMonitor,
        source: irqmod.IrqCounterSource,
        now_ms: u64,
        deadline: Deadline,
    ) irqmod.IrqReport {
        var report = irqmod.IrqReport{};
        // Take ONE per-scan snapshot up front (e.g. read /proc/interrupts once),
        // so sampling N devices costs one shared read, not N.
        if (source.refresh) |refresh| refresh(source.ctx);
        for (self.workers) |*w| {
            var col = LeasedUrnCollector{};
            const sink = core_mod.LeaseSink{ .ctx = &col, .push = LeasedUrnCollector.push };
            w.core.listLeases("hw:/**", &sink);
            for (col.urns[0..col.n]) |*urn| {
                report.scanned += 1;
                const count = source.get(source.ctx, urn) orelse continue; // unattributable → skip
                if (monitor.sample(urn, count, now_ms)) {
                    w.core.containByFlooding(urn, deadline) catch continue;
                    monitor.forget(urn); // contained — next grant re-baselines
                    report.contained += 1;
                }
            }
        }
        return report;
    }

    /// R-T1-11A — does the cap permit observing the existence of `urn`?
    /// Returns false when there's no verifier (no cap layer enforced) AND
    /// the cap_id is non-zero (an inconsistent ask). When cap_id == 0
    /// the caller hasn't presented a cap — callers must NOT use the
    /// result as "permission denied," only as "scope filter applies."
    /// Use `usesCapFilter` to decide whether to call this at all.
    pub fn capPermitsNaming(self: *Facade, cap_id: u64, urn: *const Urn) bool {
        const v = self.verifier orelse return true; // no cap layer ⇒ no filter
        return v.permitsNaming(cap_id, urn);
    }

    /// R-T1-11A — true when the cap-filter applies (verifier is wired
    /// AND a non-zero cap_id was presented). Used by `fmtList` to
    /// decide whether to wrap the collector with a scope filter.
    pub fn usesCapFilter(self: *Facade, cap_id: u64) bool {
        return self.verifier != null and cap_id != 0;
    }

    /// A client died (driven by the liveness monitor): tell EVERY worker to
    /// reclaim that client's leases. Death is partitioned-safe — each worker
    /// reclaims independently within its subtree (Part 7.1, no split-brain).
    pub fn holderDied(self: *Facade, who: ClientId) usize {
        var total: usize = 0;
        for (self.workers) |*w| {
            total += w.core.holderDied(who);
        }
        return total;
    }

    pub fn totalLeases(self: *Facade) usize {
        var n: usize = 0;
        for (self.workers) |*w| n += w.core.liveLeaseCount();
        return n;
    }

    /// List live leases across every worker whose subtree intersects
    /// `scope` (FEEDBACK SDK-03). The sink receives one
    /// :code:`core.LeaseRecord` per matching lease. Same scope-glob
    /// semantics as `enumerate` / `drainEvents`. Read-only; no lease
    /// state is mutated. The merge is per-worker partitioned (no
    /// cross-worker ordering guarantee, only stable-per-worker order
    /// which is grant order); a GUI that wants global "since-when"
    /// order can sort by `granted_at_ms` after collecting.
    pub fn listLeases(
        self: *Facade,
        scope: []const u8,
        sink: *const core_mod.LeaseSink,
    ) void {
        for (self.workers) |*w| {
            if (scopeIntersects(scope, w.scope_prefix)) {
                w.core.listLeases(scope, sink);
            }
        }
    }
};

/// M-B B1 — point every worker's lease engine at the broker's
/// subscription fan-out. The facade is the composer, so the facade owns
/// this wiring: a `Core` never learns that subscriptions exist, and no
/// caller can build a constellation and then forget to connect it (the
/// wiring happens in `init`, not in a step the server has to remember).
///
/// `pushFanout` is the existing thread-safe 3-phase RCU-lite fan-out and
/// is safe to call from any thread, which is exactly what the mutation
/// sites need — they publish from whichever connection thread ran the
/// mutation, with no Core lock held.
fn wireEventSinks(workers: []Worker) void {
    for (workers) |*w| w.core.setEventSink(.{ .ctx = null, .push = fanoutPush });
}

/// The sink body. Context-free: the registry is a module singleton, so
/// there is nothing to carry. `AttrLookup` stays null-wired, matching
/// `format.zig`'s existing fan-out calls — predicate evaluation against
/// event-intrinsic attrs only, with the inspect-on-event escape hatch
/// still deferred (unchanged by B1).
fn fanoutPush(ctx: ?*anyopaque, ev: model.Event) void {
    _ = ctx;
    subscription.pushFanout(ev, .{ .ctx = null, .get = null });
}

/// Does query `scope` possibly intersect a worker whose subtree is `prefix`?
/// "hw:/**" intersects everything; "hw:/pci/**" intersects the pci worker only.
fn scopeIntersects(scope: []const u8, prefix: []const u8) bool {
    const s = if (std.mem.endsWith(u8, scope, "**")) scope[0 .. scope.len - 2] else scope;
    // intersect if one is a prefix of the other
    return std.mem.startsWith(u8, s, prefix) or std.mem.startsWith(u8, prefix, s);
}
