//! mgf-hrb-subscription — Part 13 §5.4.2 condition-filtered subscriptions.
//!
//! The wire ops `subscribe` / `unsubscribe` (server.zig) register / tear
//! down a per-client `Subscription` here. Each Subscription carries:
//!   * A `scope` (URN glob; same shape as `list`'s scope).
//!   * A parsed `Predicate` (the JSON expression tree from §5.4.2).
//!   * A bounded ring buffer of `Event` records — the per-subscription
//!     queue the spec mandates.
//!
//! Fan-out happens inside `drain_events` (server.zig): every call pulls
//! whatever events the backends have buffered AND fans the matched ones
//! out to every registered subscription's queue. A subscriber pulls
//! from THEIR queue by passing `subscription_id` to `drain_events`.
//!
//! ## Why a module singleton (not a passed-around state struct)
//!
//! The subscription registry is broker-wide state: every wire op needs
//! to reach it, and every client connection's lifecycle needs to reap
//! its subscriptions on disconnect. Threading a `*SubscriptionRegistry`
//! through `server.handle()` + `Facade` + every formatter would be a
//! big diff for a small need. A module-level static (with its own
//! SpinMutex) is the same scope but no surface change.
//!
//! ## Memory discipline
//!
//! Each Subscription owns an `std.heap.ArenaAllocator` — the parsed
//! predicate's nested strings + slices live inside it. Subscription
//! teardown frees the arena in one shot. No per-node free is needed
//! anywhere in the evaluator.
//!
//! ## v1 grammar coverage (Part 13 §5.4.2)
//!
//! Operators:  `all_of`, `any_of`, `not`.
//! Terms:      `kind_in`, `urn_glob`, `attr_eq`, `attr_in`.
//! Empty / missing predicate → matches every event (subscriber wanted
//! "everything in scope").
//!
//! Capped sizes (defensive bounds; bigger sizes are a Phase 2B knob):
//!   * `MAX_QUEUE_LEN` = 64  events per subscription (~16 KB worst case)
//!   * `MAX_SUBSCRIPTIONS_PER_PEER` = 8
//!   * `MAX_TOTAL_SUBSCRIPTIONS` = 256
//!   * `MAX_PREDICATE_NODES` = 64 (per parse, summed across operators+terms)
//!   * `MAX_SUBSCRIPTION_ID_LEN` = 64 bytes
//!   * `MAX_STRING_LEN` = 256 bytes (predicate literals: urn_glob, attr values)

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

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

pub const MAX_QUEUE_LEN: usize = 64;
pub const MAX_SUBSCRIPTIONS_PER_PEER: usize = 8;
pub const MAX_TOTAL_SUBSCRIPTIONS: usize = 256;
pub const MAX_PREDICATE_NODES: usize = 64;
pub const MAX_SUBSCRIPTION_ID_LEN: usize = 64;
pub const MAX_STRING_LEN: usize = 256;

// ---------------------------------------------------------------------------
// Predicate types — a small AST.
// ---------------------------------------------------------------------------

/// One event-kind tag (matches `Event`'s union tags). Closed grammar:
/// `kind_in` only accepts values from this set. Unknown tags BadRequest
/// at parse time. New event kinds added to `Event` MUST also be added
/// here for `kind_in` to filter them.
pub const EventKind = enum {
    added,
    removed,
    state_changed,
    ownership_changed,
    faulted,
    gap,
    driver_bound,
    driver_unbound,
    // NOTE: `device_ready` is spec'd in Part 13 §5.4.1 + §5.4.3 but
    // not yet implemented on the Zig side; when it lands (Phase 6E),
    // add it here AND to `fromEvent` / `fromStr` in one diff.

    pub fn fromStr(s: []const u8) ?EventKind {
        const list = [_]struct { name: []const u8, kind: EventKind }{
            .{ .name = "added", .kind = .added },
            .{ .name = "removed", .kind = .removed },
            .{ .name = "state_changed", .kind = .state_changed },
            .{ .name = "ownership_changed", .kind = .ownership_changed },
            .{ .name = "faulted", .kind = .faulted },
            .{ .name = "gap", .kind = .gap },
            .{ .name = "driver_bound", .kind = .driver_bound },
            .{ .name = "driver_unbound", .kind = .driver_unbound },
        };
        for (list) |e| if (std.mem.eql(u8, s, e.name)) return e.kind;
        return null;
    }

    /// Map a `model.Event` tag to its EventKind. The two enums are
    /// kept in lock-step manually because Zig's union doesn't expose
    /// its discriminant as a separately-introspectable enum we can
    /// alias here without a circular dependency.
    pub fn fromEvent(ev: *const Event) EventKind {
        return switch (ev.*) {
            .added => .added,
            .removed => .removed,
            .state_changed => .state_changed,
            .ownership_changed => .ownership_changed,
            .faulted => .faulted,
            .gap => .gap,
            .driver_bound => .driver_bound,
            .driver_unbound => .driver_unbound,
        };
    }
};

