//! mgf-hrb `admission` (wire layer) — connection + request admission control
//! (S5 / Tier-1 #9). Extracted from `server.zig` (1A-i) so the three
//! bounds are a DETERMINISTIC Zig test target — no broker spawn, no
//! wall-clock sleeps — with the time source injected per call:
//!
//!   * **Aggregate** concurrent-connection ceiling (`MAX_ACTIVE_CLIENTS`).
//!   * **Per-tenant** connection ceiling (`MAX_CONN_PER_UID`, keyed on the
//!     SO_PEERCRED uid) so one tenant can't monopolise the budget.
//!   * **Per-tenant** rate limit (token bucket, keyed on uid).
//!
//! Plus the slowloris progress-deadline VALUE (the recv loop in the
//! transport shell enforces it; this module owns the configured number +
//! the derived poll interval).
//!
//! Pure mechanism: config is injected via `configure` (the app shell reads
//! env and supplies it); time is an explicit `now_ms` argument. The only
//! syscall is the SpinMutex. State is module-global because the registries
//! are shared across the broker's per-connection threads (Part 7.1).

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

/// B2 — aggregate concurrent-connection ceiling. 128 is comfortably above
/// realistic per-host fan-out; past that, thread-stack + lock-contention
/// cost dominates, so we back-pressure rather than collapse.
pub const MAX_ACTIVE_CLIENTS: u32 = 128;

/// R-T1-9B — per-tenant connection ceiling (by uid). Below the aggregate
/// so two tenants can both make progress; a tenant past it is shed with a
/// polite `TooBusy` rather than a kernel connection-refused.
pub const MAX_CONN_PER_UID: u32 = 32;

/// R-T1-9C — slowloris guard. `SO_RCVTIMEO` is set to the derived poll
/// interval so the recv loop wakes even with no traffic; a request line
/// that hasn't completed within the deadline of its first byte is dropped.
pub const RECV_POLL_MS: u32 = 2000;
pub const SLOWLORIS_DEADLINE_MS: u64 = 10_000;

/// The polite "you've hit a connection ceiling, back off" wire line.
/// Shared by the aggregate-cap site and the per-uid-cap site. `TooBusy`
/// maps to `OverloadedError` in the SDK.
pub const TOO_BUSY_LINE: []const u8 = "{\"ok\":false,\"err\":\"TooBusy\"}\n";

// ---- R-T1-9D rate-limit constants (milli-token integer arithmetic) ------
pub const RL_SLOTS: usize = 256;
pub const RL_COST_MT: u64 = 1000; // one op = one token
pub const RL_DEFAULT_BURST_TOKENS: u64 = 2048;
pub const RL_DEFAULT_REFILL_PER_SEC: u64 = 1000;

// ===========================================================================
// Configuration (replaces the in-line env reads; the app shell supplies it).
// ===========================================================================

pub const Config = struct {
    rl_burst_tokens: u64 = RL_DEFAULT_BURST_TOKENS,
    rl_refill_per_sec: u64 = RL_DEFAULT_REFILL_PER_SEC,
    slowloris_deadline_ms: u64 = SLOWLORIS_DEADLINE_MS,
};

var rl_burst_mt: u64 = RL_DEFAULT_BURST_TOKENS * RL_COST_MT;
/// Refill in milli-tokens per millisecond. Because 1 token = 1000 mt and
/// there are 1000 ms/s, `mt/ms` equals `tokens/sec` numerically. 0 disables
/// refill (the bucket drains once and stays empty).
var rl_refill_mt_per_ms: u64 = RL_DEFAULT_REFILL_PER_SEC;
var slowloris_deadline_ms: u64 = SLOWLORIS_DEADLINE_MS;

/// Install operator config (called once at startup from the app shell,
/// which owns env parsing — this module stays env-free + testable).
pub fn configure(c: Config) void {
    rl_burst_mt = c.rl_burst_tokens * RL_COST_MT;
    rl_refill_mt_per_ms = c.rl_refill_per_sec;
    slowloris_deadline_ms = c.slowloris_deadline_ms;
}

pub fn slowlorisDeadlineMs() u64 {
    return slowloris_deadline_ms;
}

/// Recv poll interval: at most `RECV_POLL_MS`, but no coarser than ¼ of the
/// deadline (so a short test-tuned deadline is still observed promptly),
/// floored at 50 ms so we never busy-wake.
pub fn recvPollMs() u32 {
    return @intCast(@min(
        @as(u64, RECV_POLL_MS),
        @max(@as(u64, 50), slowloris_deadline_ms / 4),
    ));
}

