//! linux_bound — "bound at hardware" classification for restart reconcile
//! (Tier-1 #8, R-T1-8 residual; survivability §4.3).
//!
//! `Core.reconcileAfterRestart` quarantines resources that are "bound in hardware
//! but unclaimed by any live client" — the orphans a crash left behind. Until now
//! the broker approximated "bound in hardware" with the PRESENT set (every device
//! the backend enumerates), which over-quarantines on a healthy host: an idle PCI
//! device the broker never touched, or one in normal host use, is not an orphan.
//!
//! This module gives reconcile the real signal for PCI. A device the broker
//! claimed for DMA passthrough is bound to **`vfio-pci`**, and that binding
//! PERSISTS across a broker crash (only the using process died) — so a PCI
//! function bound to `vfio-pci` is a genuine orphan candidate. A function bound to
//! a HOST driver (`nvme`, `i915`, `pcieport`, …) is in host use, and an UNBOUND
//! function is idle-free — neither is an orphan. Non-PCI URNs (USB) can't be
//! classified this way (a USB interface the broker detached is merely driverless,
//! ambiguous with naturally-driverless devices), so they are conservatively
//! treated as bound — reconcile keeps its prior present-set behaviour for them
//! rather than silently dropping them.
//!
//! ## Scope (and the hardware gate)
//!
//! The classification reads sysfs (`/sys/bus/pci/devices/<BDF>/driver` symlink →
//! driver name) and is buildable + testable here. Exercising reconcile against a
//! REAL `vfio-pci`-bound orphan end-to-end needs an actual passthrough device
//! bound to vfio (root + a dedicated device — the same gate as #7's VFIO bind);
//! the integration is tested via injection, and the classifier against this
//! host's real PCI driver bindings.

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

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

/// The kernel driver that marks a device as bound by the broker for passthrough.
/// A device bound to this is an orphan candidate after a restart.
const VFIO_PCI = "vfio-pci";

/// A seam reconcile consults per present URN: is this resource bound at the
/// hardware level in a way that signals a broker orphan (so reconcile should
/// consider quarantining it), or is it idle-free / in host use (leave it alone)?
pub const BoundSource = struct {
    ctx: *anyopaque,
    is_bound: *const fn (ctx: *anyopaque, urn: *const Urn) bool,
};

/// Extract the BDF from a PCI URN `hw:/pci/<BDF>/fn0`, or null if not PCI. Pure.
pub fn bdfFromPciUrn(urn: *const Urn) ?[]const u8 {
    const prefix = "hw:/pci/";
    const s = urn.slice();
    if (!std.mem.startsWith(u8, s, prefix)) return null;
    const after = s[prefix.len..];
    const slash = std.mem.indexOfScalar(u8, after, '/') orelse return null;
    const bdf = after[0..slash];
    return if (bdf.len == 0) null else bdf;
}

/// The kernel driver bound to a PCI device, or null if unbound. Reads the
/// `driver` symlink and returns its basename into `buf`.
pub fn driverFor(buf: []u8, bdf: []const u8) ?[]const u8 {
    var path_buf: [256]u8 = undefined;
    const sym = std.fmt.bufPrintZ(&path_buf, "/sys/bus/pci/devices/{s}/driver", .{bdf}) catch return null;
    const rc = linux.readlink(sym, buf.ptr, buf.len);
    if (linux.errno(rc) != .SUCCESS) return null; // no symlink → unbound
    const target = buf[0..rc]; // e.g. "../../../bus/pci/drivers/nvme"
    const slash = std.mem.lastIndexOfScalar(u8, target, '/') orelse return target;
    return target[slash + 1 ..];
}

/// The orphan-classification for a URN (the default `BoundSource`):
///   * non-PCI (USB, …) → true  — can't classify; keep reconcile's prior
///     present-set behaviour (conservative — never silently drop it).
///   * PCI bound to `vfio-pci` → true  — broker passthrough binding persists a
///     crash → orphan candidate.
///   * PCI bound to a host driver, or unbound → false — in host use / idle-free,
///     NOT a broker orphan; reconcile leaves it alone.
pub fn isBoundForReconcile(urn: *const Urn) bool {
    const bdf = bdfFromPciUrn(urn) orelse return true; // non-PCI → conservative include
    var buf: [256]u8 = undefined;
    const driver = driverFor(&buf, bdf) orelse return false; // unbound PCI → idle-free
    return std.mem.eql(u8, driver, VFIO_PCI);
}

fn isBoundCtx(ctx: *anyopaque, urn: *const Urn) bool {
    _ = ctx;
    return isBoundForReconcile(urn);
}

var g_ctx: u8 = 0;

/// The production bound-classification source for an OPERATOR-triggered reconcile
/// (the `reconcile` wire op). Conservative: a non-PCI URN it can't classify is
/// included, so an explicit operator sweep never silently drops a device.
pub fn boundSource() BoundSource {
    return .{ .ctx = &g_ctx, .is_bound = isBoundCtx };
}

