//! event_pump — the broker's own hardware-event drainer (M-B chunk B2).
//!
//! ## The defect this closes
//!
//! Before B2 the netlink uevent socket was drained by *whichever client's*
//! `drain_events` call got there first. Two operators watching the same
//! bus did not both see a device appear — they RACED for it, and the loser
//! silently saw nothing. Worse, with nobody polling, the broker's own
//! subscription engine never fanned anything out at all: `pushFanout` was
//! reachable only as a side effect of a client poll.
//!
//! One thread here ends both. The broker polls every worker backend's
//! watch fd itself, drains on readiness, and hands each event to
//! `subscription.pushFanout`. Clients never touch a backend again: they
//! read from their own per-subscription ring, so "who drained first" stops
//! being a question anyone can ask.
//!
//! ## Shape
//!
//! * One thread, spawned in `main()` beside the accept loop.
//! * `poll()` over each registered source's fd plus a shutdown `eventfd`.
//! * A bounded tick (`TICK_MS`) even when no fd is ready — NOT idle
//!   spinning. `LinuxUsbBackend.pumpUevent` flushes the watch
//!   synthesiser's debounced pending removes at the head of every pass
//!   (`usb_synth.flushExpired`), so a remove held inside the debounce
//!   window on an otherwise-quiet bus needs a pass with no readiness to
//!   surface. Pre-B2 the SDK's 0.5 s client poll supplied those passes;
//!   the tick is where that guarantee now lives. Without it, a remove on
//!   a bus that then goes quiet would be held forever — a silent loss.
//! * The pump owns NO event policy: it does not filter, coalesce or
//!   interpret. It moves events from the backend seam to the fan-out.
//!
//! ## What the pump deliberately does not import
//!
//! The facade. A source is `{fd, ctx, drain-fn}` — precisely the shape of
//! the `KernelBackend` vtable slot — so `server.zig` does the wiring by
//! walking its workers, and this module stays testable against any
//! fd + function pair (a pipe and a closure, in `event_pump_test.zig`).

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

const linux = std.os.linux;
const Err = model.Err;

/// Ceiling on registered event sources. One per worker backend that has a
/// buffered event source; the broker ships three workers today (pci, usb,
/// test) and only USB has one. 8 is generous headroom, and a fixed array
/// keeps the pump allocator-free like the rest of the boot path.
pub const MAX_SOURCES: usize = 8;

/// Poll tick, ms. See the module doc: this is the synthesiser's
/// debounce-flush guarantee, not a poll interval for events (events wake
/// the poll immediately via their fd).
pub const TICK_MS: i32 = 250;

/// Per-source scope cap — mirrors `model.Urn`'s capacity.
pub const MAX_SCOPE_BYTES: usize = 128;

/// The backend's `drain_events` vtable slot, by value. Identical signature
/// so `server.zig` can hand over `backend.vtable.drain_events.?` directly.
pub const DrainFn = *const fn (
    ctx: *anyopaque,
    scope: []const u8,
    sink: *const kabi.EventSink,
    deadline: kabi.Deadline,
) Err!void;

