//! The device registry: parse `/system/configuration/devices.csv` into match rules and bind a
//! reported device to a driver. This is the data-driven replacement for the
//! device manager's three hand-written `switch` tables (`pciDriverForIdentity`,
//! `hidDriverFor`, `usbDriverForIdentity`); the registry is now **authoritative**
//! — a device that no row matches goes unbound (logged), never guessed.
//!
//! Pure logic: no hardware access, no syscalls, no allocator. `parse` fills a
//! caller-provided `[]Rule` whose string fields (`hid`, `driver`) are slices
//! *into the CSV source*, so the source buffer must outlive the rules (the
//! manager holds it in a static buffer for the life of the process — zero-copy).
//! That keeps this module freestanding and unit-testable with plain `zig test`.
//!
//! The file format (docs/device-driver-development/device-manager.md, and the
//! `/system/configuration/devices.csv` header itself): one rule per line, nine comma-separated
//! fields, `#` starts a comment (whole-line or trailing), blank lines ignored.
//!
//!     bus, base, class, prog_if, vendor, device, subsystem, hid, driver
//!
//! `bus` is `pci`/`usb`/`acpi`; the numeric fields are hex (with or without a
//! `0x` prefix); `*` or an empty field is a wildcard (matches anything). For PCI
//! the class triple is base/subclass/prog-IF; for USB it is class/subclass/
//! protocol with vendor/device the idVendor/idProduct; ACPI matches on `hid`
//! (e.g. "PNP0303") with the triple left blank. `driver` is a full ramdisk path.

const std = @import("std");
const csv = @import("csv");

/// Which bus a rule or a reported device belongs to. `unknown` is what an
/// unrecognised `bus` token parses to — such a rule never matches (its bus
/// equals no real device's), so a typo fails safe rather than binding wrongly.
pub const Bus = enum {
    pci,
    usb,
    acpi,
    unknown,

    pub fn fromToken(token: []const u8) Bus {
        if (std.mem.eql(u8, token, "pci")) return .pci;
        if (std.mem.eql(u8, token, "usb")) return .usb;
        if (std.mem.eql(u8, token, "acpi")) return .acpi;
        return .unknown;
    }
};

/// A reported device's full identity, as the manager assembles it from a
/// `child_added`: the bus-native class triple plus the numeric ids the widened
/// ABI now carries, or the ACPI `_HID` string. Fields a given bus does not have
/// are zero / empty (a PCI function has no `hid`; an ACPI device has no vendor).
pub const Identity = struct {
    bus: Bus,
    base: u8 = 0,
    subclass: u8 = 0,
    prog_if: u8 = 0,
    vendor: u16 = 0,
    device: u16 = 0,
    subsystem: u32 = 0,
    hid: []const u8 = "",
};

/// One parsed registry row. A `null` field is a wildcard — it matches any value
/// and contributes nothing to specificity. String fields point into the CSV
/// source that was parsed (see the module doc).
pub const Rule = struct {
    bus: Bus,
    base: ?u8 = null,
    subclass: ?u8 = null,
    prog_if: ?u8 = null,
    vendor: ?u16 = null,
    device: ?u16 = null,
    subsystem: ?u32 = null,
    hid: ?[]const u8 = null,
    driver: []const u8,
};

/// Specificity weights: how much each pinned field counts toward "most specific
/// wins". Doubling from the coarsest (`base`) so that each level outweighs *all*
/// coarser levels combined (1+2+4+8+16 = 31 < 32) — a rule that pins `device`
/// always beats any rule that does not, no matter how many coarse fields the
/// latter pins. `hid` and `device` share the top tier (the user's "hid and
/// device weigh heaviest"); they never co-occur, since `hid` is ACPI-only and
/// `device` is a PCI/USB numeric id.
const weight_base: u32 = 1;
const weight_subclass: u32 = 2;
const weight_prog_if: u32 = 4;
const weight_vendor: u32 = 8;
const weight_subsystem: u32 = 16;
const weight_device: u32 = 32;
const weight_hid: u32 = 32;

/// The outcome of `matchDriver`: the winning rule's driver path, its specificity,
/// and whether another rule tied it at that specificity. `ambiguous` is a
/// registry authoring error (two equally-specific rules claiming one device); the
/// manager logs it loudly and binds the first, so a shadowed rule is visible
/// rather than silently dropped.
pub const Match = struct {
    driver: []const u8,
    specificity: u32,
    ambiguous: bool,
};

