"""``mgf-hrb explain <topic>`` — operator-facing explainers.

Each topic carries:
  1. A visual diagram of the hardware shape (ASCII / box-drawing).
  2. A vocabulary section mapping the diagram terms 1:1 to the columns
     used in the matching scan / doctor output, so a reader can find any
     term they see in the output here and vice-versa.
  3. A worked example walking through one realistic row end-to-end.

Topics:
  usb     — USB topology + mgf-hrb-usb-scan columns + a real row decoded
  pci     — PCIe topology + BDF / IOMMU groups / VFIO
  doctor  — what each doctor check actually checks, and why it matters

Adding a new topic: add the explainer text to ``_TOPICS`` and update the
listing in ``_render_topic_list``. No new dependencies.
"""

from __future__ import annotations

import difflib
import logging
import sys
from typing import Final

logger = logging.getLogger(__name__)


# ---- USB explainer ---------------------------------------------------------

_USB_EXPLAIN: Final[str] = """\
USB — hardware topology and how `mgf-hrb-usb-scan` reports it
=============================================================

USB is a forest of trees. Each tree is rooted at one Host Controller chip
on your motherboard (xHCI for USB 3.x, EHCI for USB 2.0, dwc3 for some
embedded SoCs). The kernel assigns each Host Controller a Bus number; every
device plugged into that Host Controller belongs to that Bus.

1. The visual: how a USB bus is shaped
---------------------------------------

                        Your computer
                              │
        ┌─────────────────────┼───────────────────────┐
        │                     │                       │
        ▼                     ▼                       ▼
   Host Controller       Host Controller         Host Controller
   chip (xHCI)           chip (xHCI)             chip (xHCI, USB 2)
        │                     │                       │
   [Bus 1]                [Bus 3]                 [Bus 4]   ← the "Bus" column
        │                     │                       │
   Root Hub              Root Hub                 Root Hub  ← built-in, on the
   (port 0)              (port 0)                 (port 0)    Host Controller
        │                     │
   ┌────┴─────┐        ┌──────┼──────────┬───────────┐
   ▼          ▼        ▼      ▼          ▼           ▼
  port 1   port 2   port 7  port 9    port 10    (more)
  (empty)   │       │       │         │           ports
            ▼       ▼       ▼         ▼
        Device   Device  Device     Device         ← a "device" plugs into
        on the   on the  on the     on the           one port. devnum
        port     port    port       port             rotates on replug —
                                                     not in the URN.
                          │ this device is a COMPOSITE — it ships
                          ▼ multiple INTERFACES, each independently
                          ┌─────────────────────────────────────┐
                          │ if0  Audio Control  (class 0x01)    │
                          │ if1  Audio Streaming (class 0x01)   │
                          │ if2  Video (class 0x0e)             │
                          │ if3  HID (class 0x03)               │   ← the "If"
                          │ if4  Vendor-specific (class 0xff)   │     column
                          └─────────────────────────────────────┘
                                  ▲
                          each interface is one row in mgf-hrb-usb-scan.
                          one composite device = N rows (never 1 row).

What you see in the table:

  Bus  Port-chain   If  VID:PID    Class               URN
  ---  -----------  --  ---------  ------------------  ---------------------------------
    3  9             0  5986:2178  Video               hw:/usb/bus3/port-9/dev/if0
    3  9             1  5986:2178  Video               hw:/usb/bus3/port-9/dev/if1
    3  9             3  5986:2178  HID                 hw:/usb/bus3/port-9/dev/if3


2. Column by column
-------------------

  Bus
    The Host Controller index the kernel assigned. Stable for the duration
    of the boot; NOT stable across reboots (the kernel renumbers if you
    plug in a USB PCIe card, for example). Maps to `bus<N>` in the URN.

  Port-chain
    The root-port-to-leaf hub-traversal chain, joined by dots. `9` means
    "root-hub port 9". `9.4` means "root-hub port 9 → downstream hub's
    port 4". `9.4.1.2` is a four-deep hub stack (USB allows up to 7).
    Survives bus rescans. Changes if the cable is moved to a different
    port. Maps to `port-<chain>` in the URN.

  If
    The `bInterfaceNumber` field of the USB interface descriptor — which
    of the device's interfaces this row represents. A simple device (a
    USB stick) has one interface (if0). A composite device (a USB headset
    with audio + volume keys) has several. Maps to `if<K>` in the URN.

  VID:PID
    `idVendor`:`idProduct` read from the parent device's USB descriptor.
    VID is assigned by the USB-IF (a Linux Foundation virtual hub is
    1d6b; Apple is 05ac; Intel is 8087). PID is the vendor's product
    code. Together they identify the *model*, not the individual unit
    (two identical iFigs share VID:PID — the `serial_number` field, when
    present, distinguishes the units).

  Class
    `bInterfaceClass` decoded — the USB-IF class code for this interface.
    Common codes you'll see: `Audio` (0x01), `HID` (0x03), `Mass Storage`
    (0x08), `Hub` (0x09), `Video` (0x0e), `Wireless` (0xe0). When the
    kernel doesn't recognise the code we print `Class 0xNN`, never a
    guess (G2 — honest about what we don't know).

  URN
    The canonical name the broker uses for this interface. The grammar
    is `hw:/usb/bus<N>/port-<chain>/dev/if<K>` (design Part 2.2). Every
    column above appears in the URN — so given any column, you can find
    the URN, and given any URN you can read the structure back.


3. Worked example — decoding one row
------------------------------------

  Row:  3  9   0  5986:2178  Video               hw:/usb/bus3/port-9/dev/if0

   → "On the third USB Host Controller (Bus 3), look at root-hub port 9
     (Port-chain `9`, a direct attach to the root hub — no intermediate
     hub). The device plugged in there is Vimicro (VID `5986`),
     product `2178` — a webcam. We're looking at its first interface
     (If 0), which the device advertises as Video class (0x0e). The
     canonical URN for that single interface is
     `hw:/usb/bus3/port-9/dev/if0`. The same device's interfaces 1, 2
     (more Video) and 3 (HID for volume control) appear as adjacent
     rows because the table is sorted by (Bus, Port-chain, If)."

  If you saw three rows with the same VID:PID and Port-chain but different
  If numbers — that's ONE physical composite device, claimed per-interface.
  This matters: claiming if0 doesn't disturb if3. (Design §2.4: blast
  radius of a USB claim is one interface, never one device.)


4. What's NOT in the table (and why)
-------------------------------------

  • The kernel's per-session `devnum` (the <bbb>/<ddd> in
    /dev/bus/usb/<bbb>/<ddd>) is deliberately omitted — it rotates on
    every replug and stores it as identity is a bug magnet.
  • Alternate settings (`bAlternateSetting`) aren't in the URN —
    they're a property of the typed op (set_alt_interface).
  • Endpoints aren't enumerated — they're an interface-internal detail.


5. See also
-----------

  • Tool: ./hrb-code/zig-out/bin/mgf-hrb-usb-watch
       — live `add`/`remove` events from the kernel's NETLINK_KOBJECT_UEVENT
         multicast. Run it, then plug/unplug a USB device — each interface
         add/remove fires one line in the same URN form as mgf-hrb-usb-scan.
  • Recipe: recipes/track-a-device-across-a-mode-change.md
       — how to handle a USB device that re-enumerates with a new
         VID:PID (DFU mode, dual-personality dongles).
  • Recipe: recipes/usb-event-monitor-with-role-interfaces.md
       — subscribing to add/remove/change events (Tier-1 — see
         the recipe banner for what ships today).
  • Design: hrb-design/mgf-hrb-usb-worker.md
       — the full USB worker design (URN grammar §2.2, identity
         continuity §4, the seam ops mapped to USB §5).
"""