/// One pollable event source: a readiness fd plus the drain to run when it
/// fires.
pub const Source = struct {
    fd: i32,
    ctx: *anyopaque,
    drain: DrainFn,
    scope_buf: [MAX_SCOPE_BYTES]u8 = undefined,
    scope_len: u8 = 0,
    /// Set when a drain returned an error. A source whose fd stays ready
    /// while its drain keeps failing would spin the pump at 100% CPU, so
    /// the first failure retires it (loudly). The events it would have
    /// carried are lost — which is why the retirement announces itself on
    /// stderr rather than being swallowed.
    disabled: bool = false,

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

/// Per-drain budget. A backend drain is non-blocking by contract
/// (`pumpUevent` recvs with MSG.DONTWAIT until EAGAIN); the deadline is
/// the belt to that braces, so one wedged backend cannot stop the pump
/// from servicing the others.
const DRAIN_BUDGET_MS: u64 = 1_000;

pub const Pump = struct {
    sources: [MAX_SOURCES]Source = undefined,
    n: usize = 0,
    /// Shutdown eventfd — the one thing besides a source fd the poll set
    /// waits on. `stop()` writes to it; the thread sees POLLIN and exits.
    /// A pipe would do, but eventfd is one fd instead of two and cannot
    /// half-close.
    stop_fd: i32 = -1,
    thread: ?std.Thread = null,
    /// Observability + test synchronisation: how many poll passes have
    /// completed. Monotonic; wraps never in practice.
    passes: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
    /// How many events this pump has forwarded to the fan-out.
    forwarded: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),

    /// Register a source. Rejects past `MAX_SOURCES` or an over-long
    /// scope. Must be called BEFORE `start` — the source table is not
    /// synchronised, deliberately: the wiring is a startup act, and a
    /// lock here would be a lie about a mutability that does not exist.
    pub fn addSource(
        self: *Pump,
        fd: i32,
        ctx: *anyopaque,
        drain: DrainFn,
        scope: []const u8,
    ) Err!void {
        if (self.thread != null) return Err.Conflict; // wiring is a startup act
        if (self.n >= MAX_SOURCES) return Err.Overloaded;
        if (scope.len > MAX_SCOPE_BYTES) return Err.BadRequest;
        var s = Source{ .fd = fd, .ctx = ctx, .drain = drain };
        @memcpy(s.scope_buf[0..scope.len], scope);
        s.scope_len = @intCast(scope.len);
        self.sources[self.n] = s;
        self.n += 1;
    }

    /// Spawn the pump thread. Idempotent-unsafe by design: calling it
    /// twice is a programming error, not a runtime condition, so it
    /// returns `Err.Conflict` rather than silently doing nothing.
    pub fn start(self: *Pump) Err!void {
        if (self.thread != null) return Err.Conflict;
        const efd_rc = linux.eventfd(0, linux.EFD.CLOEXEC | linux.EFD.NONBLOCK);
        if (linux.errno(efd_rc) != .SUCCESS) return Err.Unavailable;
        self.stop_fd = @intCast(efd_rc);
        self.thread = std.Thread.spawn(.{}, run, .{self}) catch {
            _ = linux.close(self.stop_fd);
            self.stop_fd = -1;
            return Err.Unavailable;
        };
    }

    /// Signal the thread, join it, close the eventfd. Safe to call on a
    /// pump that was never started, and safe to call twice — the broker's
    /// teardown paths are not all reachable from one place, so an
    /// unstarted/already-stopped `stop()` must be a no-op, not a crash.
    pub fn stop(self: *Pump) void {
        const t = self.thread orelse return;
        const one: u64 = 1;
        _ = linux.write(self.stop_fd, @ptrCast(&one), @sizeOf(u64));
        t.join();
        self.thread = null;
        _ = linux.close(self.stop_fd);
        self.stop_fd = -1;
    }
};

/// The sink every source drains into: straight to the broker-wide
/// subscription fan-out. `AttrLookup` stays null-wired, matching every
/// other `pushFanout` call site — predicate evaluation against
/// event-intrinsic attrs only (the inspect-on-event escape hatch is still
/// deferred behind a perf-aware cache).
fn pumpPush(ctx: *anyopaque, ev: model.Event) void {
    const counter: *std.atomic.Value(u64) = @ptrCast(@alignCast(ctx));
    _ = counter.fetchAdd(1, .monotonic);
    subscription.pushFanout(ev, .{ .ctx = null, .get = null });
}

fn run(self: *Pump) void {
    var fds: [MAX_SOURCES + 1]linux.pollfd = undefined;
    const sink = kabi.EventSink{ .ctx = @ptrCast(&self.forwarded), .push = pumpPush };

    while (true) {
        // Rebuild the poll set each pass: a source retired mid-run must
        // drop out of it, and the set is at most 9 entries.
        var nfds: usize = 0;
        for (self.sources[0..self.n]) |*s| {
            if (s.disabled) continue;
            fds[nfds] = .{ .fd = s.fd, .events = linux.POLL.IN, .revents = 0 };
            nfds += 1;
        }
        const stop_idx = nfds;
        fds[nfds] = .{ .fd = self.stop_fd, .events = linux.POLL.IN, .revents = 0 };
        nfds += 1;

        const rc = linux.poll(&fds, @intCast(nfds), TICK_MS);
        const e = linux.errno(rc);
        if (e != .SUCCESS and e != .INTR) {
            std.debug.print(
                "broker: event pump poll failed errno={s} — pump stopping; " ++
                    "hardware events will no longer reach subscribers until restart\n",
                .{@tagName(e)},
            );
            return;
        }
        if (fds[stop_idx].revents != 0) return; // shutdown (or the eventfd died)

        // Drain EVERY live source on every pass, ready or not. Two
        // reasons, and the second is the load-bearing one:
        //   1. A drain is non-blocking and cheap when there is nothing
        //      to read (one recv → EAGAIN).
        //   2. `pumpUevent` flushes the watch synthesiser's debounced
        //      pending removes at the head of each call. Those have no
        //      fd readiness of their own — they are held events waiting
        //      for a clock, and only a pass surfaces them.
        var i: usize = 0;
        while (i < self.n) : (i += 1) {
            const s = &self.sources[i];
            if (s.disabled) continue;
            s.drain(s.ctx, s.scope(), &sink, kabi.Deadline.fromNow(DRAIN_BUDGET_MS)) catch |err| {
                s.disabled = true;
                std.debug.print(
                    "broker: event pump retiring source fd={d} scope={s} after drain error {s} — " ++
                        "events on that backend will no longer reach subscribers\n",
                    .{ s.fd, s.scope(), @errorName(err) },
                );
            };
        }
        _ = self.passes.fetchAdd(1, .monotonic);
    }
}