pub const AttrEq = struct {
    key: []const u8,
    value: []const u8,
};

pub const AttrIn = struct {
    key: []const u8,
    values: [][]const u8,
};

/// The predicate AST. Recursive — `all_of` / `any_of` carry child
/// predicate slices; `not` carries one. Leaf terms are atomic.
///
/// `matches_all` is the explicit "no predicate" sentinel — a subscribe
/// request with no `predicate` field (or `{}`) parses to this and
/// matches every event in scope.
pub const Predicate = union(enum) {
    all_of: []Predicate,
    any_of: []Predicate,
    not: *Predicate,
    kind_in: []EventKind,
    urn_glob: []const u8,
    attr_eq: AttrEq,
    attr_in: AttrIn,
    matches_all: void,
};

// ---------------------------------------------------------------------------
// Predicate parsing — std.json.Value → Predicate.
// ---------------------------------------------------------------------------

/// Parse a JSON-shaped predicate into a Predicate AST allocated in
/// `arena`. The caller owns the arena (frees it when the subscription
/// goes away). Returns Err.BadRequest on any malformed shape — every
/// branch must check both the JSON shape AND the per-field charset/
/// size caps. The closed grammar protects the broker from unbounded
/// recursion (`MAX_PREDICATE_NODES`) or hostile inputs.
///
/// `node_budget` is a single shared counter the parser decrements
/// per node; when it hits 0, parse fails BadRequest. Prevents a
/// pathological 1000-deep predicate from exhausting the stack.
///
/// N1 (review wave 3) — Err.BadRequest vs Err.Overloaded taxonomy.
///
/// Every `arena.alloc/create/dupe(...) catch return Err.X` in this
/// function returns `Err.Overloaded`, NOT `Err.BadRequest`. The
/// arena's only failure mode is `error.OutOfMemory`; misclassifying
/// it as BadRequest told the consumer "your input is bad" when the
/// actual cause is "the broker is at capacity." The SDK's
/// OverloadedError docstring tells consumers to back off + retry,
/// which is the right response to OOM (BadRequestError says "fix
/// your input," which is wrong here). Wave-2's J-cluster J28
/// surfaced this; wave-3 ships the fix.
///
/// BadRequest is reserved for genuine shape/charset/grammar errors:
/// wrong JSON node type, op-name typo, exceeded node_budget, etc.
pub fn parsePredicate(
    v: std.json.Value,
    arena: std.mem.Allocator,
    node_budget: *u32,
) Err!Predicate {
    if (node_budget.* == 0) return Err.BadRequest;
    node_budget.* -= 1;

    const obj = switch (v) {
        .object => |o| o,
        else => return Err.BadRequest,
    };

    // Exactly one operator/term per node — predicates are tagged dicts.
    if (obj.count() != 1) return Err.BadRequest;

    var it = obj.iterator();
    const entry = it.next().?;
    const op_name = entry.key_ptr.*;
    const val = entry.value_ptr.*;

    if (std.mem.eql(u8, op_name, "all_of")) {
        const arr = switch (val) {
            .array => |a| a,
            else => return Err.BadRequest,
        };
        // N2 (review wave 3) — empty-array refusal.
        //
        // Pre-N2: `{"all_of": []}` was accepted as vacuously true (an
        // empty all_of evaluates true by definition — no failing
        // term). `{"any_of": []}` was accepted as vacuously false.
        // Both shapes are almost certainly client bugs: the user
        // who wrote `all_of: []` meant `all_of: [SOMETHING]` and
        // forgot to add the term. Silently accepting the vacuous
        // truth/falsehood gives them a subscription that ALWAYS
        // fires (all_of) or NEVER fires (any_of) — an
        // operator-debugging trap of the highest order.
        //
        // Honest discipline: reject empty arrays at parse time
        // with BadRequest + a detail message. If a consumer
        // genuinely wants "match everything," they should omit
        // the predicate entirely (the broker treats absent
        // predicate as matches-all per the §5.4.2 contract).
        if (arr.items.len == 0) return Err.BadRequest;
        const out = arena.alloc(Predicate, arr.items.len) catch return Err.Overloaded;
        for (arr.items, 0..) |child, i| {
            out[i] = try parsePredicate(child, arena, node_budget);
        }
        return .{ .all_of = out };
    }
    if (std.mem.eql(u8, op_name, "any_of")) {
        const arr = switch (val) {
            .array => |a| a,
            else => return Err.BadRequest,
        };
        // N2 — same empty-array refusal as all_of above.
        if (arr.items.len == 0) return Err.BadRequest;
        const out = arena.alloc(Predicate, arr.items.len) catch return Err.Overloaded;
        for (arr.items, 0..) |child, i| {
            out[i] = try parsePredicate(child, arena, node_budget);
        }
        return .{ .any_of = out };
    }
    if (std.mem.eql(u8, op_name, "not")) {
        const child = arena.create(Predicate) catch return Err.Overloaded;
        child.* = try parsePredicate(val, arena, node_budget);
        return .{ .not = child };
    }
    if (std.mem.eql(u8, op_name, "kind_in")) {
        const arr = switch (val) {
            .array => |a| a,
            else => return Err.BadRequest,
        };
        // N2 — empty kind_in is the same client-error class as empty
        // all_of/any_of: `kind_in: []` matches nothing forever. Refuse.
        if (arr.items.len == 0) return Err.BadRequest;
        const out = arena.alloc(EventKind, arr.items.len) catch return Err.Overloaded;
        for (arr.items, 0..) |child, i| {
            const s = switch (child) {
                .string => |sx| sx,
                else => return Err.BadRequest,
            };
            out[i] = EventKind.fromStr(s) orelse return Err.BadRequest;
        }
        return .{ .kind_in = out };
    }
    if (std.mem.eql(u8, op_name, "urn_glob")) {
        const s = switch (val) {
            .string => |sx| sx,
            else => return Err.BadRequest,
        };
        if (s.len > MAX_STRING_LEN) return Err.BadRequest;
        const copy = arena.dupe(u8, s) catch return Err.Overloaded;
        return .{ .urn_glob = copy };
    }
    if (std.mem.eql(u8, op_name, "attr_eq")) {
        const inner = switch (val) {
            .object => |o| o,
            else => return Err.BadRequest,
        };
        const key_v = inner.get("key") orelse return Err.BadRequest;
        const value_v = inner.get("value") orelse return Err.BadRequest;
        const key_s = switch (key_v) {
            .string => |sx| sx,
            else => return Err.BadRequest,
        };
        const value_s = switch (value_v) {
            .string => |sx| sx,
            else => return Err.BadRequest,
        };
        if (key_s.len > MAX_STRING_LEN or value_s.len > MAX_STRING_LEN) return Err.BadRequest;
        return .{ .attr_eq = .{
            .key = arena.dupe(u8, key_s) catch return Err.Overloaded,
            .value = arena.dupe(u8, value_s) catch return Err.Overloaded,
        } };
    }
    if (std.mem.eql(u8, op_name, "attr_in")) {
        const inner = switch (val) {
            .object => |o| o,
            else => return Err.BadRequest,
        };
        const key_v = inner.get("key") orelse return Err.BadRequest;
        const values_v = inner.get("values") orelse return Err.BadRequest;
        const key_s = switch (key_v) {
            .string => |sx| sx,
            else => return Err.BadRequest,
        };
        if (key_s.len > MAX_STRING_LEN) return Err.BadRequest;
        const arr = switch (values_v) {
            .array => |a| a,
            else => return Err.BadRequest,
        };
        // N2 — empty attr_in values list matches nothing forever; same
        // bug class as empty kind_in / all_of / any_of. Refuse at parse.
        if (arr.items.len == 0) return Err.BadRequest;
        const out_vals = arena.alloc([]const u8, arr.items.len) catch return Err.Overloaded;
        for (arr.items, 0..) |child, i| {
            const s = switch (child) {
                .string => |sx| sx,
                else => return Err.BadRequest,
            };
            if (s.len > MAX_STRING_LEN) return Err.BadRequest;
            out_vals[i] = arena.dupe(u8, s) catch return Err.Overloaded;
        }
        return .{ .attr_in = .{
            .key = arena.dupe(u8, key_s) catch return Err.Overloaded,
            .values = out_vals,
        } };
    }
    return Err.BadRequest;
}

