#!/usr/bin/env python3
"""One-shot generator for the curated ioctl-interface registry
(Part 13 §5.2.3, sub-phase 3B).

Reads the source-of-truth macro list for each curated driver,
runs each macro through the live Linux UAPI header via a C-compiler
probe (see ``tests/conformance/_ioctl_compute.py``), and writes
the resulting JSON files at
``hrb-code/src/data/driver_interfaces/<driver>.json``.

USAGE
-----
Run from the repo root after a UAPI header update:

    .venv/bin/python scripts/regenerate_ioctl_registry.py

The CI gate (``tests/conformance/test_ioctl_registry_currency.py``)
then re-derives the numbers against the live header and asserts
each JSON entry matches — i.e. it catches drift between
regeneration runs.

NEW DRIVER CHECKLIST
--------------------
1. Add a `CuratedDriver` entry below with its macro list.
2. Run the generator.
3. Commit the resulting JSON.
4. The CI gate picks it up automatically.
"""

from __future__ import annotations

import json
import sys
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

# Allow importing from tests/conformance/ without installing.
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parent
sys.path.insert(0, str(REPO_ROOT))

from tests.conformance._ioctl_compute import (  # noqa: E402
    compute_ioctl_numbers,
    has_compiler,
)


@dataclass(frozen=True)
class CuratedIoctl:
    """One ioctl entry — its macro name, expected arg shape, and
    direction tag (matches the C macro: r / w / rw / none).

    ``variable_size`` flags parameterized macros (e.g. ``HIDIOCGRAWNAME(len)``)
    whose encoded number carries a runtime size field. For those,
    the generator calls the macro with `len=0` to derive a base
    value, and the JSON entry carries `variable_size: true` so
    consumers know to overlay the runtime `len` when computing
    the actual ioctl number to pass to `ioctl(fd, X, ...)`."""

    name: str
    arg: str
    direction: str  # one of: r, w, rw, none
    variable_size: bool = False


@dataclass(frozen=True)
class CuratedDriver:
    """One driver entry: its registered name + the macro list +
    char-device + uevent metadata. Maintainer policy in the
    README; this dataclass is the data-only definition."""

    driver: str
    module: str
    header: str  # without angle brackets
    maintainer: str
    ioctls: tuple[CuratedIoctl, ...]
    char_devices: tuple[dict[str, str], ...]
    uevent_actions: tuple[str, ...]


# The curated registry — extend by appending new CuratedDriver
# entries. Per the spec's §5.2.3 list, this should cover at least:
# USBDEVFS, VFIO, DRM-render, V4L2, HIDRAW. v1 of 3B ships the two
# most directly relevant to the broker's current scope (USBDEVFS for
# the USB worker; HIDRAW because it's the most common USB driver
# class clients want a UAPI for). VFIO / DRM-render / V4L2 are
# follow-on commits when use-cases land — adding them is mechanical
# (append + regenerate + commit) under the curation policy in the
# data dir's README.

