//! Live query fanout for `GET /api/queries/live` (PLAN §11.4:455).
//!
//! The DNS query path publishes through `QuerySink`, which calls `publish`
//! before it hands the same entry to the logger: the event stream must never
//! wait on a database. `publish` therefore copies and returns — it allocates
//! nothing, touches no I/O, and holds one mutex across a scan of 32 slots.
//!
//! A subscriber that cannot keep up loses its stream rather than the queries:
//! a full ring sets `overflowed`, the subscriber task sees the flag and ends
//! the response, and the browser's `EventSource` reconnects on its own.
//!
//! `logger.Entry` carries its own bytes, so a ring slot is a plain copy with
//! nothing borrowed from the query that produced it.

const std = @import("std");

const logger = @import("../storage/logger.zig");

pub const Entry = logger.Entry;

/// Concurrent live streams. The per-IP cap (`web.sse_max_connections_per_ip`)
/// keeps one client from taking all of them; `subscribe` returning null is the
/// backstop and answers 503.
pub const max_subscribers = 32;

/// Entries one subscriber may fall behind by. At household query rates this is
/// several seconds of slack on a stalled TCP connection.
///
/// The ring is embedded in the slot, so this multiplies `@sizeOf(logger.Entry)`
/// — about 1.8 KiB once milestone 28 widened it for provenance. One subscriber
/// therefore costs roughly 115 KiB of ring and the whole hub roughly 3.6 MiB,
/// allocated once for the life of the process. That is the reason to keep both
/// this and `max_subscribers` small: they are paid whether or not anyone is
/// watching. `logger.query_log_buffer_max` bounds the other consumer of the
/// same width, the writer queue.
pub const ring_capacity = 64;

pub const SubscriberId = enum(u8) { _ };

/// What `wait` returns: an entry (or the overflow flag) is ready, the caller's
/// timeout passed and it owes the client a heartbeat, or the hub is closing and
/// the subscriber must end its response now.
pub const Wake = enum { ready, timeout, closed };

pub const Hub = struct {
    /// Guards every field of every slot. `publish` runs on the DNS hot path,
    /// so the critical section is copies and flag writes only.
    mutex: std.Io.Mutex,
    /// Milestone-16 ruling 11. Set once, never cleared: a hub that is closing
    /// belongs to a server that is going away.
    closing: bool,
    slots: [max_subscribers]Slot,

    const Slot = struct {
        active: bool,
        /// Set by `publish` when the ring is full. Never cleared while the
        /// subscriber lives: the stream it belongs to is over.
        overflowed: bool,
        head: u32,
        len: u32,
        event: std.Io.Event,
        ring: [ring_capacity]Entry,
    };

    /// Initializes in place. The rings are close to a megabyte, which a
    /// by-value `init` would copy through the caller's frame.
    ///
    /// The ring storage stays undefined: `len` says which slots hold entries.
    pub fn init(self: *Hub) void {
        self.mutex = .init;
        self.closing = false;
        for (&self.slots) |*slot| {
            slot.active = false;
            slot.overflowed = false;
            slot.head = 0;
            slot.len = 0;
            slot.event = .unset;
        }
    }

    /// Claims a slot, or null when all 32 are taken.
    ///
    /// `lockUncancelable` throughout this file: `publish`'s caller is
    /// `Handler.handle`, which has no error union to carry `error.Canceled`
    /// out of (the same reasoning as `clients.Tracker.track`), and the rest of
    /// the surface shares the mutex with it.
    pub fn subscribe(self: *Hub, io: std.Io) ?SubscriberId {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);

        for (&self.slots, 0..) |*slot, index| {
            if (slot.active) continue;
            slot.active = true;
            slot.overflowed = false;
            slot.head = 0;
            slot.len = 0;
            slot.event = .unset;
            return @enumFromInt(index);
        }
        return null;
    }

    /// Releases the slot. The caller must not be waiting on it.
    pub fn unsubscribe(self: *Hub, io: std.Io, id: SubscriberId) void {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);

        const slot = self.slotOf(id);
        slot.active = false;
        slot.overflowed = false;
        slot.len = 0;
        slot.head = 0;
    }

    /// Milestone-16 ruling 11. Ends every live stream.
    ///
    /// Without this, a graceful drain waits out one heartbeat interval per idle
    /// subscriber: shutting the sockets down does not reach a task parked inside
    /// `wait`, which is waiting on an event, not on the peer. Called before the
    /// web listener begins its shutdown.
    pub fn close(self: *Hub, io: std.Io) void {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);

        self.closing = true;
        for (&self.slots) |*slot| {
            if (slot.active) slot.event.set(io);
        }
    }

    /// Copies `entry` into every live ring and wakes its subscriber. Called
    /// once per logged query.
    pub fn publish(self: *Hub, io: std.Io, entry: Entry) void {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);

        for (&self.slots) |*slot| {
            if (!slot.active or slot.overflowed) continue;
            if (slot.len == ring_capacity) {
                slot.overflowed = true;
            } else {
                slot.ring[(slot.head + slot.len) % ring_capacity] = entry;
                slot.len += 1;
            }
            slot.event.set(io);
        }
    }

    /// The oldest entry this subscriber has not seen, or null when its ring is
    /// empty. Check `overflowed` first: entries that predate the overflow are
    /// still readable, but the stream must end once they run out.
    pub fn next(self: *Hub, io: std.Io, id: SubscriberId) ?Entry {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);

        const slot = self.slotOf(id);
        if (slot.len == 0) return null;
        const entry = slot.ring[slot.head];
        slot.head = (slot.head + 1) % ring_capacity;
        slot.len -= 1;
        return entry;
    }

    /// True once this subscriber missed an entry. The subscriber task ends the
    /// response when it sees this.
    pub fn overflowed(self: *Hub, io: std.Io, id: SubscriberId) bool {
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);
        return self.slotOf(id).overflowed;
    }

    /// Blocks until something is ready for this subscriber, `timeout` passes,
    /// or the hub closes; `.timeout` is the heartbeat's cue and `.closed` ends
    /// the stream.
    ///
    /// The event is reset under the mutex and only while the ring is empty, so
    /// a `publish` that lands between the check and the wait sets the event
    /// again and the wait returns at once. Only the owning subscriber task
    /// calls this, which is what `Event.reset` requires (`Io.zig:1866`).
    ///
    /// A spurious futex wakeup reports `.timeout` (`Io.zig:1824`): the caller
    /// sends one heartbeat it did not strictly owe.
    pub fn wait(
        self: *Hub,
        io: std.Io,
        id: SubscriberId,
        timeout: std.Io.Clock.Duration,
    ) std.Io.Cancelable!Wake {
        self.mutex.lockUncancelable(io);
        if (self.closing) {
            self.mutex.unlock(io);
            return .closed;
        }
        const slot = self.slotOf(id);
        if (slot.len > 0 or slot.overflowed) {
            self.mutex.unlock(io);
            return .ready;
        }
        slot.event.reset();
        self.mutex.unlock(io);

        slot.event.waitTimeout(io, .{ .duration = timeout }) catch |err| switch (err) {
            error.Timeout => return .timeout,
            error.Canceled => |e| return e,
        };

        // `close` wakes the same event a publish does, so the flag is what tells
        // the two apart. Reading it here rather than on the next call through
        // keeps the drain from writing one more frame into a dying connection.
        self.mutex.lockUncancelable(io);
        defer self.mutex.unlock(io);
        return if (self.closing) .closed else .ready;
    }

    fn slotOf(self: *Hub, id: SubscriberId) *Slot {
        const slot = &self.slots[@intFromEnum(id)];
        std.debug.assert(slot.active);
        return slot;
    }
};

