//! Curated ioctl-interface registry (Part 13 §5.2.3, sub-phase 3B).
//!
//! The data lives at `hrb-code/src/data/driver_interfaces/*.json`,
//! one file per curated driver class. The Python-side maintainer
//! tool (`scripts/regenerate_ioctl_registry.py`) writes those
//! files; the conformance gate
//! (`tests/conformance/test_ioctl_registry_currency.py`) verifies
//! each entry's ioctl number matches the live kernel UAPI header
//! on every CI run. The broker side (this module) parses the JSON
//! at startup, exposes a `lookup(name) → ?*const DriverEntry`
//! call, and the server's `driver_info` formatter splices the
//! registry fields into the response.
//!
//! ## Why JSON instead of YAML
//!
//! The spec says YAML. We ship JSON because Zig's stdlib has a
//! built-in JSON parser and zero YAML support — adding a YAML
//! dependency to ship a 50-line registry would be cargo-culted
//! convention. The data shape is identical; the maintainer
//! workflow (regenerator script) doesn't care about format. The
//! deviation is documented in the data dir's README.
//!
//! ## v1 limitations
//!
//! - Fixed-number ioctls only. Parameterized `(len)` macros
//!   (e.g. HIDIOCGRAWNAME) are excluded — they need a schema
//!   extension (variable_size flag + base value).
//! - Five curated drivers in the spec; v1 ships USBDEVFS + HIDRAW.
//!   Adding VFIO / DRM-render / V4L2 is mechanical (append to
//!   `scripts/regenerate_ioctl_registry.py`'s CURATED list +
//!   re-run; the conformance gate picks them up automatically).
//! - Loaded at compile time via `@embedFile` — refresh on SIGHUP
//!   (spec §5.2.3) is a follow-up. The current sequence is:
//!   regenerate JSON → `zig build` → restart broker.

const std = @import("std");

/// Raw bytes of each curated JSON file. Compile-time embedded so
/// the broker stays single-binary; the JSON files travel with the
/// build artifact. To add a driver: ship its JSON, then append a
/// const here.
const usbdevfs_bytes = @embedFile("data/driver_interfaces/usbdevfs.json");
const hidraw_bytes = @embedFile("data/driver_interfaces/hidraw.json");
const vfio_bytes = @embedFile("data/driver_interfaces/vfio.json");
const drm_bytes = @embedFile("data/driver_interfaces/drm.json");
const v4l2_bytes = @embedFile("data/driver_interfaces/v4l2.json");

/// One ioctl entry as projected from the JSON. All slices point
/// into the loaded JSON arena; valid for the broker's lifetime.
pub const Ioctl = struct {
    name: []const u8,
    /// Canonical "0xXXXXXXXX" — for fixed-number ioctls this is
    /// the full computed value; for variable-size ones this is
    /// the base value at size=0 (the consumer overlays the runtime
    /// size into the size field via `_IOC_SIZESHIFT` when calling
    /// the live macro).
    number_hex: []const u8,
    arg: []const u8,
    direction: []const u8, // "r" | "w" | "rw" | "none"
    /// True when the macro takes a runtime `len` parameter
    /// (HIDIOC* family). The `number_hex` then represents the
    /// base-at-size-0; the wire response also flags this so the
    /// consumer renders the right shape.
    variable_size: bool = false,
};

pub const CharDevice = struct {
    pattern: []const u8,
    permissions: []const u8,
};

/// One driver entry; matches the registry JSON's top-level shape.
pub const DriverEntry = struct {
    driver: []const u8,
    module: []const u8,
    header: []const u8,
    maintainer: []const u8,
    last_verified: []const u8,
    ioctls: []Ioctl,
    char_devices: []CharDevice,
    uevent_actions: [][]const u8,
};

const max_entries: usize = 8;

/// Process-wide static registry. Loaded once via `init`, then
/// looked up by name. Backing arena outlives every lookup so the
/// returned slices are valid through process lifetime.
const Registry = struct {
    entries: [max_entries]?*DriverEntry = [_]?*DriverEntry{null} ** max_entries,
    count: usize = 0,
    arena: ?std.heap.ArenaAllocator = null,
};

var registry: Registry = .{};
var init_once: std.atomic.Value(bool) = .init(false);

/// Parse the embedded JSON files into the registry. Idempotent +
/// concurrency-safe via the atomic guard — the first caller does
/// the work; later callers no-op. Failure to parse any file logs
/// to stderr but doesn't abort the broker — the registry just
/// stays empty for that driver (honest emptiness; `lookup`
/// returns null, `driver_info` reports the not-in-registry
/// shape).
pub fn init(alloc: std.mem.Allocator) void {
    if (init_once.swap(true, .seq_cst)) return;

    var arena = std.heap.ArenaAllocator.init(alloc);
    registry.arena = arena;
    const aalloc = arena.allocator();

    const sources = [_][]const u8{
        usbdevfs_bytes,
        hidraw_bytes,
        vfio_bytes,
        drm_bytes,
        v4l2_bytes,
    };
    for (sources) |json_bytes| {
        loadOne(aalloc, json_bytes) catch |e| {
            std.debug.print(
                "driver_registry: failed to parse one entry: {s}\n",
                .{@errorName(e)},
            );
            continue;
        };
    }
}

