//! mgf-hrb-usb-scan — the U2 exit-gate reporter, polished for operators.
//!
//! Walks the live `/sys/bus/usb/devices/` tree directly (no broker needed)
//! and renders a tabular per-interface report. Columns:
//!
//!   Bus  Port-chain  If  VID:PID    Class                  URN
//!
//! Plus a small footer with bus + interface counts and a one-line legend.
//!
//! Output goes to stderr (via std.debug.print, matching the broker's
//! startup-message style). Rows are alphabetically grep-able by URN so
//! piping into `sort -k7` keeps everything aligned.
//!
//! Exit codes:
//!   0 — enumeration succeeded (zero interfaces is honest empty, not an error).
//!   1 — sysfs unreadable / IO error.

const std = @import("std");
const linux = std.os.linux;

const SYSFS_USB = "/sys/bus/usb/devices";
const SYSFS_USB_Z: [:0]const u8 = SYSFS_USB;

// ---- USB-IF class-code decode (subset) -------------------------------------
// https://www.usb.org/defined-class-codes — only the common-on-real-hosts
// entries; uncommon codes render as "Class 0xNN" so the report stays honest.
fn classLabel(class: u8) []const u8 {
    return switch (class) {
        0x01 => "Audio",
        0x02 => "Comm / CDC",
        0x03 => "HID",
        0x05 => "Physical",
        0x06 => "Image",
        0x07 => "Printer",
        0x08 => "Mass Storage",
        0x09 => "Hub",
        0x0a => "CDC-Data",
        0x0b => "Smart Card",
        0x0d => "Content Security",
        0x0e => "Video",
        0x0f => "Personal Health",
        0x10 => "Audio/Video",
        0x11 => "Billboard",
        0xdc => "Diagnostic",
        0xe0 => "Wireless",
        0xef => "Misc",
        0xfe => "App Specific",
        0xff => "Vendor Specific",
        else => "?",
    };
}

// ---- raw-syscall sysfs helpers (matches linux_backend.zig / linux_usb.zig) -

fn sysReadAttr(buf: []u8, path: [:0]const u8) ?[]const u8 {
    const rc = linux.open(path, .{ .ACCMODE = .RDONLY }, 0);
    if (linux.errno(rc) != .SUCCESS) return null;
    const fd: i32 = @intCast(rc);
    defer _ = linux.close(fd);
    const n = linux.read(fd, buf.ptr, buf.len);
    if (linux.errno(n) != .SUCCESS) return null;
    var len: usize = n;
    while (len > 0 and (buf[len - 1] == '\n' or buf[len - 1] == '\r')) len -= 1;
    return buf[0..len];
}

// ---- the row + the parser --------------------------------------------------

const Row = struct {
    bus: u8,
    port_chain: [16]u8 = [_]u8{0} ** 16,
    port_depth: u8 = 0,
    port_chain_str: [16]u8 = [_]u8{0} ** 16, // dot-formatted, for column rendering
    port_chain_str_len: u8 = 0,
    if_num: u8,
    class: u8 = 0,
    has_class: bool = false,
    vid: u16 = 0,
    pid: u16 = 0,
    has_vid_pid: bool = false,
    urn: [80]u8 = [_]u8{0} ** 80,
    urn_len: u8 = 0,

    fn urnSlice(self: *const Row) []const u8 {
        return self.urn[0..self.urn_len];
    }
    fn portChainSliceStr(self: *const Row) []const u8 {
        return self.port_chain_str[0..self.port_chain_str_len];
    }
};

