//! mgf-hrb-usb (model layer) — USB-specific helpers for the worker.
//!
//! Phase **U1** of the USB worker (see `hrb-design/mgf-hrb-usb-worker.md` §9):
//! the model + the seeding helpers + the in-memory Composite shape, *before*
//! any Linux substrate code lands. The kabi conformance suite already exercises
//! USB URNs via the generic `FakeBackend`; this module adds the USB-specific
//! modelling — interfaces-first, dot-separated port-chain URNs, composite-
//! device topology — and the pin tests that go with it (see `usb_test.zig`).
//!
//! Owns no I/O. Linux-noun-free (D-003 / dependency law). The real Linux read
//! side lands in Phase U2 (`LinuxBackend.usb` against `/sys/bus/usb/devices`
//! + netlink); the privileged helper lands in Phase U3. This file is U1.

const std = @import("std");
const model = @import("mgf_hrb_model");
const identity = @import("mgf_hrb_identity");
const fake_backend = @import("mgf_hrb_fake_backend");

const Urn = model.Urn;
const Snapshot = identity.Snapshot;
const IdentityClass = identity.IdentityClass;
const FakeBackend = fake_backend.FakeBackend;

/// USB interface descriptor (the leaf of the topology — what kernel drivers
/// bind to, and what `claim` arbitrates). §2.4: composite devices ship multiple
/// interfaces; the *interface* is the unit of arbitration, not the device.
///
/// Alternate settings (`bAlternateSetting`) are NOT in the URN — they're a
/// property of typed ops (§5.9). `alt = 0` is the default.
pub const Interface = struct {
    if_num: u8, // bInterfaceNumber — the URN's if<K> suffix
    class: u8, // bInterfaceClass
    subclass: u8 = 0,
    protocol: u8 = 0,
    alt: u8 = 0,
};

/// USB composite device — parent identity + the interface set the device
/// currently exposes. The "currently exposes" matters: the iFig-DFU mode
/// transition replaces this set (§4 / FEEDBACK MODEL-01).
pub const Composite = struct {
    bus: u8, // host-controller index — the URN's bus<N>
    /// Port-chain from root port to leaf. Dot-separated in the URN
    /// (`port-2.3.1`); a u8-per-rung byte sequence here.
    port_chain: []const u8,
    vid: u16,
    pid: u16,
    /// `null` means "device doesn't expose iSerialNumber" — the comparator's
    /// asymmetric branch. Empty string is treated the same as null (G2 over
    /// "lying with an empty serial").
    serial_number: ?[]const u8 = null,
    interfaces: []const Interface,
};

/// Maximum bytes the URN buffer (128B) can hold for a USB resource. A
/// pathologically deep hub stack would still fit comfortably:
/// `hw:/usb/bus<u8>/port-<8 rungs × 3 chars>/dev/if<u8>` ≈ 60 bytes.
/// This constant is the conservative budget callers can rely on.
pub const MAX_USB_URN_BYTES: usize = 96;

/// Maximum bytes the port-chain segment (just the digits + dots, no
/// `port-` prefix) can occupy. 8 rungs × 3 digits + 7 dots = 31 bytes; round
/// up to 32 for safety.
const MAX_PORT_CHAIN_BYTES: usize = 32;

/// Render a port chain into a buffer as dot-separated decimal: `[2,3,1]` →
/// `"2.3.1"`. Returns the slice written. Private helper.
fn renderPortChain(buf: *[MAX_PORT_CHAIN_BYTES]u8, port_chain: []const u8) []const u8 {
    var pos: usize = 0;
    for (port_chain, 0..) |rung, i| {
        if (i != 0) {
            buf[pos] = '.';
            pos += 1;
        }
        const written = std.fmt.bufPrint(buf[pos..], "{d}", .{rung}) catch unreachable;
        pos += written.len;
    }
    return buf[0..pos];
}

/// Build the canonical URN for one USB interface (§2.2 grammar).
///
/// Format: `hw:/usb/bus<N>/port-<chain>/dev/if<K>`.
/// `port_chain = []u8{2, 3, 1}` renders as `port-2.3.1`.
///
/// The `dev/` segment is present (per §2.2) so the parent device is itself a
/// navigable URN — consumers walk parent/child via topology, never by parsing.
pub fn urnForInterface(
    buf: *[MAX_USB_URN_BYTES]u8,
    bus: u8,
    port_chain: []const u8,
    if_num: u8,
) Urn {
    var chain_buf: [MAX_PORT_CHAIN_BYTES]u8 = undefined;
    const chain = renderPortChain(&chain_buf, port_chain);
    const written = std.fmt.bufPrint(
        buf,
        "hw:/usb/bus{d}/port-{s}/dev/if{d}",
        .{ bus, chain, if_num },
    ) catch unreachable;
    return Urn.from(written);
}

/// Build the canonical URN for the *parent device* of a composite (§2.1 /
/// §2.2). Useful for parent-pointer queries and for the device row in
/// `query_topology`; not itself claimable (§2.4 — interfaces-first).
///
/// Format: `hw:/usb/bus<N>/port-<chain>/dev`.
pub fn urnForDevice(
    buf: *[MAX_USB_URN_BYTES]u8,
    bus: u8,
    port_chain: []const u8,
) Urn {
    var chain_buf: [MAX_PORT_CHAIN_BYTES]u8 = undefined;
    const chain = renderPortChain(&chain_buf, port_chain);
    const written = std.fmt.bufPrint(
        buf,
        "hw:/usb/bus{d}/port-{s}/dev",
        .{ bus, chain },
    ) catch unreachable;
    return Urn.from(written);
}

/// Build a `Snapshot` for one interface of a composite, picking the
/// identity-class per §2.3:
///
/// - **intrinsic** when the device exposes `iSerialNumber` (the strongest
///   within-vendor identity);
/// - **topological** otherwise (the `(bus, port_chain, if_num)` baseline,
///   defeated only by a cable move or hub power blip).
///
/// `synthetic_pinned` is operator-applied (admin alias) and not constructed
/// here — that's a policy concern (Part 8.4).
pub fn snapshotForInterface(
    composite: Composite,
    if_num: u8,
    captured_at_ms: u64,
) Snapshot {
    var urn_buf: [MAX_USB_URN_BYTES]u8 = undefined;
    const urn = urnForInterface(&urn_buf, composite.bus, composite.port_chain, if_num);

    const has_serial = if (composite.serial_number) |sn| sn.len > 0 else false;
    return Snapshot.init(.{
        .urn = urn,
        .identity_class = if (has_serial) .intrinsic else .topological,
        .vid = composite.vid,
        .pid = composite.pid,
        .serial_number = composite.serial_number,
        .bus = composite.bus,
        .port_chain = composite.port_chain,
        .captured_at_ms = captured_at_ms,
    });
}

/// Seed a `FakeBackend` with one composite device's interfaces. Emits one
/// `usb_interface` `Resource` *per interface* (§2.4 / D-013): the device is a
/// parent node in topology but never appears as a claimable Resource.
///
/// Returns the number of interfaces seeded (== `composite.interfaces.len`).
/// Allocation comes from the `FakeBackend`'s own allocator.
pub fn seedComposite(fake: *FakeBackend, composite: Composite) !usize {
    for (composite.interfaces) |iface| {
        var urn_buf: [MAX_USB_URN_BYTES]u8 = undefined;
        const urn = urnForInterface(&urn_buf, composite.bus, composite.port_chain, iface.if_num);
        try fake.add(urn.slice(), .claimable, .usb_interface);
    }
    return composite.interfaces.len;
}