// ---------------------------------------------------------------------------
// Predicate evaluation.
// ---------------------------------------------------------------------------

/// Caller-supplied attribute lookup for `attr_eq` / `attr_in` evaluation.
/// Given an event URN + a key, return the value (slice owned by caller's
/// buffer; valid for the duration of the lookupAttr call). Return null
/// when the URN/key has no value.
///
/// Two kinds of attrs are looked up:
///   * Event-intrinsic: `kind`, `urn`, `driver` (for driver_bound /
///     driver_unbound). These are answered without touching any
///     backend — see `lookupIntrinsicAttr`.
///   * Device attrs (`vid`, `pid`, `bInterfaceClass`, …): these
///     require an `inspect` call. The server provides a closure that
///     wraps `facade.inspect()` and pulls the attr out. v1 cost: one
///     inspect per event per attr_eq term; events are infrequent so
///     this is acceptable. Caching is a future tightening.
pub const AttrLookup = struct {
    ctx: ?*anyopaque,
    /// Returns a slice valid until the next call. Null on no-such-attr.
    /// May be null when no lookup is wired (then attr_eq/attr_in only
    /// match against event-intrinsic attrs).
    get: ?*const fn (ctx: ?*anyopaque, urn: []const u8, key: []const u8) ?[]const u8,
};