// ===========================================================================
// Aggregate connection cap.
// ===========================================================================

var active_clients: std.atomic.Value(u32) = .init(0);

/// Try to admit one connection against the aggregate ceiling. Returns true
/// if admitted (the caller MUST later `aggregateRelease`); false at the cap
/// (caller sheds `TOO_BUSY_LINE` + closes). Atomic CAS keeps the count
/// accurate under concurrent accepts.
pub fn aggregateAcquire() bool {
    const live = active_clients.fetchAdd(1, .acq_rel);
    if (live >= MAX_ACTIVE_CLIENTS) {
        _ = active_clients.fetchSub(1, .release);
        return false;
    }
    return true;
}

/// Release an aggregate slot. Returns the live count AFTER the release
/// (for operator logging).
pub fn aggregateRelease() u32 {
    return active_clients.fetchSub(1, .release) - 1;
}

// ===========================================================================
// Per-tenant connection cap (by uid). A slot is FREE iff its count is 0
// (the uid is meaningful only when count > 0, so uid 0 = root is handled).
// Sized to MAX_ACTIVE_CLIENTS: there can never be more distinct uids than
// the aggregate cap, so the table can't fill before the aggregate already
// rejected the connection.
// ===========================================================================

var uid_conn_lock: kabi.SpinMutex = .{};
var uid_conn_uid: [MAX_ACTIVE_CLIENTS]u32 = [_]u32{0} ** MAX_ACTIVE_CLIENTS;
var uid_conn_count: [MAX_ACTIVE_CLIENTS]u32 = [_]u32{0} ** MAX_ACTIVE_CLIENTS;

/// Try to admit one more connection for `uid`. False (caller sheds TooBusy)
/// when the uid is already at `MAX_CONN_PER_UID`.
pub fn uidConnAcquire(uid: u32) bool {
    uid_conn_lock.lock();
    defer uid_conn_lock.unlock();
    var free_slot: ?usize = null;
    for (uid_conn_count, 0..) |c, i| {
        if (c == 0) {
            if (free_slot == null) free_slot = i;
            continue;
        }
        if (uid_conn_uid[i] == uid) {
            if (c >= MAX_CONN_PER_UID) return false;
            uid_conn_count[i] = c + 1;
            return true;
        }
    }
    if (free_slot) |i| {
        uid_conn_uid[i] = uid;
        uid_conn_count[i] = 1;
        return true;
    }
    // Table full (unreachable below the aggregate cap, but fail closed).
    return false;
}

/// Release one connection slot for `uid`.
pub fn uidConnRelease(uid: u32) void {
    uid_conn_lock.lock();
    defer uid_conn_lock.unlock();
    for (uid_conn_count, 0..) |c, i| {
        if (c > 0 and uid_conn_uid[i] == uid) {
            uid_conn_count[i] = c - 1;
            return;
        }
    }
}

// ===========================================================================
// Per-tenant rate limit (token bucket). State SURVIVES reconnects (else a
// flood reconnecting per op would reset its bucket and evade the limit), so
// slots aren't tied to connection lifecycle — LRU-evicted when the table
// fills. A slot is occupied iff `rl_used`.
// ===========================================================================

var rl_lock: kabi.SpinMutex = .{};
var rl_used: [RL_SLOTS]bool = [_]bool{false} ** RL_SLOTS;
var rl_uid: [RL_SLOTS]u32 = [_]u32{0} ** RL_SLOTS;
var rl_tokens_mt: [RL_SLOTS]u64 = [_]u64{0} ** RL_SLOTS;
var rl_last_ms: [RL_SLOTS]u64 = [_]u64{0} ** RL_SLOTS;

/// Charge one op against `uid`'s bucket at time `now_ms`. Returns true if a
/// token was available (op proceeds), false if over rate (shed with
/// `Err.Overloaded`). New uids start with a full burst; a full table
/// evicts the least-recently-used slot. The clock is INJECTED so unit
/// tests are deterministic — the broker passes `kabi.nowMs()`.
pub fn rlAllow(uid: u32, now_ms: u64) bool {
    rl_lock.lock();
    defer rl_lock.unlock();
    var free: ?usize = null;
    var lru: usize = 0;
    var lru_ms: u64 = std.math.maxInt(u64);
    for (0..RL_SLOTS) |i| {
        if (!rl_used[i]) {
            if (free == null) free = i;
            continue;
        }
        if (rl_uid[i] == uid) {
            const elapsed = now_ms - rl_last_ms[i];
            const refilled = rl_tokens_mt[i] + elapsed * rl_refill_mt_per_ms;
            rl_tokens_mt[i] = @min(rl_burst_mt, refilled);
            rl_last_ms[i] = now_ms;
            if (rl_tokens_mt[i] >= RL_COST_MT) {
                rl_tokens_mt[i] -= RL_COST_MT;
                return true;
            }
            return false;
        }
        if (rl_last_ms[i] < lru_ms) {
            lru_ms = rl_last_ms[i];
            lru = i;
        }
    }
    const slot = free orelse lru;
    rl_used[slot] = true;
    rl_uid[slot] = uid;
    rl_tokens_mt[slot] = if (rl_burst_mt > RL_COST_MT) rl_burst_mt - RL_COST_MT else 0;
    rl_last_ms[slot] = now_ms;
    return true;
}