/// Whether `rule` matches `id`: same bus, and every pinned (non-wildcard) field
/// equal. `hid` compares as a string; the rest as integers.
fn matches(rule: Rule, id: Identity) bool {
    if (rule.bus != id.bus) return false;
    if (rule.base) |b| if (b != id.base) return false;
    if (rule.subclass) |s| if (s != id.subclass) return false;
    if (rule.prog_if) |p| if (p != id.prog_if) return false;
    if (rule.vendor) |v| if (v != id.vendor) return false;
    if (rule.device) |d| if (d != id.device) return false;
    if (rule.subsystem) |s| if (s != id.subsystem) return false;
    if (rule.hid) |h| if (!std.mem.eql(u8, h, id.hid)) return false;
    return true;
}

/// The specificity score of a rule — the sum of the weights of its pinned fields.
fn specificity(rule: Rule) u32 {
    var score: u32 = 0;
    if (rule.base != null) score += weight_base;
    if (rule.subclass != null) score += weight_subclass;
    if (rule.prog_if != null) score += weight_prog_if;
    if (rule.vendor != null) score += weight_vendor;
    if (rule.device != null) score += weight_device;
    if (rule.subsystem != null) score += weight_subsystem;
    if (rule.hid != null) score += weight_hid;
    return score;
}

/// Bind a reported device to a driver: of every rule that matches `id`, return
/// the most specific. `null` when nothing matches (the device goes unbound —
/// the authoritative registry does not guess). On an exact specificity tie the
/// first such rule in file order wins and `ambiguous` is set.
pub fn matchDriver(rules: []const Rule, id: Identity) ?Match {
    var best: ?Match = null;
    for (rules) |rule| {
        if (!matches(rule, id)) continue;
        const score = specificity(rule);
        if (best) |current| {
            if (score > current.specificity) {
                best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false };
            } else if (score == current.specificity) {
                // Two equally-specific rules claim this device — keep the first,
                // flag the ambiguity for the manager to log.
                best.?.ambiguous = true;
            }
        } else {
            best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false };
        }
    }
    return best;
}

// --- parsing -----------------------------------------------------------------

/// What one CSV line parsed to. `malformed` is a non-comment, non-blank line the
/// parser could not read (wrong field count, unparsable number, empty driver) —
/// the manager counts these and logs, so a broken registry is loud, not silent.
const Line = union(enum) {
    rule: Rule,
    ignorable, // blank or comment
    malformed,
};

/// The result of `parse`: how many rules landed in the caller's buffer, and how
/// many non-ignorable lines were malformed (for the manager to log). `truncated`
/// is set if there were more valid rules than the buffer could hold.
pub const ParseResult = struct {
    count: usize,
    malformed: usize,
    truncated: bool,
};

/// Parse one hex field into `T`, honouring `*`/empty as a wildcard (`null`) and
/// an optional `0x` prefix. Returns an error only for a genuinely unparsable
/// non-wildcard token, so the caller can mark the whole line malformed.
fn parseHexField(comptime T: type, field: []const u8) !?T {
    const token = std.mem.trim(u8, field, " \t");
    if (token.len == 0 or std.mem.eql(u8, token, "*")) return null;
    const digits = if (std.mem.startsWith(u8, token, "0x") or std.mem.startsWith(u8, token, "0X"))
        token[2..]
    else
        token;
    return try std.fmt.parseInt(T, digits, 16);
}

/// Parse a wildcard-or-string field (the `hid` column): `*`/empty → wildcard.
fn parseStringField(field: []const u8) ?[]const u8 {
    const token = std.mem.trim(u8, field, " \t");
    if (token.len == 0 or std.mem.eql(u8, token, "*")) return null;
    return token;
}

/// Classify and (if a rule) parse one line. Split out from `parse` so it can be
/// unit-tested directly. `line` is the raw line including no newline.
fn parseLine(line: []const u8) Line {
    const body = csv.stripComment(line);
    if (body.len == 0) return .ignorable;

    // Nine comma-separated fields (csv.fields trims each): bus, base, class,
    // prog_if, vendor, device, subsystem, hid, driver.
    var cols: [9][]const u8 = undefined;
    var count: usize = 0;
    var it = csv.fields(body);
    while (it.next()) |field| {
        if (count >= cols.len) return .malformed; // too many columns
        cols[count] = field;
        count += 1;
    }
    if (count != cols.len) return .malformed; // too few columns

    const bus = Bus.fromToken(cols[0]);
    if (bus == .unknown) return .malformed;

    const driver = cols[8];
    if (driver.len == 0) return .malformed;

    return .{ .rule = .{
        .bus = bus,
        .base = parseHexField(u8, cols[1]) catch return .malformed,
        .subclass = parseHexField(u8, cols[2]) catch return .malformed,
        .prog_if = parseHexField(u8, cols[3]) catch return .malformed,
        .vendor = parseHexField(u16, cols[4]) catch return .malformed,
        .device = parseHexField(u16, cols[5]) catch return .malformed,
        .subsystem = parseHexField(u32, cols[6]) catch return .malformed,
        .hid = parseStringField(cols[7]),
        .driver = driver,
    } };
}