fn loadOne(alloc: std.mem.Allocator, json_bytes: []const u8) !void {
    var parsed = try std.json.parseFromSlice(
        std.json.Value,
        alloc,
        json_bytes,
        .{},
    );
    // The arena owns the json.Value but std.json.parseFromSlice's
    // returned wrapper has its own arena that drops on .deinit.
    // We don't call deinit — we COPY every byte we keep into our
    // arena via `aalloc.dupe(u8, ...)`. After this function returns
    // the parsed wrapper's arena gets dropped at the next
    // registry.arena.deinit (only on full shutdown today; benign).
    defer parsed.deinit();

    const root = switch (parsed.value) {
        .object => |o| o,
        else => return error.UnexpectedSchema,
    };

    const entry = try alloc.create(DriverEntry);
    entry.* = .{
        .driver = try dupString(alloc, root, "driver"),
        .module = try dupString(alloc, root, "module"),
        .header = try dupString(alloc, root, "header"),
        .maintainer = try dupString(alloc, root, "maintainer"),
        .last_verified = try dupString(alloc, root, "last_verified"),
        .ioctls = try dupIoctls(alloc, root),
        .char_devices = try dupCharDevices(alloc, root),
        .uevent_actions = try dupStringArray(alloc, root, "uevent_actions"),
    };

    if (registry.count >= max_entries) return error.RegistryFull;
    registry.entries[registry.count] = entry;
    registry.count += 1;
}

fn dupString(
    alloc: std.mem.Allocator,
    obj: std.json.ObjectMap,
    key: []const u8,
) ![]const u8 {
    const v = obj.get(key) orelse return error.MissingField;
    const s = switch (v) {
        .string => |sx| sx,
        else => return error.WrongShape,
    };
    return alloc.dupe(u8, s);
}

fn dupStringArray(
    alloc: std.mem.Allocator,
    obj: std.json.ObjectMap,
    key: []const u8,
) ![][]const u8 {
    const v = obj.get(key) orelse return error.MissingField;
    const arr = switch (v) {
        .array => |a| a,
        else => return error.WrongShape,
    };
    const out = try alloc.alloc([]const u8, arr.items.len);
    for (arr.items, 0..) |item, i| {
        const s = switch (item) {
            .string => |sx| sx,
            else => return error.WrongShape,
        };
        out[i] = try alloc.dupe(u8, s);
    }
    return out;
}

fn dupIoctls(
    alloc: std.mem.Allocator,
    obj: std.json.ObjectMap,
) ![]Ioctl {
    const v = obj.get("ioctls") orelse return error.MissingField;
    const arr = switch (v) {
        .array => |a| a,
        else => return error.WrongShape,
    };
    const out = try alloc.alloc(Ioctl, arr.items.len);
    for (arr.items, 0..) |item, i| {
        const inner = switch (item) {
            .object => |o| o,
            else => return error.WrongShape,
        };
        // Variable-size entries store the value under
        // `base_number` (the size=0 value) and carry
        // `variable_size: true`. Fixed entries use `number`.
        const is_var = blk: {
            const vs = inner.get("variable_size") orelse break :blk false;
            break :blk switch (vs) {
                .bool => |b| b,
                else => false,
            };
        };
        const number_key = if (is_var) "base_number" else "number";
        out[i] = .{
            .name = try dupString(alloc, inner, "name"),
            .number_hex = try dupString(alloc, inner, number_key),
            .arg = try dupString(alloc, inner, "arg"),
            .direction = try dupString(alloc, inner, "direction"),
            .variable_size = is_var,
        };
    }
    return out;
}

fn dupCharDevices(
    alloc: std.mem.Allocator,
    obj: std.json.ObjectMap,
) ![]CharDevice {
    const v = obj.get("char_devices") orelse return error.MissingField;
    const arr = switch (v) {
        .array => |a| a,
        else => return error.WrongShape,
    };
    const out = try alloc.alloc(CharDevice, arr.items.len);
    for (arr.items, 0..) |item, i| {
        const inner = switch (item) {
            .object => |o| o,
            else => return error.WrongShape,
        };
        out[i] = .{
            .pattern = try dupString(alloc, inner, "pattern"),
            .permissions = try dupString(alloc, inner, "permissions"),
        };
    }
    return out;
}

/// Look up a driver entry by name. Returns null if the name isn't
/// in the registry — honest emptiness (the spec's "not curated").
/// The returned pointer is valid for the broker's lifetime.
pub fn lookup(name: []const u8) ?*const DriverEntry {
    for (registry.entries[0..registry.count]) |slot| {
        const e = slot orelse continue;
        if (std.mem.eql(u8, e.driver, name)) return e;
    }
    return null;
}

/// Test/debug helper: how many entries did init() load?
pub fn count() usize {
    return registry.count;
}