# ---- PCIe explainer --------------------------------------------------------

_PCI_EXPLAIN: Final[str] = """\
PCIe — hardware topology and how the broker addresses PCI devices
=================================================================

PCIe (PCI Express) is the high-speed bus that connects your CPU to most
of the heavy hardware — GPU, NVMe storage, network cards, USB Host
Controllers themselves. Unlike USB, PCIe is point-to-point: every device
has a dedicated electrical link back toward the CPU's Root Complex.

1. The visual: how a PCIe topology is shaped
---------------------------------------------

                               CPU
                                │
                          Root Complex          ← integrated into the CPU.
                          (also the                also where the IOMMU lives
                          IOMMU root)              (the chip that prevents a
                                │                  rogue device from DMA-ing
                                │                  into kernel memory).
                ┌───────────────┼───────────────┐
                ▼               ▼               ▼
            Root Port       Root Port       Root Port
            on Bus 0        on Bus 0        on Bus 0
            (00:01.0)       (00:1c.0)       (00:1f.0)
                │               │                │
                ▼               ▼                ▼
              Bus 1          Bus 2           Chipset
            (PCIe link)   (PCIe link)      (LPC bridge,
                │               │            SATA, USB
                ▼               ▼            controllers,
            01:00.0         02:00.0         audio, …)
            (GPU)           (NVMe drive)
                │
            01:00.1                          ← the *same* card may host
            (HDMI audio                        multiple "functions" —
            on the GPU)                        the .0 / .1 in the BDF.


2. The vocabulary
-----------------

  BDF — Bus:Device.Function
    Every PCIe endpoint has a stable address of the form `BB:DD.F`:
      • Bus     two-hex-digit bus number (0x00 is the root, others
                are assigned as the kernel walks PCIe bridges).
      • Device  two-hex-digit device number on that bus
                (always 0x00 for a Root Port; can be up to 0x1f).
      • Function  single hex digit. A single physical card can expose
                  up to 8 functions — a GPU usually has function 0 for
                  graphics and function 1 for HDMI audio.

  Domain
    A four-hex-digit prefix (`0000:`) for hosts with multiple separate
    PCIe roots (rare on consumer hardware). Usually `0000` everywhere.
    Together: `0000:01:00.0` is the canonical "full BDF" form.

  IOMMU group
    A set of PCIe functions that share an IOMMU translation context.
    The kernel decides grouping based on the topology (devices behind
    the same bridge usually share a group). **The whole group moves
    together** on a VFIO bind — you can't take if you can't take its
    siblings. This is why a "passthrough one function" plan sometimes
    surprises you with "you'll also take its 7 friends."

  VFIO
    The kernel ABI mgf-hrb uses to bind a PCIe function for passthrough.
    Requires `/dev/vfio/vfio` to exist and the function's IOMMU group
    to be bindable. `mgf-hrb doctor` checks both — see
    `mgf-hrb explain doctor` for what that looks like.


3. What you see in `mgf-hrb scan`
---------------------------------

  PCI functions  (2):
    hw:/pci/0000:01:00.0/fn0
    hw:/pci/0000:02:00.0/fn0

  The URN grammar is `hw:/pci/<domain>:<bus>:<device>.<function>/fn<n>`.
  The `/fn<n>` suffix is redundant with the `.<function>` BDF tail today
  (matched for forward-compatibility with multi-function abstractions).

  Mapping back to the diagram above:
    • `0000:01:00.0/fn0` is the GPU's function 0 on Bus 1.
    • `0000:02:00.0/fn0` is the NVMe drive's function 0 on Bus 2.


4. Worked example — claiming a GPU for passthrough
---------------------------------------------------

  URN:  hw:/pci/0000:01:00.0/fn0   (a discrete GPU)

   → The broker looks up the IOMMU group for 01:00.0. Suppose the group
     also contains 01:00.1 (HDMI audio). Two outcomes:

       (a) The host doesn't separate them: the broker refuses with
           `Unsupported{reason=group_not_isolated}` — taking the GPU
           would mean taking the HDMI audio too, and you didn't ask
           for that. The fix is an ACS-quirks kernel option, or a
           motherboard that isolates the slot.

       (b) The group is isolated (or contains only 01:00.0 because
           the platform's ACS support is good): the broker binds the
           group via VFIO, hands the holder a raw_channel fd backed
           by /dev/vfio/<group>, and the holder talks to the device
           directly. The broker is now out of the use path (G4).


5. See also
-----------

  • Recipe: recipes/claim-a-device-for-vfio-passthrough.md
       — the VManager pattern, end-to-end.
  • Design: hrb-design/mgf-hrb-complete-design.md
       Part 6.5 — the Linux PCI backend; honest-Unsupported
       acquire when /dev/vfio is absent.
  • FEEDBACK paper LINUX-01 — IOMMU-presence enforcement (why
       `mgf-hrb doctor` flags no-IOMMU hosts as WARN, not OK).
"""