/// True iff `ev` matches `pred`. `lookup` is consulted for `attr_eq`
/// / `attr_in` terms when the key isn't event-intrinsic.
pub fn evaluate(
    pred: *const Predicate,
    ev: *const Event,
    lookup: AttrLookup,
) bool {
    return switch (pred.*) {
        .matches_all => true,
        .all_of => |children| {
            for (children) |*c| {
                if (!evaluate(c, ev, lookup)) return false;
            }
            return true;
        },
        .any_of => |children| {
            for (children) |*c| {
                if (evaluate(c, ev, lookup)) return true;
            }
            return false;
        },
        .not => |child| !evaluate(child, ev, lookup),
        .kind_in => |kinds| {
            const ev_kind = EventKind.fromEvent(ev);
            for (kinds) |k| if (k == ev_kind) return true;
            return false;
        },
        .urn_glob => |glob| {
            const ev_urn = eventUrn(ev) orelse return false;
            return Urn.from(ev_urn).matches(glob);
        },
        .attr_eq => |ae| {
            const ev_urn_opt = eventUrn(ev);
            // Intrinsic-first lookup (no syscalls).
            if (lookupIntrinsicAttr(ev, ae.key)) |v| {
                return std.mem.eql(u8, v, ae.value);
            }
            // Fall back to device-attr lookup via the closure.
            if (lookup.get) |fn_| if (ev_urn_opt) |ev_urn| {
                if (fn_(lookup.ctx, ev_urn, ae.key)) |v| {
                    return std.mem.eql(u8, v, ae.value);
                }
            };
            return false;
        },
        .attr_in => |ai| {
            const ev_urn_opt = eventUrn(ev);
            if (lookupIntrinsicAttr(ev, ai.key)) |v| {
                for (ai.values) |candidate| {
                    if (std.mem.eql(u8, v, candidate)) return true;
                }
                return false;
            }
            if (lookup.get) |fn_| if (ev_urn_opt) |ev_urn| {
                if (fn_(lookup.ctx, ev_urn, ai.key)) |v| {
                    for (ai.values) |candidate| {
                        if (std.mem.eql(u8, v, candidate)) return true;
                    }
                }
            };
            return false;
        },
    };
}