/// Parse a whole `/system/configuration/devices.csv` into `out_rules`. The string fields of the
/// returned rules point into `source`, which must outlive them.
pub fn parse(source: []const u8, out_rules: []Rule) ParseResult {
    var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false };
    var lines = std.mem.splitScalar(u8, source, '\n');
    while (lines.next()) |line| {
        switch (parseLine(line)) {
            .ignorable => {},
            .malformed => result.malformed += 1,
            .rule => |rule| {
                if (result.count >= out_rules.len) {
                    result.truncated = true;
                    continue;
                }
                out_rules[result.count] = rule;
                result.count += 1;
            },
        }
    }
    return result;
}

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

const testing = std.testing;

// The worked example from the design: a specific virtio-gpu rule (pins vendor +
// device) and a generic display rule (class only) both match the virtio card;
// the specific one must win. And a plain VGA adapter still falls to the generic
// rule. This is the whole point of widening the ABI to carry vendor/device.
test "virtio device rule beats the generic display rule" {
    const text =
        \\# bus, base, class, prog_if, vendor, device, subsystem, hid, driver
        \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display
        \\pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu
    ;
    var rules: [8]Rule = undefined;
    const parsed = parse(text, &rules);
    try testing.expectEqual(@as(usize, 2), parsed.count);
    try testing.expectEqual(@as(usize, 0), parsed.malformed);

    // The virtio-gpu function: display / other, vendor 1AF4 device 1050.
    const virtio = matchDriver(rules[0..parsed.count], .{
        .bus = .pci, .base = 0x03, .subclass = 0x80, .prog_if = 0x00,
        .vendor = 0x1AF4, .device = 0x1050,
    }).?;
    try testing.expect(!virtio.ambiguous);
    try testing.expectEqualStrings("/system/drivers/virtio-gpu", virtio.driver);

    // A plain VGA adapter (display / VGA) still binds the generic display driver.
    const vga = matchDriver(rules[0..parsed.count], .{
        .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00,
        .vendor = 0x1234, .device = 0x1111,
    }).?;
    try testing.expectEqualStrings("/system/drivers/display", vga.driver);
}

test "no matching row leaves the device unbound" {
    const text = "pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus\n";
    var rules: [8]Rule = undefined;
    const parsed = parse(text, &rules);
    try testing.expectEqual(@as(usize, 1), parsed.count);

    // An AHCI controller (mass storage / SATA / AHCI) has no row — unbound.
    const unmatched = matchDriver(rules[0..parsed.count], .{
        .bus = .pci, .base = 0x01, .subclass = 0x06, .prog_if = 0x01,
    });
    try testing.expect(unmatched == null);
}

test "acpi rows match on hid" {
    const text =
        \\acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus
        \\acpi, *, *, *, *, *, *, PNP0F13, /system/drivers/ps2-bus
    ;
    var rules: [8]Rule = undefined;
    const parsed = parse(text, &rules);
    try testing.expectEqual(@as(usize, 2), parsed.count);

    const keyboard = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0303" }).?;
    try testing.expectEqualStrings("/system/drivers/ps2-bus", keyboard.driver);
    const nothing = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0A03" });
    try testing.expect(nothing == null);
}

test "equally specific rules flag ambiguity" {
    const text =
        \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-a
        \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-b
    ;
    var rules: [8]Rule = undefined;
    const parsed = parse(text, &rules);
    const hit = matchDriver(rules[0..parsed.count], .{
        .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00,
    }).?;
    try testing.expect(hit.ambiguous);
    try testing.expectEqualStrings("/system/drivers/display-a", hit.driver); // first wins
}

test "comments, blanks, and malformed lines" {
    const text =
        \\# a header comment
        \\
        \\pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus  # trailing comment
        \\pci, ZZ, 03, 30, *, *, *, *, /system/drivers/broken
        \\pci, 03, 00, 00, *, *, *, *,
        \\bogus-bus, *, *, *, *, *, *, *, /system/drivers/x
    ;
    var rules: [8]Rule = undefined;
    const parsed = parse(text, &rules);
    try testing.expectEqual(@as(usize, 1), parsed.count); // only the xhci row is valid
    try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad hex, empty driver, bad bus
    try testing.expectEqualStrings("/system/drivers/usb-xhci-bus", rules[0].driver);
    try testing.expect(rules[0].hid == null); // trailing comment stripped, hid still wildcard
}
