//! mgf-hrb interrupt-rate containment (survivability D7, Tier-2 defense-in-depth).
//!
//! A malicious or malfunctioning device that raises interrupts continuously is a
//! use-path DoS against the host. The broker hands the device's fd to the client
//! and steps out of the interrupt path (G4) — so it is *not* in the IRQ path, and
//! the IOMMU + kernel IRQ handling are the first line of defense. But the broker
//! *granted* the device, so it owns the policy of *whether to keep it granted*
//! under abuse. This module is that policy: a cold-path, control-plane monitor
//! that samples per-device interrupt counters and flags any device whose rate
//! exceeds a ceiling so the broker can revoke + quarantine it
//! (`Core.containByFlooding`). "Worth a hook, not worth slowing anything" — the
//! hot use path is untouched; this runs only when an operator/supervisor triggers
//! the `irq_scan` op (the broker has no background-tick infrastructure, and an
//! always-on poller is not warranted for a 🟡-MINOR defense-in-depth hook).
//!
//! The interrupt-counter SOURCE is a seam (`IrqCounterSource`) so the policy is
//! exercised deterministically in tests (inject counts) and the real per-device
//! kernel-counter reader (`/proc/interrupts` keyed by a PCI device's `msi_irqs`)
//! plugs in behind it without changing the policy. Per-device IRQ attribution is
//! substrate-specific (PCI MSI has it; USB shares the HCD IRQ), so the production
//! source is honestly partial — see the wiring in `server.zig`.

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

const Urn = model.Urn;

/// Max distinct devices the monitor tracks history for (S5 bound — the history
/// can't grow without limit). Live leases are already bounded by the #9 lease
/// ceilings, so the leased-URN set this tracks is bounded; this cap is the
/// belt-and-braces. On overflow the oldest entry is evicted (it simply
/// re-baselines next time it's sampled).
pub const MAX_TRACKED: usize = 256;

/// A pull-seam for a device's cumulative interrupt count. `get` returns the
/// current counter for `urn`, or null when the device's count isn't attributable
/// (e.g. a USB interface sharing the host-controller IRQ) — null means "skip,
/// can't judge," never "zero."
pub const IrqCounterSource = struct {
    ctx: *anyopaque,
    get: *const fn (ctx: *anyopaque, urn: *const Urn) ?u64,
    /// Optional per-scan snapshot hook. `Facade.scanInterruptRates` calls it
    /// ONCE at the start of a scan, so a source backed by an expensive shared
    /// read (the real `/proc/interrupts` reader) can take a single point-in-time
    /// snapshot and serve every device from it instead of re-reading per device.
    /// Null for sources with no shared cost (e.g. an injected test source).
    refresh: ?*const fn (ctx: *anyopaque) void = null,
};

/// Aggregate result of an interrupt-rate scan.
pub const IrqReport = struct { scanned: usize = 0, contained: usize = 0 };

/// Per-device interrupt-rate monitor. Holds one history entry per device (last
/// counter + timestamp) and decides, on each fresh sample, whether the device's
/// rate since the previous sample exceeds the ceiling.
pub const IrqMonitor = struct {
    const Entry = struct { urn: Urn, last_count: u64, last_ms: u64 };

    entries: std.ArrayListUnmanaged(Entry) = .empty,
    alloc: std.mem.Allocator,
    lock: kabi.SpinMutex = .{},
    /// The ceiling: a device sustaining more than this many interrupts per
    /// second between two samples is judged to be flooding.
    max_rate_per_sec: u64,

    pub fn init(alloc: std.mem.Allocator, max_rate_per_sec: u64) IrqMonitor {
        return .{ .alloc = alloc, .max_rate_per_sec = max_rate_per_sec };
    }

    pub fn deinit(self: *IrqMonitor) void {
        self.entries.deinit(self.alloc);
    }

    fn find(self: *IrqMonitor, urn: *const Urn) ?*Entry {
        for (self.entries.items) |*e| {
            if (e.urn.eql(urn)) return e;
        }
        return null;
    }

    /// Record a fresh interrupt-counter `count` for `urn` at `now_ms`. Returns
    /// true iff the device is FLOODING — its interrupt rate since the previous
    /// sample exceeds `max_rate_per_sec`.
    ///
    /// The FIRST sample for a device establishes a baseline and returns false
    /// (there's no interval to compute a rate over yet). A counter that went
    /// DOWN (device reset / counter wrap) re-baselines and returns false (never
    /// a false positive from a wrap). A zero-length interval returns false (no
    /// division by zero). Cumulative counters only — `count` is monotonic
    /// between resets, like `/proc/interrupts`.
    pub fn sample(self: *IrqMonitor, urn: *const Urn, count: u64, now_ms: u64) bool {
        self.lock.lock();
        defer self.lock.unlock();

        if (self.find(urn)) |e| {
            // Counter reset / wrap, or no time elapsed → re-baseline, no verdict.
            if (count < e.last_count or now_ms <= e.last_ms) {
                e.last_count = count;
                e.last_ms = now_ms;
                return false;
            }
            const delta = count - e.last_count;
            const dt_ms = now_ms - e.last_ms;
            const rate_per_sec = (delta *% 1000) / dt_ms;
            e.last_count = count;
            e.last_ms = now_ms;
            return rate_per_sec > self.max_rate_per_sec;
        }

        // First sight of this device — record a baseline.
        if (self.entries.items.len >= MAX_TRACKED) {
            _ = self.entries.orderedRemove(0); // evict oldest; it re-baselines later
        }
        self.entries.append(self.alloc, .{ .urn = urn.*, .last_count = count, .last_ms = now_ms }) catch {};
        return false;
    }

    /// Drop a device's history (e.g. after it's been contained or its lease
    /// released) so a later re-grant starts from a clean baseline. Idempotent.
    pub fn forget(self: *IrqMonitor, urn: *const Urn) void {
        self.lock.lock();
        defer self.lock.unlock();
        for (self.entries.items, 0..) |e, i| {
            if (e.urn.eql(urn)) {
                _ = self.entries.swapRemove(i);
                return;
            }
        }
    }
};