// ---------------------------------------------------------------------------
// tests
// ---------------------------------------------------------------------------

const testing = std.testing;

fn sampleEntry(timestamp: i64, domain: []const u8) Entry {
    return .init(.{
        .timestamp = timestamp,
        .domain = domain,
        .client_ip = "192.0.2.10",
        .qtype = 1,
    });
}

fn newHub(gpa: std.mem.Allocator) !*Hub {
    const hub = try gpa.create(Hub);
    hub.init();
    return hub;
}

test "a subscriber reads what was published, oldest first" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    hub.publish(io, sampleEntry(1, "first.example"));
    hub.publish(io, sampleEntry(2, "second.example"));

    try testing.expectEqualStrings("first.example", hub.next(io, id).?.domain());
    try testing.expectEqualStrings("second.example", hub.next(io, id).?.domain());
    try testing.expect(hub.next(io, id) == null);
    try testing.expect(!hub.overflowed(io, id));
}

test "an entry published before a subscription is not delivered" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    hub.publish(io, sampleEntry(1, "early.example"));

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);
    try testing.expect(hub.next(io, id) == null);
}

test "every live subscriber receives its own copy" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const first = hub.subscribe(io).?;
    const second = hub.subscribe(io).?;
    defer hub.unsubscribe(io, first);
    defer hub.unsubscribe(io, second);

    hub.publish(io, sampleEntry(7, "shared.example"));

    try testing.expectEqualStrings("shared.example", hub.next(io, first).?.domain());
    try testing.expectEqualStrings("shared.example", hub.next(io, second).?.domain());
}

test "the hub hands out every slot and then refuses" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    var ids: [max_subscribers]SubscriberId = undefined;
    for (&ids) |*id| id.* = hub.subscribe(io).?;
    try testing.expect(hub.subscribe(io) == null);

    hub.unsubscribe(io, ids[3]);
    const reused = hub.subscribe(io).?;
    try testing.expectEqual(ids[3], reused);
}