/// Test-only — reset every registry + config to defaults so each unit test
/// starts from a clean module state (the globals persist across tests in a
/// single test binary).
pub fn resetForTest() void {
    active_clients.store(0, .release);
    uid_conn_uid = [_]u32{0} ** MAX_ACTIVE_CLIENTS;
    uid_conn_count = [_]u32{0} ** MAX_ACTIVE_CLIENTS;
    rl_used = [_]bool{false} ** RL_SLOTS;
    rl_uid = [_]u32{0} ** RL_SLOTS;
    rl_tokens_mt = [_]u64{0} ** RL_SLOTS;
    rl_last_ms = [_]u64{0} ** RL_SLOTS;
    configure(.{});
}

// ===========================================================================
// Unit tests — the payoff of the extraction: the admission math is now
// directly + deterministically testable (was only wire-tested, with sleeps).
// ===========================================================================

const testing = std.testing;

test "aggregate cap: admits up to MAX_ACTIVE_CLIENTS, then sheds" {
    resetForTest();
    var i: u32 = 0;
    while (i < MAX_ACTIVE_CLIENTS) : (i += 1) try testing.expect(aggregateAcquire());
    try testing.expect(!aggregateAcquire()); // at the ceiling
    // Releasing one frees a slot.
    _ = aggregateRelease();
    try testing.expect(aggregateAcquire());
}

test "per-uid conn cap: one uid bounded, another unaffected" {
    resetForTest();
    var i: u32 = 0;
    while (i < MAX_CONN_PER_UID) : (i += 1) try testing.expect(uidConnAcquire(1000));
    try testing.expect(!uidConnAcquire(1000)); // uid 1000 at its ceiling
    try testing.expect(uidConnAcquire(1001)); // different uid: fine
    uidConnRelease(1000);
    try testing.expect(uidConnAcquire(1000)); // freed a slot
}

test "rate limit: burst then shed, deterministic with injected clock" {
    resetForTest();
    configure(.{ .rl_burst_tokens = 5, .rl_refill_per_sec = 0 }); // no refill
    var i: usize = 0;
    while (i < 5) : (i += 1) try testing.expect(rlAllow(7, 0)); // burst of 5
    try testing.expect(!rlAllow(7, 0)); // 6th shed
    try testing.expect(!rlAllow(7, 1000)); // still shed — refill disabled
}

test "rate limit: bucket refills over (injected) time" {
    resetForTest();
    // 1 token/sec → 1 mt/ms; a 1000 ms gap refills exactly 1 token.
    configure(.{ .rl_burst_tokens = 2, .rl_refill_per_sec = 1 });
    try testing.expect(rlAllow(7, 0));
    try testing.expect(rlAllow(7, 0));
    try testing.expect(!rlAllow(7, 0)); // burst drained
    // 1000 ms later: exactly 1 token refilled.
    try testing.expect(rlAllow(7, 1000));
    try testing.expect(!rlAllow(7, 1000)); // only one refilled
}

test "rate limit: distinct uids have independent buckets" {
    resetForTest();
    configure(.{ .rl_burst_tokens = 1, .rl_refill_per_sec = 0 });
    try testing.expect(rlAllow(1, 0));
    try testing.expect(!rlAllow(1, 0)); // uid 1 drained
    try testing.expect(rlAllow(2, 0)); // uid 2 independent
}

test "recvPollMs: ¼ of the deadline, clamped" {
    resetForTest();
    configure(.{ .slowloris_deadline_ms = 10_000 });
    try testing.expectEqual(@as(u32, RECV_POLL_MS), recvPollMs()); // 2500 capped at 2000
    configure(.{ .slowloris_deadline_ms = 400 });
    try testing.expectEqual(@as(u32, 100), recvPollMs()); // 400/4
    configure(.{ .slowloris_deadline_ms = 40 });
    try testing.expectEqual(@as(u32, 50), recvPollMs()); // floored at 50
}