/// Default ceiling: a device sustaining more than a million interrupts per
/// second between two operator scans is judged abusive. Deliberately generous —
/// a legitimate high-rate device (a fast NIC under load) stays well under it, so
/// containment fires only on genuine runaway floods (defense-in-depth, not a
/// throttle).
pub const DEFAULT_MAX_IRQ_PER_SEC: u64 = 1_000_000;

// ── Broker-wide instance + counter source (module globals, like shm/admission) ──
var g_monitor: ?IrqMonitor = null;
var g_source: ?IrqCounterSource = null;
var g_null_ctx: u8 = 0;

/// Install the broker-wide IRQ monitor. Call once at startup.
pub fn initGlobal(alloc: std.mem.Allocator, max_rate_per_sec: u64) !void {
    g_monitor = IrqMonitor.init(alloc, max_rate_per_sec);
}

/// The broker-wide monitor (reached only from the cold `irq_scan` dispatch path).
pub fn globalMonitor() *IrqMonitor {
    return &(g_monitor.?);
}

/// Set the broker-wide counter source (startup wires the linux reader; tests
/// inject their own via the facade scan's explicit-source overload).
pub fn setCounterSource(src: IrqCounterSource) void {
    g_source = src;
}

/// The broker-wide counter source, or a null source (skips every device) if one
/// was never set — so the op is always safe to call even before a real reader is
/// wired. The production reader lives in `linux_irq.counterSource()` and is set
/// at startup (`server.zig`); this null fallback keeps the op safe in degraded /
/// test paths that never called `setCounterSource`.
pub fn globalSource() IrqCounterSource {
    return g_source orelse nullCounterSource();
}

fn nullGet(ctx: *anyopaque, urn: *const Urn) ?u64 {
    _ = ctx;
    _ = urn;
    return null;
}

/// A safe no-op source (every device unattributable → the scan contains
/// nothing). The fallback when no real source is set.
pub fn nullCounterSource() IrqCounterSource {
    return .{ .ctx = &g_null_ctx, .get = nullGet };
}

// ===========================================================================
// D7 — interrupt-rate policy pins.
// ===========================================================================

const testing = std.testing;

test "D7: first sample baselines; a sustained over-ceiling rate flags flooding" {
    var m = IrqMonitor.init(testing.allocator, 1000); // ceiling: 1000 IRQ/s
    defer m.deinit();
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");

    // First sample is a baseline — no rate yet.
    try testing.expect(!m.sample(&urn, 0, 1000));
    // +50_000 interrupts over 100 ms = 500_000/s ≫ 1000/s → flooding.
    try testing.expect(m.sample(&urn, 50_000, 1100));
}

test "D7: a modest rate under the ceiling is not flagged" {
    var m = IrqMonitor.init(testing.allocator, 1000);
    defer m.deinit();
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");
    try testing.expect(!m.sample(&urn, 0, 1000));
    // +50 interrupts over 100 ms = 500/s < 1000/s → fine.
    try testing.expect(!m.sample(&urn, 50, 1100));
}

test "D7: a counter reset/wrap re-baselines (no false positive)" {
    var m = IrqMonitor.init(testing.allocator, 1000);
    defer m.deinit();
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");
    try testing.expect(!m.sample(&urn, 1_000_000, 1000)); // baseline high
    // Device reset: counter drops. Must NOT be read as a giant negative→huge rate.
    try testing.expect(!m.sample(&urn, 5, 1100)); // re-baseline, no verdict
    // From the new baseline, a calm rate is still calm.
    try testing.expect(!m.sample(&urn, 30, 1200));
}

test "D7: a zero-length interval never divides by zero" {
    var m = IrqMonitor.init(testing.allocator, 1000);
    defer m.deinit();
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");
    try testing.expect(!m.sample(&urn, 0, 1000));
    try testing.expect(!m.sample(&urn, 999_999, 1000)); // same ms → no verdict, no crash
}

test "D7: distinct devices are tracked independently; forget re-baselines" {
    var m = IrqMonitor.init(testing.allocator, 1000);
    defer m.deinit();
    const a = Urn.from("hw:/pci/0000:01:00.0/fn0");
    const b = Urn.from("hw:/pci/0000:02:00.0/fn0");
    try testing.expect(!m.sample(&a, 0, 1000));
    try testing.expect(!m.sample(&b, 0, 1000));
    // a floods, b stays calm — independent histories.
    try testing.expect(m.sample(&a, 1_000_000, 1100));
    try testing.expect(!m.sample(&b, 10, 1100));
    // forget(a) drops its history → next sample is a fresh baseline (no verdict).
    m.forget(&a);
    try testing.expect(!m.sample(&a, 9_999_999, 1200));
}