test "a full ring marks the subscriber overflowed and stops copying" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    for (0..ring_capacity) |i| hub.publish(io, sampleEntry(@intCast(i), "fill.example"));
    try testing.expect(!hub.overflowed(io, id));

    hub.publish(io, sampleEntry(999, "lost.example"));
    try testing.expect(hub.overflowed(io, id));

    // What the ring already held is still readable; the entry that overflowed
    // it is not, and the flag stays set.
    var drained: usize = 0;
    while (hub.next(io, id)) |entry| : (drained += 1) {
        try testing.expectEqualStrings("fill.example", entry.domain());
    }
    try testing.expectEqual(@as(usize, ring_capacity), drained);
    try testing.expect(hub.overflowed(io, id));
}

test "the ring wraps around its head" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    // Two and a half laps, consuming as we go: the head passes the end of the
    // storage twice and no entry is lost.
    for (0..ring_capacity * 2 + ring_capacity / 2) |i| {
        var buf: [32]u8 = undefined;
        const domain = try std.fmt.bufPrint(&buf, "d{d}.example", .{i});
        hub.publish(io, sampleEntry(@intCast(i), domain));

        const got = hub.next(io, id).?;
        try testing.expectEqualStrings(domain, got.domain());
        try testing.expectEqual(@as(i64, @intCast(i)), got.timestamp);
    }
    try testing.expect(!hub.overflowed(io, id));
}

test "wait returns as soon as an entry is waiting" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    const long: std.Io.Clock.Duration = .{ .raw = .fromSeconds(60), .clock = .awake };
    hub.publish(io, sampleEntry(1, "ready.example"));
    try testing.expectEqual(Wake.ready, try hub.wait(io, id, long));
}

test "wait times out on an idle subscriber so the heartbeat can go out" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    const brief: std.Io.Clock.Duration = .{ .raw = .fromMilliseconds(20), .clock = .awake };
    try testing.expectEqual(Wake.timeout, try hub.wait(io, id, brief));
    try testing.expect(hub.next(io, id) == null);
}

test "a publish wakes a waiting subscriber" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    const long: std.Io.Clock.Duration = .{ .raw = .fromSeconds(60), .clock = .awake };
    var future = try io.concurrent(Hub.wait, .{ hub, io, id, long });

    hub.publish(io, sampleEntry(5, "late.example"));

    try testing.expectEqual(Wake.ready, try future.await(io));
    try testing.expectEqualStrings("late.example", hub.next(io, id).?.domain());
}

test "an overflow wakes a waiting subscriber" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    for (0..ring_capacity) |i| hub.publish(io, sampleEntry(@intCast(i), "fill.example"));
    while (hub.next(io, id)) |_| {}

    // The ring is empty again but its head sits mid-storage; refill it and
    // overflow, so the wake comes from the flag rather than from an entry.
    for (0..ring_capacity) |i| hub.publish(io, sampleEntry(@intCast(i), "fill.example"));

    const long: std.Io.Clock.Duration = .{ .raw = .fromSeconds(60), .clock = .awake };
    var future = try io.concurrent(Hub.wait, .{ hub, io, id, long });
    hub.publish(io, sampleEntry(999, "lost.example"));

    try testing.expectEqual(Wake.ready, try future.await(io));
    try testing.expect(hub.overflowed(io, id));
}

test "close wakes a parked subscriber instead of leaving it on the heartbeat" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    // Far longer than the 15 s heartbeat the handler passes, so a pass here
    // cannot come from the timeout arm.
    const long: std.Io.Clock.Duration = .{ .raw = .fromSeconds(600), .clock = .awake };
    var future = try io.concurrent(Hub.wait, .{ hub, io, id, long });

    hub.close(io);

    try testing.expectEqual(Wake.closed, try future.await(io));
}

test "wait on a closed hub returns at once, with no entry pending" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    hub.close(io);

    const long: std.Io.Clock.Duration = .{ .raw = .fromSeconds(600), .clock = .awake };
    try testing.expectEqual(Wake.closed, try hub.wait(io, id, long));
}

test "publishing while subscribers come and go reaches only the live ones" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const hub = try newHub(testing.allocator);
    defer testing.allocator.destroy(hub);

    const steady = hub.subscribe(io).?;
    defer hub.unsubscribe(io, steady);

    var churner = try io.concurrent(churn, .{ hub, io });

    var published: usize = 0;
    while (published < 500) : (published += 1) {
        hub.publish(io, sampleEntry(@intCast(published), "churn.example"));
        // Keep the steady subscriber under its ring cap: this test is about
        // the churn, not about overflow.
        while (hub.next(io, steady)) |_| {}
    }
    churner.await(io);
    try testing.expect(!hub.overflowed(io, steady));

    // Every slot the churner used is free again.
    var ids: [max_subscribers - 1]SubscriberId = undefined;
    for (&ids) |*id| id.* = hub.subscribe(io).?;
    for (ids) |id| hub.unsubscribe(io, id);
}

fn churn(hub: *Hub, io: std.Io) void {
    for (0..200) |i| {
        const id = hub.subscribe(io) orelse continue;
        if (i % 3 == 0) _ = hub.next(io, id);
        hub.unsubscribe(io, id);
    }
}