/// STRICT bound-classification: a URN is an orphan candidate ONLY if it is a PCI
/// device bound to `vfio-pci` (a confident, persists-a-crash signal). Non-PCI
/// (USB — ambiguous), unbound, and host-driver-bound devices are all excluded.
/// Used by the UNATTENDED auto-reconcile on a detected self-restart, where the
/// broker must quarantine only what it is sure is an orphan — never disrupt idle
/// or USB devices on every restart.
pub fn isBoundStrict(urn: *const Urn) bool {
    const bdf = bdfFromPciUrn(urn) orelse return false; // non-PCI → not a confident orphan
    var buf: [256]u8 = undefined;
    const driver = driverFor(&buf, bdf) orelse return false; // unbound → not an orphan
    return std.mem.eql(u8, driver, VFIO_PCI);
}

fn isBoundStrictCtx(ctx: *anyopaque, urn: *const Urn) bool {
    _ = ctx;
    return isBoundStrict(urn);
}

/// The strict bound source for unattended auto-reconcile on a self-restart.
pub fn boundSourceStrict() BoundSource {
    return .{ .ctx = &g_ctx, .is_bound = isBoundStrictCtx };
}

// ===========================================================================
// R-T1-8 (b) — bound-classification pins.
// ===========================================================================

const testing = std.testing;

test "bound-class: non-PCI URNs are conservatively included (can't classify)" {
    // USB can't be classified by driver binding → keep present-set behaviour.
    try testing.expect(isBoundForReconcile(&Urn.from("hw:/usb/bus1/dev4/if0")));
    // A malformed/non-pci URN is likewise conservatively included.
    try testing.expect(isBoundForReconcile(&Urn.from("hw:/test/dev0")));
}

test "bound-class STRICT: non-PCI is excluded (unattended auto-reconcile is confident-only)" {
    // The strict policy (auto-reconcile) excludes everything but a confident
    // vfio-pci PCI orphan — so a restart never disrupts USB / idle / host devices.
    try testing.expect(!isBoundStrict(&Urn.from("hw:/usb/bus1/dev4/if0"))); // non-PCI excluded
    try testing.expect(!isBoundStrict(&Urn.from("hw:/test/dev0"))); // fake / non-PCI excluded
    try testing.expect(!isBoundStrict(&Urn.from("hw:/pci/dead:ff:1f.7/fn0"))); // unbound PCI excluded
    // (a real vfio-pci-bound device → true, but binding one needs the #7 gate.)
}

test "bound-class: a PCI device with no driver (unbound) is NOT an orphan" {
    // A PCI BDF that doesn't exist on this host has no driver symlink → unbound →
    // idle-free, not a broker orphan → excluded from reconcile.
    try testing.expect(!isBoundForReconcile(&Urn.from("hw:/pci/dead:ff:1f.7/fn0")));
}

test "bound-class: BDF extraction" {
    try testing.expect(std.mem.eql(u8, "0000:01:00.0", bdfFromPciUrn(&Urn.from("hw:/pci/0000:01:00.0/fn0")).?));
    try testing.expect(bdfFromPciUrn(&Urn.from("hw:/usb/bus1/dev4/if0")) == null);
}

test "bound-class (host integration): a host-driver-bound PCI device is NOT an orphan" {
    // Best-effort against this host. Pick the first real PCI device bound to a
    // (non-vfio) host driver and assert it is classified as not-an-orphan — so a
    // reconcile won't quarantine devices the host is actively using. On a host
    // with no driver-bound PCI devices this finds nothing and passes.
    const rc = linux.open("/sys/bus/pci/devices", .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, 0);
    if (linux.errno(rc) != .SUCCESS) return;
    const fd: i32 = @intCast(rc);
    defer _ = linux.close(fd);
    var buf: [4096]u8 align(@alignOf(linux.dirent64)) = undefined;
    while (true) {
        const got = linux.getdents64(fd, &buf, buf.len);
        if (linux.errno(got) != .SUCCESS or got == 0) break;
        var pos: usize = 0;
        while (pos < got) {
            const dent: *linux.dirent64 = @ptrCast(@alignCast(&buf[pos]));
            pos += dent.reclen;
            const name = std.mem.span(@as([*:0]const u8, @ptrCast(&dent.name)));
            if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue;
            var dbuf: [256]u8 = undefined;
            if (driverFor(&dbuf, name)) |drv| {
                if (!std.mem.eql(u8, drv, VFIO_PCI)) {
                    // A host-driver-bound device — must NOT be flagged as an orphan.
                    var urn_buf: [128]u8 = undefined;
                    const urn_str = std.fmt.bufPrint(&urn_buf, "hw:/pci/{s}/fn0", .{name}) catch continue;
                    try testing.expect(!isBoundForReconcile(&Urn.from(urn_str)));
                    return; // one real host-bound device is enough
                }
            }
        }
    }
}