# ---- Doctor explainer ------------------------------------------------------

_DOCTOR_EXPLAIN: Final[str] = """\
mgf-hrb doctor — what each check actually checks, and why it matters
=====================================================================

`mgf-hrb doctor` walks a fixed checklist of preconditions the broker
needs to do useful work. Each check belongs to one of three categories;
each row in the report has a status, a detail, optionally a `why this
matters` line, and (when actionable) a `fix` line.

1. The visual: what the broker depends on
------------------------------------------

                Operator running `mgf-hrb` CLI
                            │
                            │  (1) Unix socket — checked by doctor
                            ▼
                   ┌─────────────────────┐
                   │   broker (Zig)      │
                   │   listens on        │
                   │   /tmp/mgf-hrb.sock │
                   └─────────┬───────────┘
                             │
                  ┌──────────┴──────────┐
                  ▼                     ▼
            Substrate              Hardware policy
            (sysfs + /dev nodes)   (IOMMU, udev rule,
                  │                 privileged group)
                  │
       ┌──────────┼─────────────┐
       ▼          ▼             ▼
   /sys/bus/  /sys/bus/    /dev/vfio/vfio    ← all checked by doctor
   pci/       usb/         (PCIe passthrough)
   devices    devices


2. Categories in the report
----------------------------

  ── Substrate (sysfs + /dev nodes) ──
    Can the broker read the kernel's view of devices? Reads
    /sys/bus/pci/devices and /sys/bus/usb/devices via raw syscalls
    (no libusb / libpci). Absence means enumerate returns empty but
    doesn't error — `mgf-hrb scan` would just show "no resources".

  ── Hardware policy (IOMMU, group membership, udev) ──
    Can the broker safely *touch* the hardware? IOMMU groups
    matter for PCIe passthrough (DMA-safety); the udev rule + group
    membership matter for non-root USB claim. These are policy, not
    presence — failures here mean "broker can see the hardware but
    can't legally claim it."

  ── Broker (socket + cap table) ──
    Is the broker itself reachable and bootstrapped? Without a live
    socket the SDK can't speak to the broker at all (FAIL — not even
    enumerate works).


3. Each check, with its mapping
-------------------------------

  PCI sysfs / USB sysfs
    READS:  /sys/bus/pci/devices, /sys/bus/usb/devices
    FAILS WHEN: the directory doesn't exist (containerised hosts that
                hide /sys), or isn't readable by the current user.
    IMPACT IF FAILED: `mgf-hrb scan` returns empty for that family.

  VFIO device
    READS:  /dev/vfio/vfio
    FAILS WHEN: absent (sandboxes, containers, hosts without IOMMU
                support enabled).
    IMPACT IF FAILED: `acquire` on PCIe functions honestly returns
                Unsupported (G2 — never a fake success). Read-side
                ops (scan, info) still work.

  IOMMU groups
    READS:  /sys/kernel/iommu_groups, enumerates its children.
    FAILS WHEN: directory absent (no IOMMU enabled at boot) or empty.
    IMPACT IF FAILED: DMA-capable passthrough is unsafe (a device
                could DMA anywhere in host memory). WARN, not FAIL,
                because non-passthrough ops still work.

  mgf-hrb udev rule
    DEFERRED (the rule check arrives with Phase U3 of the USB worker).
    Will look for /etc/udev/rules.d/70-mgf-hrb.rules and validate the
    `mgf-hrb` group exists with appropriate device-node ACLs.

  caller in privileged group
    DEFERRED. Will check that the current user is in the broker's
    privileged group (per the udev rule). Without this, non-root
    claim returns NotPermitted with no hint.

  broker socket
    PROBES: connect() against the socket path.
    FAILS WHEN: socket file absent, or present but connect refuses
                (a stale file left by a crashed broker).
    IMPACT IF FAILED: the SDK can't reach the broker. Everything
                downstream is blocked.

  backend capabilities / capability table seeded
    DEFERRED. Will report the live `BackendCapabilities` for each
    backend (which seam ops are supported, at which latency tier)
    and the trust-anchor capability seeding status.


4. Statuses, in plain language
-------------------------------

  OK        passed.
  WARN      degraded but the broker still works for some flows
              (typical: no VFIO, no IOMMU — read-side ops still
              run; only claim/raw are affected).
  FAIL      the broker won't function. The Summary box will list
              FAIL rows under "ACTION REQUIRED" with their fix lines.
  ...       (deferred) the check isn't yet wired in code. Not a
              problem; the check shape is documented so an operator
              knows what *will* be checked when the worker grows.


5. See also
-----------

  • FEEDBACK paper TOOLS-01 (PARTIALLY SHIPPED 2026-05-24) — the
       design conversation that produced this command.
  • Design: hrb-design/mgf-hrb-usb-worker.md §7 — the operator-
       facing surface, including the udev rule recipe and the
       structured `Fix` enum for permission errors.
"""