CURATED: tuple[CuratedDriver, ...] = (
    CuratedDriver(
        driver="usbdevfs",
        module="usbcore",
        header="linux/usbdevice_fs.h",
        maintainer="mgf-hrb",
        ioctls=(
            CuratedIoctl("USBDEVFS_CONTROL", "struct usbdevfs_ctrltransfer", "rw"),
            CuratedIoctl("USBDEVFS_BULK", "struct usbdevfs_bulktransfer", "rw"),
            CuratedIoctl("USBDEVFS_RESETEP", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_SETINTERFACE", "struct usbdevfs_setinterface", "r"),
            CuratedIoctl("USBDEVFS_SETCONFIGURATION", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_GETDRIVER", "struct usbdevfs_getdriver", "w"),
            CuratedIoctl("USBDEVFS_SUBMITURB", "struct usbdevfs_urb", "r"),
            CuratedIoctl("USBDEVFS_DISCARDURB", "(none)", "none"),
            CuratedIoctl("USBDEVFS_REAPURB", "void *", "w"),
            CuratedIoctl("USBDEVFS_REAPURBNDELAY", "void *", "w"),
            CuratedIoctl("USBDEVFS_DISCSIGNAL", "struct usbdevfs_disconnectsignal", "r"),
            CuratedIoctl("USBDEVFS_CLAIMINTERFACE", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_RELEASEINTERFACE", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_CONNECTINFO", "struct usbdevfs_connectinfo", "w"),
            CuratedIoctl("USBDEVFS_IOCTL", "struct usbdevfs_ioctl", "rw"),
            CuratedIoctl("USBDEVFS_HUB_PORTINFO", "struct usbdevfs_hub_portinfo", "r"),
            CuratedIoctl("USBDEVFS_RESET", "(none)", "none"),
            CuratedIoctl("USBDEVFS_CLEAR_HALT", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_DISCONNECT", "(none)", "none"),
            CuratedIoctl("USBDEVFS_CONNECT", "(none)", "none"),
            CuratedIoctl("USBDEVFS_CLAIM_PORT", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_RELEASE_PORT", "unsigned int", "r"),
            CuratedIoctl("USBDEVFS_GET_CAPABILITIES", "__u32", "r"),
            CuratedIoctl("USBDEVFS_DISCONNECT_CLAIM", "struct usbdevfs_disconnect_claim", "r"),
            CuratedIoctl("USBDEVFS_ALLOC_STREAMS", "struct usbdevfs_streams", "r"),
            CuratedIoctl("USBDEVFS_FREE_STREAMS", "struct usbdevfs_streams", "r"),
            CuratedIoctl("USBDEVFS_DROP_PRIVILEGES", "__u32", "w"),
            CuratedIoctl("USBDEVFS_GET_SPEED", "(none)", "none"),
            CuratedIoctl("USBDEVFS_FORBID_SUSPEND", "(none)", "none"),
            CuratedIoctl("USBDEVFS_ALLOW_SUSPEND", "(none)", "none"),
            CuratedIoctl("USBDEVFS_WAIT_FOR_RESUME", "(none)", "none"),
        ),
        char_devices=(
            {"pattern": "/dev/bus/usb/*/*", "permissions": "0664"},
        ),
        uevent_actions=("bind", "unbind"),
    ),
    CuratedDriver(
        driver="hidraw",
        module="hidraw",
        header="linux/hidraw.h",
        maintainer="mgf-hrb",
        ioctls=(
            # Fixed-number macros first.
            CuratedIoctl("HIDIOCGRDESCSIZE", "int", "r"),
            CuratedIoctl("HIDIOCGRDESC", "struct hidraw_report_descriptor", "r"),
            CuratedIoctl("HIDIOCGRAWINFO", "struct hidraw_devinfo", "r"),
            # Variable-size macros: `_IOC(...)` with the size field
            # supplied by the caller at runtime as `len`. The base
            # number we record below is the value at `len=0`;
            # consumers OR-in `(actual_len << _IOC_SIZESHIFT)` (in
            # practice they call the live macro; the registry's
            # base_number is for shape/comparison + UAPI docs).
            CuratedIoctl("HIDIOCGRAWNAME", "char[len]", "r", variable_size=True),
            CuratedIoctl("HIDIOCGRAWPHYS", "char[len]", "r", variable_size=True),
            CuratedIoctl("HIDIOCSFEATURE", "char[len]", "rw", variable_size=True),
            CuratedIoctl("HIDIOCGFEATURE", "char[len]", "rw", variable_size=True),
            CuratedIoctl("HIDIOCGRAWUNIQ", "char[len]", "r", variable_size=True),
            CuratedIoctl("HIDIOCSINPUT", "char[len]", "rw", variable_size=True),
            CuratedIoctl("HIDIOCGINPUT", "char[len]", "rw", variable_size=True),
            CuratedIoctl("HIDIOCSOUTPUT", "char[len]", "rw", variable_size=True),
            CuratedIoctl("HIDIOCGOUTPUT", "char[len]", "rw", variable_size=True),
        ),
        char_devices=(
            {"pattern": "/dev/hidraw*", "permissions": "0660"},
        ),
        uevent_actions=("bind", "unbind"),
    ),
    CuratedDriver(
        driver="vfio",
        module="vfio",
        header="linux/vfio.h",
        maintainer="mgf-hrb",
        ioctls=(
            # VFIO macros all expand to `_IO(VFIO_TYPE, ...)` — no
            # size, no arg. Direction is "none" everywhere. We
            # curate the container + group + device + iommu surfaces
            # that a passthrough client routinely touches; the
            # migration ioctls (VFIO_MIG_*) and PCI_HOT_RESET stay
            # out of v1 (they're niche).
            CuratedIoctl("VFIO_GET_API_VERSION", "(none)", "none"),
            CuratedIoctl("VFIO_CHECK_EXTENSION", "(none)", "none"),
            CuratedIoctl("VFIO_SET_IOMMU", "(none)", "none"),
            CuratedIoctl("VFIO_GROUP_GET_STATUS", "(none)", "none"),
            CuratedIoctl("VFIO_GROUP_SET_CONTAINER", "(none)", "none"),
            CuratedIoctl("VFIO_GROUP_UNSET_CONTAINER", "(none)", "none"),
            CuratedIoctl("VFIO_GROUP_GET_DEVICE_FD", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_GET_INFO", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_GET_REGION_INFO", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_GET_IRQ_INFO", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_SET_IRQS", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_RESET", "(none)", "none"),
            CuratedIoctl("VFIO_DEVICE_FEATURE", "(none)", "none"),
            CuratedIoctl("VFIO_IOMMU_MAP_DMA", "(none)", "none"),
            CuratedIoctl("VFIO_IOMMU_UNMAP_DMA", "(none)", "none"),
        ),
        char_devices=(
            {"pattern": "/dev/vfio/*", "permissions": "0600"},
        ),
        uevent_actions=("bind", "unbind"),
    ),
    CuratedDriver(
        driver="drm",
        module="drm",
        header="drm/drm.h",
        maintainer="mgf-hrb",
        ioctls=(
            # DRM uses DRM_IOWR / DRM_IOR / DRM_IOW wrappers (all
            # type='d'). The compute helper sees the expanded
            # numbers; the helper itself doesn't care about the
            # wrapper layer. We curate the generic DRM surface —
            # render-node use (the GPU DMA-BUF / GEM path the
            # `drm_render_ready` adapter watches) lives in driver-
            # specific UAPIs (drm/i915_drm.h, drm/amdgpu_drm.h)
            # which are per-vendor; not in v1.
            CuratedIoctl("DRM_IOCTL_VERSION", "struct drm_version", "rw"),
            CuratedIoctl("DRM_IOCTL_GET_UNIQUE", "struct drm_unique", "rw"),
            CuratedIoctl("DRM_IOCTL_GET_MAGIC", "struct drm_auth", "r"),
            CuratedIoctl("DRM_IOCTL_SET_VERSION", "struct drm_set_version", "rw"),
            CuratedIoctl("DRM_IOCTL_GEM_CLOSE", "struct drm_gem_close", "w"),
            CuratedIoctl("DRM_IOCTL_GEM_OPEN", "struct drm_gem_open", "rw"),
            CuratedIoctl("DRM_IOCTL_GET_CAP", "struct drm_get_cap", "rw"),
            CuratedIoctl("DRM_IOCTL_SET_CLIENT_CAP", "struct drm_set_client_cap", "w"),
            CuratedIoctl("DRM_IOCTL_PRIME_HANDLE_TO_FD", "struct drm_prime_handle", "rw"),
            CuratedIoctl("DRM_IOCTL_PRIME_FD_TO_HANDLE", "struct drm_prime_handle", "rw"),
        ),
        char_devices=(
            {"pattern": "/dev/dri/card*", "permissions": "0660"},
            {"pattern": "/dev/dri/renderD*", "permissions": "0666"},
        ),
        uevent_actions=("bind", "unbind"),
    ),
    CuratedDriver(
        driver="v4l2",
        module="videodev",
        header="linux/videodev2.h",
        maintainer="mgf-hrb",
        ioctls=(
            # V4L2's macro set is enormous (capture / output /
            # subdev / mediactl etc). We curate the capture-path
            # subset every `mgf-hrb usb_input_ready`-style adapter
            # touches: query / format / requeue / dequeue / ctrl.
            # Output, vbi, m2m, subdev are deferred.
            CuratedIoctl("VIDIOC_QUERYCAP", "struct v4l2_capability", "r"),
            CuratedIoctl("VIDIOC_ENUM_FMT", "struct v4l2_fmtdesc", "rw"),
            CuratedIoctl("VIDIOC_G_FMT", "struct v4l2_format", "rw"),
            CuratedIoctl("VIDIOC_S_FMT", "struct v4l2_format", "rw"),
            CuratedIoctl("VIDIOC_REQBUFS", "struct v4l2_requestbuffers", "rw"),
            CuratedIoctl("VIDIOC_QUERYBUF", "struct v4l2_buffer", "rw"),
            CuratedIoctl("VIDIOC_QBUF", "struct v4l2_buffer", "rw"),
            CuratedIoctl("VIDIOC_DQBUF", "struct v4l2_buffer", "rw"),
            CuratedIoctl("VIDIOC_STREAMON", "int", "w"),
            CuratedIoctl("VIDIOC_STREAMOFF", "int", "w"),
            CuratedIoctl("VIDIOC_G_INPUT", "int", "r"),
            CuratedIoctl("VIDIOC_S_INPUT", "int", "rw"),
        ),
        char_devices=(
            {"pattern": "/dev/video*", "permissions": "0660"},
        ),
        uevent_actions=("bind", "unbind"),
    ),
)


def _render_ioctl(io: CuratedIoctl, number: int) -> dict[str, object]:
    """Render one ioctl into the JSON shape. For fixed-number
    ioctls: emit `number` + omit `variable_size`. For variable-
    size: emit `base_number` (the value at size=0) + `variable_size:
    true` so consumers know to overlay the runtime `len` into the
    size field when computing the real number."""
    if io.variable_size:
        return {
            "name": io.name,
            "base_number": f"0x{number:08x}",
            "variable_size": True,
            "arg": io.arg,
            "direction": io.direction,
        }
    return {
        "name": io.name,
        "number": f"0x{number:08x}",
        "arg": io.arg,
        "direction": io.direction,
    }


def render_one(driver: CuratedDriver) -> dict[str, object]:
    """Build the JSON-serialisable dict for one curated driver."""
    if not has_compiler():
        raise RuntimeError(
            "regenerate requires a C compiler (cc or gcc); install build-essential"
        )
    macros: list[tuple[str, str]] = [
        (i.name, f"{i.name}(0)" if i.variable_size else i.name)
        for i in driver.ioctls
    ]
    numbers = compute_ioctl_numbers(driver.header, macros)
    if numbers is None:
        raise RuntimeError("compiler reported missing post-has_compiler check")

    today = datetime.now(tz=UTC).date().isoformat()
    return {
        "driver": driver.driver,
        "module": driver.module,
        "header": f"<{driver.header}>",
        "maintainer": driver.maintainer,
        "last_verified": today,
        "ioctls": [
            _render_ioctl(i, numbers[i.name])
            for i in driver.ioctls
        ],
        "char_devices": list(driver.char_devices),
        "uevent_actions": list(driver.uevent_actions),
    }


def main() -> int:
    out_dir = REPO_ROOT / "hrb-code" / "src" / "data" / "driver_interfaces"
    out_dir.mkdir(parents=True, exist_ok=True)
    for driver in CURATED:
        record = render_one(driver)
        out_path = out_dir / f"{driver.driver}.json"
        out_path.write_text(json.dumps(record, indent=2) + "\n")
        print(f"wrote {out_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