/// Parse a sysfs USB entry name (e.g. `3-9.4:1.0`) into a Row's structural
/// fields. Returns null for non-interface entries (`usbN`, device-only
/// `<bus>-<port>` rows, malformed names).
fn parseEntry(name: []const u8, row: *Row) bool {
    if (std.mem.startsWith(u8, name, "usb")) return false;
    const colon = std.mem.indexOfScalar(u8, name, ':') orelse return false;
    const device_part = name[0..colon];
    const config_iface = name[colon + 1 ..];

    const dash = std.mem.indexOfScalar(u8, device_part, '-') orelse return false;
    const bus_str = device_part[0..dash];
    const port_str = device_part[dash + 1 ..];
    row.bus = std.fmt.parseInt(u8, bus_str, 10) catch return false;

    const dot = std.mem.indexOfScalar(u8, config_iface, '.') orelse return false;
    const iface_str = config_iface[dot + 1 ..];
    row.if_num = std.fmt.parseInt(u8, iface_str, 10) catch return false;

    // port chain → both the byte array (for the URN builder) and the dotted
    // string form (for the column render).
    var chain_str_pos: usize = 0;
    var it = std.mem.tokenizeScalar(u8, port_str, '.');
    while (it.next()) |rung_str| {
        if (row.port_depth >= row.port_chain.len) return false;
        const rung = std.fmt.parseInt(u8, rung_str, 10) catch return false;
        row.port_chain[row.port_depth] = rung;
        row.port_depth += 1;
        if (chain_str_pos != 0 and chain_str_pos < row.port_chain_str.len) {
            row.port_chain_str[chain_str_pos] = '.';
            chain_str_pos += 1;
        }
        const written = std.fmt.bufPrint(
            row.port_chain_str[chain_str_pos..],
            "{d}",
            .{rung},
        ) catch return false;
        chain_str_pos += written.len;
    }
    if (row.port_depth == 0) return false;
    row.port_chain_str_len = @intCast(chain_str_pos);

    // Build the URN.
    const urn_written = std.fmt.bufPrint(
        &row.urn,
        "hw:/usb/bus{d}/port-{s}/dev/if{d}",
        .{ row.bus, row.portChainSliceStr(), row.if_num },
    ) catch return false;
    row.urn_len = @intCast(urn_written.len);
    return true;
}

/// Fill in vid/pid (from the parent device dir) and class (from this
/// interface dir). All optional — if any read fails the row still renders
/// with the missing column showing a fixed-width placeholder.
///
/// Root-hub quirk: for interfaces named `<bus>-0:c.i`, the parent device
/// dir is `usb<bus>` (not `<bus>-0`), so we look up the parent path with
/// that special case applied.
fn fillRichAttrs(name: []const u8, device_part: []const u8, row: *Row) void {
    var path_buf: [256]u8 = undefined;
    var attr_buf: [16]u8 = undefined;

    // Resolve the parent device dir, with the root-hub special case.
    var parent_buf: [16]u8 = undefined;
    const parent_dir: []const u8 = if (std.mem.endsWith(u8, device_part, "-0")) blk: {
        // `<bus>-0` → `usb<bus>` (the root-hub alias)
        const written = std.fmt.bufPrint(&parent_buf, "usb{d}", .{row.bus}) catch break :blk device_part;
        break :blk written;
    } else device_part;

    // Parent device's idVendor / idProduct.
    if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idVendor", .{ SYSFS_USB, parent_dir })) |vpath| {
        if (sysReadAttr(&attr_buf, vpath)) |s| {
            if (std.fmt.parseInt(u16, s, 16)) |v| {
                row.vid = v;
                row.has_vid_pid = true;
            } else |_| {}
        }
    } else |_| {}
    if (row.has_vid_pid) {
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idProduct", .{ SYSFS_USB, parent_dir })) |ppath| {
            if (sysReadAttr(&attr_buf, ppath)) |s| {
                if (std.fmt.parseInt(u16, s, 16)) |p| {
                    row.pid = p;
                } else |_| {
                    row.has_vid_pid = false;
                }
            } else {
                row.has_vid_pid = false;
            }
        } else |_| {
            row.has_vid_pid = false;
        }
    }

    // This interface's bInterfaceClass.
    if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/bInterfaceClass", .{ SYSFS_USB, name })) |cpath| {
        if (sysReadAttr(&attr_buf, cpath)) |s| {
            if (std.fmt.parseInt(u8, s, 16)) |c| {
                row.class = c;
                row.has_class = true;
            } else |_| {}
        }
    } else |_| {}
}

// ---- the walk + render -----------------------------------------------------