# ---- Topic table -----------------------------------------------------------

_TOPICS: Final[dict[str, str]] = {
    "usb": _USB_EXPLAIN,
    "pci": _PCI_EXPLAIN,
    "doctor": _DOCTOR_EXPLAIN,
}


# ---- Public entry ----------------------------------------------------------


def explain_command(*, topic: str | None) -> int:
    """Dispatch to the explainer for ``topic``. When ``topic`` is None or an
    empty string, render the topic listing. Unknown topics get a listing
    plus a "did you mean" suggestion (via difflib)."""
    logger.debug("explain starting", extra={"topic": topic or "(list)"})

    if not topic or topic == "list":
        _render_topic_list()
        return 0

    text = _TOPICS.get(topic.lower())
    if text is None:
        _render_unknown_topic(topic)
        return 2

    print(text)
    return 0


def _render_topic_list() -> None:
    print("mgf-hrb explain — operator-facing topic catalogue")
    print()
    print("  Each topic carries a hardware-shape diagram, a column-by-column")
    print("  mapping to the matching scan/doctor output, and a worked example.")
    print()
    print("  Available topics:")
    for name in sorted(_TOPICS.keys()):
        print(f"    mgf-hrb explain {name}")
    print()
    print("  Tip: pair the explainer with the live tool — for example:")
    print("    ./hrb-code/zig-out/bin/mgf-hrb-usb-scan   # see your hardware")
    print("    mgf-hrb explain usb                       # learn what it means")


def _render_unknown_topic(topic: str) -> None:
    print(f"mgf-hrb explain: unknown topic '{topic}'.", file=sys.stderr)
    print(file=sys.stderr)
    close = difflib.get_close_matches(topic.lower(), _TOPICS.keys(), n=2, cutoff=0.4)
    if close:
        print("  Did you mean:", file=sys.stderr)
        for c in close:
            print(f"    mgf-hrb explain {c}", file=sys.stderr)
        print(file=sys.stderr)
    print("  Available topics:", file=sys.stderr)
    for name in sorted(_TOPICS.keys()):
        print(f"    mgf-hrb explain {name}", file=sys.stderr)


__all__ = ["explain_command"]