/// The event's URN, when the variant carries one. Gap carries none.
///
/// Lifetime note: we read the URN's bytes through `ev` (the caller's
/// pointer), not through a switch-capture value. A switch on `ev.*`
/// copies the union into a temporary; payload slices taken from that
/// temporary become dangling the moment this function returns — a
/// bug that manifests intermittently depending on call-stack reuse.
/// Going through `ev.*.<variant>` keeps the slice anchored to the
/// caller's Event lifetime.
pub fn eventUrn(ev: *const Event) ?[]const u8 {
    return switch (ev.*) {
        .added => ev.added.urn.slice(),
        .removed => ev.removed.urn.slice(),
        .state_changed => ev.state_changed.urn.slice(),
        .ownership_changed => ev.ownership_changed.urn.slice(),
        .faulted => ev.faulted.urn.slice(),
        .driver_bound => ev.driver_bound.urn.slice(),
        .driver_unbound => ev.driver_unbound.urn.slice(),
        .gap => null,
    };
}

/// Event-intrinsic attr lookup: keys that are answerable WITHOUT a
/// backend round-trip. Today's set:
///   * `kind` → the event's tag name (`"added"`, `"driver_bound"`, …)
///   * `urn`  → the event's URN (when it carries one)
///   * `driver` → for driver_bound / driver_unbound, the driver name
///
/// Returns null when the key isn't intrinsic; the evaluator then
/// falls back to the device-attr lookup closure (if any).
pub fn lookupIntrinsicAttr(ev: *const Event, key: []const u8) ?[]const u8 {
    if (std.mem.eql(u8, key, "kind")) return @tagName(EventKind.fromEvent(ev));
    if (std.mem.eql(u8, key, "urn")) return eventUrn(ev);
    if (std.mem.eql(u8, key, "driver")) return switch (ev.*) {
        // Same lifetime trick as `eventUrn` — read through `ev`, not
        // through a switch-capture, so the returned slice stays
        // anchored to the caller's Event.
        .driver_bound => ev.driver_bound.driver.slice(),
        .driver_unbound => ev.driver_unbound.driver.slice(),
        else => null,
    };
    return null;
}

// ---------------------------------------------------------------------------
// Subscription + ring buffer.
// ---------------------------------------------------------------------------

/// One subscription's bounded queue. Fixed-size ring; on overflow the
/// OLDEST event is dropped to make room, and a one-shot `overflow_seen`
/// flag is raised. The flag surfaces as a synthetic
/// `gap{source=subscription_overflow}` event at the next `drainHead`
/// — guaranteeing the consumer sees ONE honest gap per overflow burst
/// without growing the queue past its cap. Spec model: Part 13 §5.4.2.
/// Gap-honest at the subscription boundary.
///
/// Why drain-time synthesis (not push-time inline): writing the gap
/// in-place during overflow would either consume a queue slot — which
/// would then overflow itself on the next push — or overwrite the
/// displacing event we're trying to make room for. Drain-time is the
/// clean point; the gap surfaces once per burst, then the flag resets
/// and the burst can re-arm.
const RingBuffer = struct {
    items: [MAX_QUEUE_LEN]Event = undefined,
    head: usize = 0, // index of oldest
    len: usize = 0,
    overflow_seen: bool = false,
    overflow_since_ms: u64 = 0,

    fn push(self: *RingBuffer, ev: Event) void {
        if (self.len < MAX_QUEUE_LEN) {
            const idx = (self.head + self.len) % MAX_QUEUE_LEN;
            self.items[idx] = ev;
            self.len += 1;
            return;
        }
        // Overflow: drop oldest by advancing head; push new at the
        // newly-vacated tail position. Mark the flag.
        if (!self.overflow_seen) {
            self.overflow_seen = true;
            self.overflow_since_ms = kabi.nowMs();
        }
        self.head = (self.head + 1) % MAX_QUEUE_LEN;
        // len stays at MAX_QUEUE_LEN; new tail index is one before the
        // dropped slot (post-advance).
        const idx = (self.head + self.len - 1) % MAX_QUEUE_LEN;
        self.items[idx] = ev;
    }

    /// Pop the oldest event, or null when empty. Surfaces a synthetic
    /// gap on FIRST pop after an overflow burst — at which point the
    /// flag clears, so the same burst doesn't keep emitting gaps.
    fn pop(self: *RingBuffer) ?Event {
        if (self.overflow_seen) {
            self.overflow_seen = false;
            return Event{ .gap = .{
                .source = .subscription_overflow,
                .since_ms = self.overflow_since_ms,
            } };
        }
        if (self.len == 0) return null;
        const ev = self.items[self.head];
        self.head = (self.head + 1) % MAX_QUEUE_LEN;
        self.len -= 1;
        return ev;
    }
};