fn renderHeader() void {
    std.debug.print(
        \\mgf-hrb-usb-scan — /sys/bus/usb/devices
        \\
        \\Each row is one USB *interface* (the unit of arbitration — composite
        \\devices show one row per interface, never one per device, per the
        \\design's §2.4 / D-013). Fidelity is `live` when the descriptor read
        \\succeeded; `?` when sysfs raced during the walk. VID:PID is read
        \\from the parent device; Class is the USB-IF class code decoded.
        \\
        \\
    , .{});
    std.debug.print(
        "  {s:>3}  {s:<11}  {s:>2}  {s:<9}  {s:<18}  {s}\n",
        .{ "Bus", "Port-chain", "If", "VID:PID", "Class", "URN" },
    );
    std.debug.print(
        "  {s:>3}  {s:<11}  {s:>2}  {s:<9}  {s:<18}  {s}\n",
        .{ "---", "-----------", "--", "---------", "------------------", "---------------------------------" },
    );
}

fn renderRow(row: *const Row) void {
    var vidpid_buf: [16]u8 = undefined;
    const vidpid = if (row.has_vid_pid)
        std.fmt.bufPrint(&vidpid_buf, "{x:0>4}:{x:0>4}", .{ row.vid, row.pid }) catch "----:----"
    else
        "----:----";

    var class_buf: [32]u8 = undefined;
    const class_str = if (row.has_class) blk: {
        const label = classLabel(row.class);
        if (label[0] == '?') {
            break :blk std.fmt.bufPrint(&class_buf, "Class 0x{x:0>2}", .{row.class}) catch "?";
        } else {
            break :blk label;
        }
    } else "----";

    std.debug.print(
        "  {d:>3}  {s:<11}  {d:>2}  {s:<9}  {s:<18}  {s}\n",
        .{
            row.bus,
            row.portChainSliceStr(),
            row.if_num,
            vidpid,
            class_str,
            row.urnSlice(),
        },
    );
}

fn renderFooter(interfaces: usize, buses: usize) void {
    std.debug.print(
        "\n{d} interface(s) across {d} bus(es).\n",
        .{ interfaces, buses },
    );
    std.debug.print(
        \\
        \\Don't recognise a column or term? Run `mgf-hrb explain usb`
        \\for the hardware diagram (Host Controller → Root Hub → ports →
        \\composite devices → interfaces) and a column-by-column mapping.
        \\
    , .{});
    if (interfaces == 0) {
        std.debug.print(
            \\
            \\No USB devices found. If you expected some, check:
            \\  • /sys/bus/usb/devices is readable (some containers hide it)
            \\  • USB host controllers are enabled in the kernel
            \\  • The mgf-hrb broker is NOT required for this tool — it talks
            \\    to sysfs directly. A broker socket missing is unrelated.
            \\
        , .{});
    }
}

// Comparator for sorting rows by (bus, port_chain, if_num) so composites
// stay grouped.
fn rowLessThan(_: void, a: Row, b: Row) bool {
    if (a.bus != b.bus) return a.bus < b.bus;
    const ap = a.port_chain[0..a.port_depth];
    const bp = b.port_chain[0..b.port_depth];
    const n = @min(ap.len, bp.len);
    for (0..n) |i| {
        if (ap[i] != bp[i]) return ap[i] < bp[i];
    }
    if (ap.len != bp.len) return ap.len < bp.len;
    return a.if_num < b.if_num;
}

pub fn main() !u8 {
    const alloc = std.heap.page_allocator;

    const dfd_rc = linux.open(SYSFS_USB_Z, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, 0);
    if (linux.errno(dfd_rc) != .SUCCESS) {
        std.debug.print(
            "error: cannot open {s} — does this host have USB sysfs mounted?\n",
            .{SYSFS_USB},
        );
        return 1;
    }
    const dfd: i32 = @intCast(dfd_rc);
    defer _ = linux.close(dfd);

    var rows: std.ArrayListUnmanaged(Row) = .empty;
    defer rows.deinit(alloc);

    var dbuf: [4096]u8 = undefined;
    while (true) {
        const nread = linux.getdents64(dfd, &dbuf, dbuf.len);
        if (linux.errno(nread) != .SUCCESS) break;
        if (nread == 0) break;
        var off: usize = 0;
        while (off + 19 <= nread) {
            const reclen = std.mem.readInt(u16, dbuf[off + 16 ..][0..2], .little);
            if (reclen == 0) break;
            const name_start = off + 19;
            const name_ptr: [*:0]const u8 = @ptrCast(&dbuf[name_start]);
            const name = std.mem.span(name_ptr);
            off += reclen;
            if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue;

            var row = Row{ .bus = 0, .if_num = 0 };
            if (!parseEntry(name, &row)) continue;

            // device_part is the entry name up to the colon (e.g. "3-9.4")
            const colon = std.mem.indexOfScalar(u8, name, ':').?;
            const device_part = name[0..colon];
            fillRichAttrs(name, device_part, &row);

            rows.append(alloc, row) catch return 1;
        }
    }

    std.mem.sort(Row, rows.items, {}, rowLessThan);

    renderHeader();

    var bus_seen = [_]bool{false} ** 256;
    var bus_count: usize = 0;
    for (rows.items) |row| {
        renderRow(&row);
        if (!bus_seen[row.bus]) {
            bus_seen[row.bus] = true;
            bus_count += 1;
        }
    }
    renderFooter(rows.items.len, bus_count);

    return 0;
}