/// One client subscription. Owned by the SubscriptionRegistry.
pub const Subscription = struct {
    /// Caller-chosen id (unique per peer). Echoed in the response.
    id_buf: [MAX_SUBSCRIPTION_ID_LEN]u8 = undefined,
    id_len: u8 = 0,
    /// Per-connection owner key (the broker uses the client cfd —
    /// connection-unique while live). Subscriptions are reaped via
    /// `reapPeer(peer_key)` from the server's handleClient defer
    /// when the connection closes.
    peer_key: u64,
    /// URN glob (e.g. "hw:/usb/**"). Pre-filter: events whose URN
    /// doesn't match the scope are dropped before predicate eval.
    scope_buf: [128]u8 = undefined,
    scope_len: u8 = 0,
    /// Per-subscription arena — predicate AST + its inner strings
    /// live here. Freed in one shot on teardown.
    arena: std.heap.ArenaAllocator,
    predicate: Predicate,
    /// Bounded queue (per §5.4.2).
    queue: RingBuffer = .{},
    /// R2-A3 — eval-in-progress refcount. `pushFanout` increments this
    /// under the registry lock for every subscription it's about to
    /// evaluate OUTSIDE the lock, and decrements it (under the lock)
    /// when done. `reapPeer`/`unregister` must NOT free a subscription
    /// whose `refs > 0` — a fan-out thread is reading its predicate AST;
    /// they mark it `dead` + unlink it and defer the free to whichever
    /// side drops `refs` to zero. Always accessed under `registry.lock`.
    refs: u32 = 0,
    /// R2-A3 — set when `reapPeer`/`unregister` unlinked this sub while
    /// `refs > 0` (deferred free). A dead sub is already removed from the
    /// registry slots; `pushFanout`'s commit phase skips delivering to it
    /// and frees it when the last in-flight eval releases its ref.
    dead: bool = false,

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

// ---------------------------------------------------------------------------
// SubscriptionRegistry — module-level singleton.
// ---------------------------------------------------------------------------

/// Module-level registry. Access only via the `register` / `unregister`
/// / `pushFanout` / `drainSubscription` / `reapPeer` helpers; those
/// take the mutex internally. Direct access would race with other
/// threads (the broker is thread-per-client; Part 7.1).
const Registry = struct {
    lock: kabi.SpinMutex = .{},
    // Subscriptions are heap-allocated so the slice can hand out
    // stable pointers as it grows; backing storage is a pre-sized
    // optional slot table to avoid an allocator dependency at the
    // module scope.
    slots: [MAX_TOTAL_SUBSCRIPTIONS]?*Subscription = [_]?*Subscription{null} ** MAX_TOTAL_SUBSCRIPTIONS,
    /// The allocator used to back Subscription structs + their arenas.
    /// Installed by `init`; nullable so importing the module without
    /// using it is harmless.
    alloc: ?std.mem.Allocator = null,
};

var registry: Registry = .{};

/// Install the backing allocator. Call once during broker startup
/// (main.zig). Safe to call multiple times (idempotent — the latest
/// allocator wins, but existing subscriptions are unaffected).
pub fn init(alloc: std.mem.Allocator) void {
    registry.lock.lock();
    defer registry.lock.unlock();
    registry.alloc = alloc;
}

/// Reset the registry — frees every subscription + clears all slots.
/// Used by tests; production teardown happens at process exit so this
/// is not strictly required there.
pub fn deinit() void {
    registry.lock.lock();
    defer registry.lock.unlock();
    for (&registry.slots) |*slot| {
        if (slot.*) |sub| {
            sub.arena.deinit();
            if (registry.alloc) |a| a.destroy(sub);
        }
        slot.* = null;
    }
}

/// Register a new subscription. Returns the assigned subscription
/// (the same id the caller supplied; we just echo it back). Fails
/// BadRequest if the id is empty / too long / a duplicate-for-this-peer,
/// Overloaded if registry / per-peer caps are hit.
pub fn register(
    peer_key: u64,
    id_str: []const u8,
    scope: []const u8,
    predicate: Predicate,
    arena: std.heap.ArenaAllocator,
) Err!*Subscription {
    if (id_str.len == 0 or id_str.len > MAX_SUBSCRIPTION_ID_LEN) {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.BadRequest;
    }
    if (scope.len > 128) {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.BadRequest;
    }

    registry.lock.lock();
    defer registry.lock.unlock();

    const alloc = registry.alloc orelse {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.Unavailable;
    };

    // Per-peer dedup + cap check.
    var per_peer: usize = 0;
    for (registry.slots) |slot| {
        const sub = slot orelse continue;
        if (sub.peer_key != peer_key) continue;
        per_peer += 1;
        if (std.mem.eql(u8, sub.id(), id_str)) {
            var arena_mut = arena;
            arena_mut.deinit();
            return Err.BadRequest; // duplicate id for this peer
        }
    }
    if (per_peer >= MAX_SUBSCRIPTIONS_PER_PEER) {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.Overloaded;
    }

    // Find a free slot.
    var free_slot: ?usize = null;
    for (registry.slots, 0..) |slot, i| {
        if (slot == null) {
            free_slot = i;
            break;
        }
    }
    const slot_idx = free_slot orelse {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.Overloaded;
    };

    const sub = alloc.create(Subscription) catch {
        var arena_mut = arena;
        arena_mut.deinit();
        return Err.Overloaded;
    };
    sub.* = .{
        .peer_key = peer_key,
        .arena = arena,
        .predicate = predicate,
    };
    @memcpy(sub.id_buf[0..id_str.len], id_str);
    sub.id_len = @intCast(id_str.len);
    @memcpy(sub.scope_buf[0..scope.len], scope);
    sub.scope_len = @intCast(scope.len);
    registry.slots[slot_idx] = sub;
    return sub;
}

/// Free a subscription's backing memory (arena + struct). MUST be called
/// with `registry.lock` held AND only when `sub.refs == 0` (no fan-out
/// thread is reading its predicate). R2-A3 — the single free site;
/// `unregister`/`reapPeer`/`pushFanout` route here once it's safe.
fn freeSubLocked(sub: *Subscription) void {
    sub.arena.deinit();
    if (registry.alloc) |a| a.destroy(sub);
}

/// Unlink `sub` from its slot and free it — or, if a fan-out eval is
/// in flight (`refs > 0`), mark it `dead` and defer the free to the
/// eval's commit phase. Caller holds `registry.lock`. `slot` is the
/// table cell pointing at `sub` (cleared here so no new fan-out picks
/// it up). R2-A3.
fn retireSubLocked(slot: *?*Subscription, sub: *Subscription) void {
    slot.* = null; // unlink first — no new pushFanout will snapshot it
    if (sub.refs > 0) {
        sub.dead = true; // deferred free: the last in-flight eval frees it
    } else {
        freeSubLocked(sub);
    }
}

/// Tear down `(peer_key, id_str)`. Returns `Err.NotFound` if no
/// such subscription exists (idempotency = caller's responsibility).
pub fn unregister(peer_key: u64, id_str: []const u8) Err!void {
    registry.lock.lock();
    defer registry.lock.unlock();
    for (&registry.slots) |*slot| {
        const sub = slot.* orelse continue;
        if (sub.peer_key != peer_key) continue;
        if (!std.mem.eql(u8, sub.id(), id_str)) continue;
        retireSubLocked(slot, sub);
        return;
    }
    return Err.NotFound;
}

/// Reap every subscription owned by `peer_key`. Called from the
/// server's per-client teardown path when a peer disconnects.
pub fn reapPeer(peer_key: u64) void {
    registry.lock.lock();
    defer registry.lock.unlock();
    for (&registry.slots) |*slot| {
        const sub = slot.* orelse continue;
        if (sub.peer_key != peer_key) continue;
        retireSubLocked(slot, sub);
    }
}

/// Fan one event out to EVERY active subscription whose scope matches
/// the event's URN AND whose predicate evaluates true. Called from
/// the server's `drain_events` formatter once per event pulled from
/// the underlying backend's buffer.
///
/// `lookup` is the device-attr resolver — provided by the server so
/// the predicate evaluator can fetch USB/PCI attrs without subscription.zig
/// importing the facade.
///
/// R2-A3 — three phases so the (potentially slow, sysfs-backed) predicate
/// evaluation runs OUTSIDE `registry.lock`, without a use-after-free
/// against a concurrent `reapPeer`/`unregister`:
///
///   1. **Snapshot (locked):** walk the slots, apply the cheap scope
///      pre-filter, and for each candidate increment its `refs` and
///      record the pointer. `refs > 0` pins the subscription: a
///      concurrent reap unlinks + marks it `dead` but will NOT free it.
///   2. **Evaluate (UNLOCKED):** run `evaluate(predicate, ev, lookup)`
///      for each snapshotted sub. Safe to deref the predicate AST — the
///      ref pin keeps the arena alive even if the peer disconnects mid-
///      eval. The slow `lookup` no longer holds the registry lock.
///   3. **Commit (locked):** for each sub, deliver to its queue iff it
///      matched AND isn't `dead` (a reaped sub's client is gone); drop
///      the ref; free the sub if this was the last ref AND it's `dead`.
///
/// CONTRACT (relaxed by R2-A3): because eval now runs unlocked, the
/// `lookup` callback MAY re-enter this module if it ever needs to — the
/// old "must not re-enter or it deadlocks" hazard is gone. (In practice
/// `lookup` is the facade's sysfs fetch and still has no reason to.)
pub fn pushFanout(ev: Event, lookup: AttrLookup) void {
    // Phase 1 — snapshot scope-matching candidates under the lock,
    // pinning each with a ref so it can't be freed during eval.
    var snap: [MAX_TOTAL_SUBSCRIPTIONS]*Subscription = undefined;
    var n: usize = 0;
    {
        registry.lock.lock();
        defer registry.lock.unlock();
        for (&registry.slots) |*slot| {
            const sub = slot.* orelse continue;
            // Scope pre-filter: a gap event has no URN; let it through to
            // every subscription (gap-honest — consumers must learn about
            // gaps regardless of their scope).
            if (eventUrn(&ev)) |ev_urn| {
                const ev_urn_obj = Urn.from(ev_urn);
                if (!ev_urn_obj.matches(sub.scope())) continue;
            }
            sub.refs += 1;
            snap[n] = sub;
            n += 1;
        }
    }

    // Phase 2 — evaluate predicates OUTSIDE the lock. The ref pin makes
    // dereferencing each sub's predicate AST safe even if its peer is
    // being reaped concurrently. `matched[i]` records the verdict.
    var matched: [MAX_TOTAL_SUBSCRIPTIONS]bool = undefined;
    for (snap[0..n], 0..) |sub, i| {
        matched[i] = evaluate(&sub.predicate, &ev, lookup);
    }

    // Phase 3 — commit under the lock: deliver to live matches, release
    // the ref, and free any sub whose last in-flight eval just ended
    // after it was reaped (deferred free).
    {
        registry.lock.lock();
        defer registry.lock.unlock();
        for (snap[0..n], 0..) |sub, i| {
            if (matched[i] and !sub.dead) sub.queue.push(ev);
            sub.refs -= 1;
            if (sub.refs == 0 and sub.dead) freeSubLocked(sub);
        }
    }
}

/// Drain up to `max` events from `(peer_key, id_str)`'s queue
/// into `out`. Returns the number of events written. `Err.NotFound`
/// when the subscription doesn't exist (or belongs to a different
/// peer — caller's identity is enforced here, not by the dispatcher).
pub fn drainSubscription(
    peer_key: u64,
    id_str: []const u8,
    out: []Event,
) Err!usize {
    registry.lock.lock();
    defer registry.lock.unlock();
    for (&registry.slots) |*slot| {
        const sub = slot.* orelse continue;
        if (sub.peer_key != peer_key) continue;
        if (!std.mem.eql(u8, sub.id(), id_str)) continue;
        var n: usize = 0;
        while (n < out.len) {
            const ev = sub.queue.pop() orelse break;
            out[n] = ev;
            n += 1;
        }
        return n;
    }
    return Err.NotFound;
}

/// Test-only helper: how many subscriptions are registered right now.
pub fn count() usize {
    registry.lock.lock();
    defer registry.lock.unlock();
    var n: usize = 0;
    for (registry.slots) |slot| if (slot != null) {
        n += 1;
    };
    return n;
}
