# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2015-2016, 2026 InterGenJLU
"""System configuration generator for InterGenOS installer."""

import logging
import os
import subprocess
import uuid
from pathlib import Path

LOG = logging.getLogger("forge.config")


def generate_fstab(target, partitions):
    """Generate /etc/fstab — PARTUUID for plain installs, UUID-of-mapper for LUKS.

    Ratified boot design (2026-04-08/09 + reaffirmed 2026-04-10 FINAL +
    Q-INIT 2026-05-05/06): PARTUUID + rootwait + no-installed-system-
    initramfs for plain installs. The kernel reads PARTUUID from the
    partition table directly without needing userland blkid; that is the
    whole reason for choosing PARTUUID over filesystem UUID.

    LUKS opt-in (D-001) narrowing: when partitions["root_mapper"] is set,
    /etc/fstab references the unlocked filesystem UUID (NOT the underlying
    LUKS partition's PARTUUID; that PARTUUID points at the encrypted
    container which is not directly mountable). The FDE initramfs runs
    fde-init.sh which uses /etc/crypttab to unlock, then exposes
    /dev/mapper/cryptroot whose ext4 UUID this fstab entry references.

    B-041 (T0-2 sub-cluster 6): closes the install-time drift from the
    ratified image-time path (scripts/create-image.sh:148-150 + 177 emit
    PARTUUID). Pre-fix this file emitted root=UUID= for both plain and
    LUKS installs, which would have caused plain installs to halt at
    first boot with "VFS: Unable to mount root fs" — no initramfs means
    no userland blkid means kernel can't resolve filesystem UUID.
    """
    fstab_path = Path(target) / "etc" / "fstab"
    fstab_path.parent.mkdir(parents=True, exist_ok=True)

    lines = [
        "# /etc/fstab — InterGenOS",
        "# Generated by InterGenOS installer",
        "# <file system>  <mount point>  <type>  <options>         <dump>  <pass>",
    ]

    # Root entry: LUKS uses unlocked-mapper filesystem UUID; plain uses
    # PARTUUID of the partition holding the filesystem.
    if partitions.get("root_mapper"):
        root_mapper = partitions["root_mapper"]
        root_uuid = _get_uuid(root_mapper)
        if root_uuid:
            lines.append(f"UUID={root_uuid}  /              ext4    defaults          1       1")
        else:
            lines.append(f"{root_mapper}  /              ext4    defaults          1       1")
    else:
        root_partuuid = _get_partuuid(partitions["root"])
        if root_partuuid:
            lines.append(f"PARTUUID={root_partuuid}  /              ext4    defaults          1       1")
        else:
            lines.append(f"{partitions['root']}  /              ext4    defaults          1       1")

    # EFI System Partition — PARTUUID per ratified boot design (firmware
    # reads partition table directly; PARTUUID is the canonical key).
    # umask=0077 hides ESP contents from non-root users (signing keys may
    # be staged here transiently during signed-boot updates).
    if partitions.get("efi") and partitions.get("esp"):
        esp_partuuid = _get_partuuid(partitions["esp"])
        if esp_partuuid:
            lines.append(f"PARTUUID={esp_partuuid}  /boot/efi      vfat    umask=0077        0       2")
        else:
            lines.append(f"{partitions['esp']}  /boot/efi      vfat    umask=0077        0       2")

    fstab_path.write_text("\n".join(lines) + "\n")


def generate_hostname(target, hostname):
    """Generate /etc/hostname and /etc/hosts."""
    etc = Path(target) / "etc"
    etc.mkdir(parents=True, exist_ok=True)

    (etc / "hostname").write_text(f"{hostname}\n")

    hosts = (
        f"# /etc/hosts — InterGenOS\n"
        f"\n"
        f"127.0.0.1    localhost\n"
        f"127.0.1.1    {hostname}.localdomain {hostname}\n"
        f"::1          localhost ip6-localhost ip6-loopback\n"
        f"ff02::1      ip6-allnodes\n"
        f"ff02::2      ip6-allrouters\n"
    )
    (etc / "hosts").write_text(hosts)


def generate_locale(target, locale="en_US.UTF-8"):
    """Generate /etc/locale.conf AND compile the locale into the target glibc.

    C-010 + J-026 combined fix: previously only wrote /etc/locale.conf, which
    pointed LANG at a locale that didn't exist on the target — glibc-core/
    build.sh:69-74 bakes only C.UTF-8 / en_US / en_US.UTF-8, so any other
    user-picked locale (fr_FR.UTF-8, de_DE.UTF-8, ja_JP.UTF-8, ...) silently
    fell back to C at every login.

    Fix: after writing /etc/locale.conf, invoke localedef in the target chroot
    to compile the locale into /usr/lib/locale/<locale>/ on the installed
    system. localedef parses the locale name as <base>.<encoding> (e.g.
    "fr_FR.UTF-8" → base="fr_FR" + encoding="UTF-8") and reads the matching
    locale definition file under /usr/share/i18n/locales/ + the charmap under
    /usr/share/i18n/charmaps/.

    Skipped for locales without an encoding suffix (C, POSIX, single-name)
    — these are always-present in the glibc-core baked set; localedef would
    fail with "cannot read character map" since they don't have a separate
    encoding step.

    Skipped for malformed locale strings (empty base or encoding after the
    partition) — locale.conf is still written so the user can hand-fix
    post-install, but localedef gets garbage input on malformed names.

    Orchestrator owns virtual_fs lifecycle (C-006); this runs during
    PHASE_CONFIG which is between PHASE_VIRTUAL_FS and PHASE_CLEANUP, so
    the chroot is mountable.
    """
    from .hooks import run_chroot
    etc = Path(target) / "etc"
    (etc / "locale.conf").write_text(f"LANG={locale}\n")

    # C / POSIX / single-name locale — always-present in baked set; nothing
    # to compile. localedef would fail with "cannot read character map".
    if "." not in locale:
        return

    base, _, encoding = locale.partition(".")
    if not base or not encoding:
        # Malformed locale string ("foo.", ".bar", ".") — locale.conf
        # already written; don't run localedef on garbage.
        return

    cmd = f"localedef -i {base} -f {encoding} {locale}"
    rc, stdout, stderr = run_chroot(str(target), cmd)
    if rc != 0:
        raise RuntimeError(
            f"localedef failed for locale {locale!r} "
            f"(base={base!r}, encoding={encoding!r}): "
            f"{(stderr or '').strip()}"
        )


def generate_vconsole(target, keymap="us"):
    """Generate /etc/vconsole.conf."""
    etc = Path(target) / "etc"
    (etc / "vconsole.conf").write_text(
        f"KEYMAP={keymap}\n"
        f"FONT=Lat2-Terminus16\n"
    )


def set_timezone(target, timezone="UTC"):
    """Set timezone via /etc/localtime symlink."""
    localtime = Path(target) / "etc" / "localtime"
    zoneinfo = Path(target) / "usr" / "share" / "zoneinfo" / timezone

    if localtime.exists() or localtime.is_symlink():
        localtime.unlink()

    if zoneinfo.exists():
        localtime.symlink_to(f"/usr/share/zoneinfo/{timezone}")
    else:
        # The requested zone is missing from the target's zoneinfo — surface it
        # instead of silently shipping UTC. This is the class that hid the
        # glibc-core zoneinfo-staging gap (the install passed with the clock on
        # UTC and no signal); never fall back silently.
        LOG.warning("set_timezone: %r not found under %s/usr/share/zoneinfo — "
                    "falling back to UTC; the installed clock will be UTC, not "
                    "%r", timezone, target, timezone)
        localtime.symlink_to("/usr/share/zoneinfo/UTC")


AI_PANEL_EXTENSION_UUID = "intergen@intergenos.org"


def _augment_enabled_extensions_override(override_text, add_uuid):
    """Given the text of the 91 enabled-extensions gschema override, return the
    text for a higher-priority override that adds add_uuid to enabled-extensions.

    Returns the new override text, or None if the source has no parseable
    enabled-extensions line or add_uuid is already present (nothing to do).
    Pure (no I/O) so the list parse/merge is unit-testable.
    """
    import ast
    for line in override_text.splitlines():
        s = line.strip()
        if s.startswith("enabled-extensions="):
            try:
                lst = ast.literal_eval(s.split("=", 1)[1].strip())
            except (ValueError, SyntaxError):
                return None
            if not isinstance(lst, (list, tuple)) or add_uuid in lst:
                return None
            lst = list(lst) + [add_uuid]
            arr = "[" + ", ".join("'%s'" % e for e in lst) + "]"
            return "[org.gnome.shell]\nenabled-extensions=%s\n" % arr
    return None


def enable_ai_panel_extension(target):
    """Opt-in enable of the InterGen AI panel GNOME extension (D-010).

    The default 91_intergenos-extensions.gschema.override deliberately OMITS
    intergen@intergenos.org (AI is opt-in); when the user chooses AI at install,
    ship a higher-priority 95 override (gschema overrides are last-wins by
    filename) that adds it, derived from the 91 default list READ FROM THE TARGET
    so it stays in sync with whatever shipped. The caller must run
    glib-compile-schemas in the chroot afterward for the change to take effect.

    Returns True if the 95 override was written (recompile needed), False if
    skipped (source missing / extension already enabled by default).
    """
    schemas = Path(target) / "usr" / "share" / "glib-2.0" / "schemas"
    src = schemas / "91_intergenos-extensions.gschema.override"
    if not src.exists():
        LOG.warning("enable_ai_panel_extension: %s missing; cannot opt-in the "
                    "AI panel extension", src)
        return False
    new_text = _augment_enabled_extensions_override(
        src.read_text(), AI_PANEL_EXTENSION_UUID)
    if new_text is None:
        return False
    (schemas / "95_intergen-ai.gschema.override").write_text(
        "# Opt-in (D-010): the InterGen AI panel extension is enabled here\n"
        "# ONLY because InterGen AI was chosen at install time. Numbered above\n"
        "# 91 so it wins; derived from 91's list so it stays in sync.\n"
        + new_text)
    return True


def generate_network(target):
    """Generate systemd-networkd DHCP config."""
    netdir = Path(target) / "etc" / "systemd" / "network"
    netdir.mkdir(parents=True, exist_ok=True)

    (netdir / "10-dhcp.network").write_text(
        "[Match]\n"
        "Name=en*\n"
        "\n"
        "[Network]\n"
        "DHCP=yes\n"
    )

    # DNS via systemd-resolved
    resolv = Path(target) / "etc" / "resolv.conf"
    if resolv.exists() or resolv.is_symlink():
        resolv.unlink()
    resolv.symlink_to("/run/systemd/resolve/stub-resolv.conf")


def generate_os_release(target):
    """Generate /etc/os-release and related identity files."""
    etc = Path(target) / "etc"

    (etc / "os-release").write_text(
        'NAME="InterGenOS"\n'
        'VERSION="1.0-dev (Revival)"\n'
        'ID=intergenos\n'
        'ID_LIKE=lfs\n'
        'VERSION_ID=1.0\n'
        'VERSION_CODENAME=revival\n'
        'PRETTY_NAME="InterGenOS 1.0-dev (Revival)"\n'
        'HOME_URL="https://github.com/InterGenJLU/intergenos"\n'
        'BUG_REPORT_URL="https://github.com/InterGenJLU/intergenos/issues"\n'
    )

    (etc / "igos-release").write_text("1.0-dev\n")


def generate_branding(target):
    """Generate /etc/issue and /etc/motd branding files."""
    etc = Path(target) / "etc"

    (etc / "issue").write_text(
        "\n"
        "  InterGenOS 1.0-dev (Revival)\n"
        "  Kernel \\r on \\m (\\l)\n"
        "\n"
    )

    (etc / "motd").write_text(
        "\n"
        '  Welcome to InterGenOS\n'
        '  "A system you understand, can modify, and can trust."\n'
        "\n"
        "  Documentation:  https://github.com/InterGenJLU/intergenos\n"
        "  Report issues:  https://github.com/InterGenJLU/intergenos/issues\n"
        "\n"
    )


def generate_crypttab(target, partitions):
    """Generate /etc/crypttab for LUKS-at-install systems (D-001).

    Writes a single line declaring the LUKS root volume:
        cryptroot UUID=<luks-uuid> none <options>

    - "cryptroot" matches CRYPT_NAME in installer/init/fde-init.sh and
      LUKS_MAPPER_NAME in installer/backend/disks.py — these three must
      agree, since fde-init.sh's awk lookup keys on field 1.
    - "UUID=<luks-uuid>" references the LUKS-formatted underlying
      partition (NOT the ext4 filesystem UUID — that lives on the
      mapper, which doesn't exist until cryptsetup-open has run).
    - "none" tells cryptsetup-open to read the passphrase from stdin
      (which fde-init.sh attaches to /dev/tty so the prompt fires).
    - Options come from partitions["crypt_opts"] (a list set by
      disks.partition_disk). Always includes "luks,discard" baseline;
      adds "tpm2" / "fido2" when D-001 EXPERIMENTAL methods are
      enrolled (operator Option A 2026-05-18T22:52Z). fde-init.sh
      reads field 4 to decide which unlock methods to attempt before
      the passphrase prompt.

    Only emitted when partitions reflects a LUKS install (presence of
    "luks_enabled": True). Plain installs do not get a /etc/crypttab.
    """
    if not partitions.get("luks_enabled"):
        return  # plain install — no crypttab needed

    etc = Path(target) / "etc"
    etc.mkdir(parents=True, exist_ok=True)

    luks_uuid = _get_uuid(partitions["root"])
    if luks_uuid:
        device_ref = f"UUID={luks_uuid}"
    else:
        # blkid lookup failed — fall through to bare partition path. This
        # is fragile across device renumbering between boots, but better
        # than emitting a broken crypttab line that fde-init.sh would
        # refuse with FATAL.
        device_ref = partitions["root"]

    # crypt_opts default = ["luks", "discard"] for plain-passphrase
    # LUKS installs; partition_disk extends with tpm2/fido2 tokens
    # when EXPERIMENTAL slots get enrolled.
    opts_list = partitions.get("crypt_opts") or ["luks", "discard"]
    opts = ",".join(opts_list)

    crypttab = (
        "# /etc/crypttab — InterGenOS\n"
        "# Generated by InterGenOS installer (D-001 LUKS-at-install v1.0)\n"
        "# <name>     <device>                <key file>  <options>\n"
        f"cryptroot   {device_ref}    none        {opts}\n"
    )
    (etc / "crypttab").write_text(crypttab)


def generate_grub_defaults(target, partitions, detect_other_oses=True):
    """Generate /etc/default/grub.

    Ratified boot design: GRUB_CMDLINE_LINUX uses PARTUUID for plain
    installs (no installed-system initramfs → kernel resolves root from
    partition table without userland). LUKS installs use the unlocked
    /dev/mapper/cryptroot UUID; the FDE initramfs (D-005) ignores root=
    cmdline and runs cryptsetup via /etc/crypttab, but this fallback
    cmdline is what the grub-loads-vmlinuz recovery path uses (manual
    unlock required for LUKS recovery — documented in the user FDE guide).

    B-041 (T0-2): closes the install-time drift from the ratified image-
    time path. Pre-fix this emitted root=UUID= for both plain + LUKS;
    plain installs would have halted at first boot with "VFS: Unable
    to mount root fs" — same fix mechanism as generate_fstab.

    GRUB_DISABLE_OS_PROBER honors the user's install-time choice (Option C,
    2026-05-24): when detect_other_oses=True (user opted in to dual-boot
    detection), os-prober runs on every kernel update and keeps Windows /
    other-Linux entries fresh in the menu. When False, os-prober is
    disabled permanently (the secure default — os-prober mounts other
    partitions during grub-mkconfig, an attack surface). Default True
    matches Forge's per-install prompt default (most users installing
    on a machine with another OS want dual-boot to keep working). The
    legacy hardcoded `=false` was the source of a real defect: after
    first install + first kernel update, Windows entries would vanish
    from the GRUB menu because the file disagreed with bootloader.py's
    one-shot env-var override.
    """
    etc_default = Path(target) / "etc" / "default"
    etc_default.mkdir(parents=True, exist_ok=True)

    os_prober_value = "false" if detect_other_oses else "true"

    # Duplicate-root= fix has TWO halves; both required.
    #
    #   1. GRUB_CMDLINE_LINUX intentionally does NOT include `root=...`.
    #      /etc/grub.d/10_linux probes the live filesystem and injects
    #      `root=...` for every linux-image menuentry. If both this file
    #      AND 10_linux supply root=, the bare-vmlinuz menuentry's
    #      linux command line carries TWO `root=` args.
    #
    #   2. GRUB_DISABLE_LINUX_PARTUUID=false explicitly. Upstream
    #      10_linux DEFAULTS this to "true" (line 50:
    #      `: ${GRUB_DISABLE_LINUX_PARTUUID=true}`). Without an explicit
    #      `=false` here, 10_linux falls into its device-path branch
    #      (line 60: `LINUX_ROOT_DEVICE=${GRUB_DEVICE}`) and emits
    #      `root=/dev/sda2` — a renamable kernel-name path that breaks
    #      on USB plug-in shifts and other re-enumeration events.
    #      Setting `=false` here forces 10_linux into the PARTUUID
    #      branch (line 274: `LINUX_ROOT_DEVICE=PARTUUID=…`), matching
    #      the UKI's `/etc/kernel/cmdline` content and the ratified
    #      plain-install boot design (2026-04-08/09 + 2026-04-10 FINAL).
    #
    # Surfaced 2026-05-27 install #29 inspection (operator-direct
    # follow-up to the 6-anomaly wave): /etc/default/grub had
    # `root=PARTUUID=…` in GRUB_CMDLINE_LINUX AND grub.cfg's linux line
    # carried a second root= from 10_linux's probe.
    #
    # The UKI menuentry (GRUB_DEFAULT=0) chainloads the UKI directly;
    # it doesn't consume GRUB_CMDLINE_LINUX. The UKI's own .cmdline
    # section is written by generate_kernel_cmdline() and DOES include
    # `root=…` because that file is the kernel's only cmdline source
    # on the UKI boot path.
    # D-030 transparent boot (GBC001.5, 2026-06-05): no `quiet`, no
    # `loglevel=3`. Both suppress the boot output D-030 deliberately surfaces
    # — `quiet` hides systemd [OK]/[FAILED]; `loglevel=3` (err+ only) hides the
    # kernel INFO/NOTICE/WARNING (levels 4-6) that flag a "weird module load or
    # hardware quirk" the moment it happens. Stripping both lets the kernel fall
    # back to its compiled default verbosity; systemd shows status by default
    # once `quiet` is gone. See docs/users/desktop-experience.md.
    # pcie_aspm=off: hard-disable PCIe ASPM bus-wide. The RTL8821CE (rtw88) on
    # HP 15-dw0037wm-class hardware leaves ASPM L1 only briefly on IRQ; under
    # NAPI poll the link stays in L1, RX DMA stalls, and the card escalates from
    # AER "Correctable RxErr" to a fatal "pci bus timeout, check dma status" ->
    # "Hardware restart was requested" that wedges the PCIe bus and LOCKS the
    # machine (observed on .241, 2026-06-10 — see G3-12). The rtw88_pci
    # disable_aspm=y module param alone did NOT prevent the lockup (firmware can
    # re-enable L1 substates the module param doesn't reach), so we force ASPM
    # off at the kernel level too. Pairs with disable_msi=y in
    # /etc/modprobe.d/rtw88-aspm.conf. Bus-wide costs some PCIe idle power on
    # laptops, but a card that freezes the system is the worse outcome.
    cmdline_tail = "rootwait vt.default_utf8=1 pcie_aspm=off"
    # GRUB theme + gfxmode + background + color block. Mirrors the
    # set that intergenos-grub-theme/build.sh would have appended to
    # /etc/default/grub at package install time, but pulled into the
    # Forge SoT here because this function does a full write_text()
    # — anything the package wrote earlier would otherwise be wiped.
    # Surfaced 2026-05-27 install #27: theme settings package-installed
    # then overwritten here, leaving the installed system with no
    # GRUB_THEME, so grub-mkconfig's 00_header never emitted `set
    # theme=...` and the InterGenOS background never displayed.
    #
    # Resolution chain mirrors the 12 PNGs shipped by intergenos-grub-
    # theme to /boot/grub/backgrounds/<WxH>.png + the 1920x1080
    # default at /boot/grub/grub_background.png (Steam hardware-survey
    # set; covers 16:9 + 16:10 + 21:9 aspect families).
    gfx_chain = (
        "3840x2400,3840x2160,3440x1440,3072x1920,2560x1600,2560x1440,"
        "1920x1200,1920x1080,1680x1050,1600x900,1366x768,1280x800,"
        "1024x768,800x600,auto"
    )
    (etc_default / "grub").write_text(
        "# GRUB defaults for InterGenOS\n"
        "GRUB_DEFAULT=0\n"
        "GRUB_TIMEOUT_STYLE=menu\n"
        "GRUB_TIMEOUT=10\n"
        'GRUB_DISTRIBUTOR="InterGenOS"\n'
        'GRUB_CMDLINE_LINUX_DEFAULT=""\n'
        f'GRUB_CMDLINE_LINUX="{cmdline_tail}"\n'
        "GRUB_DISABLE_LINUX_PARTUUID=false\n"
        f"GRUB_DISABLE_OS_PROBER={os_prober_value}\n"
        f"GRUB_GFXMODE={gfx_chain}\n"
        'GRUB_BACKGROUND="/boot/grub/grub_background.png"\n'
        'GRUB_THEME="/boot/grub/themes/intergenos/theme.txt"\n'
        'GRUB_COLOR_NORMAL="light-gray/black"\n'
        'GRUB_COLOR_HIGHLIGHT="light-blue/black"\n'
    )


def _root_arg_for_cmdline(partitions):
    """Resolve the canonical `root=` kernel-cmdline fragment for an install.

    Plain installs: `root=PARTUUID=<part-uuid-of-root>` — the kernel reads
    PARTUUID from the partition table at mount time without needing an
    initramfs or userland blkid. This is the ratified design (2026-04-08/
    09 + 2026-04-10 FINAL + Q-INIT).

    LUKS installs: `root=UUID=<fs-uuid-of-mapper>` — the FDE initramfs
    handles the actual unlock via /etc/crypttab and exposes the mapper
    node; the kernel root= is informational on this path because
    fde-init.sh has already done switch_root by the time the kernel-level
    root= resolution would fire. Kept consistent with the fstab entry.

    Returns a string like `root=PARTUUID=abc-123` or `root=UUID=def-456`.
    Falls back to `root=<raw-device-path>` only when blkid fails — last-
    resort path that should not be exercised on a healthy install.
    """
    if partitions.get("root_mapper"):
        root_mapper = partitions["root_mapper"]
        root_uuid = _get_uuid(root_mapper)
        return f"root=UUID={root_uuid}" if root_uuid else f"root={root_mapper}"
    root_partuuid = _get_partuuid(partitions["root"])
    if root_partuuid:
        return f"root=PARTUUID={root_partuuid}"
    return f"root={partitions['root']}"


def generate_kernel_cmdline(target, partitions):
    """Write /etc/kernel/cmdline — UKI source-of-truth cmdline (B-041 + D-005).

    The linux-kernel package's post_install hook (D-005 Phase A) bakes
    /etc/kernel/cmdline into the UKI's .cmdline section via ukify. Without
    this file the hook falls back to /proc/cmdline (post-install.sh:141-145)
    which at install time is the LIVE ISO's cmdline (`root=live-iso-stuff
    igos.mode=live ...`). That would land in the first installed-system
    UKI and cause boot failure.

    Writing /etc/kernel/cmdline here is the install-time half of the B-041
    resolution explicitly anticipated by the post-install hook comment
    "B-041 cmdline drift: resolves via UKI .cmdline section bundling
    (sourced from /etc/kernel/cmdline; falls back to /proc/cmdline if
    unset)".

    Cmdline contents:
      root=PARTUUID=...  (or root=UUID=... for LUKS — see
                          _root_arg_for_cmdline rationale)
      rootwait           — wait for storage to enumerate (required for
                          no-initramfs path; matches image-time create-
                          image.sh:177 emission)
      quiet              — suppress kernel printk noise; `splash` removed
                          per D-030 no-Plymouth transparency posture
                          (operator ratification 2026-05-22, commit
                          8de756f1; see docs/users/desktop-experience.md
                          §"No Plymouth boot splash")
      loglevel=3         — surface warnings + errors but suppress noise
                          (matches installer/init/cmdline.live.txt
                          posture for the installed system)
    """
    kernel_dir = Path(target) / "etc" / "kernel"
    kernel_dir.mkdir(parents=True, exist_ok=True)

    root_arg = _root_arg_for_cmdline(partitions)
    # vt.default_utf8=1 is explicit-set rather than relying on kernel
    # default (which is "1" on modern kernels but unconfigured during
    # the early-boot framebuffer-takeover window). Without it, the
    # framebuffer console + VT encoding can briefly disagree, rendering
    # unmapped Unicode chars as "@" placeholders during the window
    # between kernel takeover of fbcon and systemd-vconsole-setup
    # (which runs late, after PID-1-systemd is up). Surfaced 2026-05-26
    # install #18 first-boot.
    # D-030 transparent boot (GBC001.5): no `quiet`/`loglevel=3` on the UKI
    # cmdline either — same rationale as cmdline_tail above.
    # pcie_aspm=off: force PCIe ASPM off bus-wide on the installed UKI cmdline.
    # The RTL8821CE (rtw88) escalates from AER RxErr noise to a fatal "pci bus
    # timeout, check dma status" DMA hang that wedges the bus and LOCKS the
    # machine (G3-12, observed on .241 2026-06-10); the rtw88_pci disable_aspm=y
    # module param alone did NOT prevent it. Pairs with disable_msi=y in
    # /etc/modprobe.d/rtw88-aspm.conf.
    cmdline = f"{root_arg} rootwait vt.default_utf8=1 pcie_aspm=off"

    (kernel_dir / "cmdline").write_text(cmdline + "\n")


def generate_all(target, partitions, hostname="intergenos",
                 locale="en_US.UTF-8", keymap="us", timezone="UTC",
                 detect_other_oses=True):
    """Generate all system configuration files.

    detect_other_oses: Option C 2026-05-24 — threaded through to
    generate_grub_defaults so the installed system's
    /etc/default/grub permanently reflects the user's dual-boot-
    detection choice. See generate_grub_defaults docstring.
    """
    generate_fstab(target, partitions)
    generate_crypttab(target, partitions)   # D-001 LUKS-at-install
    generate_kernel_cmdline(target, partitions)  # B-041 UKI source-of-truth
    generate_hostname(target, hostname)
    generate_machine_id(target)
    generate_locale(target, locale)
    generate_vconsole(target, keymap)
    set_timezone(target, timezone)
    generate_network(target)
    generate_os_release(target)
    generate_branding(target)
    generate_grub_defaults(target, partitions, detect_other_oses=detect_other_oses)


def generate_machine_id(target):
    """Write a random 32-hex-char /etc/machine-id on the target.

    systemd-firstboot.service activates on boot when /etc/machine-id is
    missing OR set to the literal string "uninitialized" (see man
    systemd-firstboot.service ConditionFirstBoot=). If we don't write
    this file, the installed system drops the user into the interactive
    firstboot prompt on first boot — a terrible UX for a 'completed'
    install. Surfaced 2026-05-26 install attempt #18 first-boot: user
    saw firstboot prompt + scary screen artifacts, didn't know if system
    was bricked.

    The value must be 32 lowercase hex chars + trailing newline, mode
    0444 (read-only for everyone — systemd refuses to read otherwise).
    See man machine-id(5). uuid.uuid4().hex gives exactly the right
    shape (128-bit random, hex-encoded, no dashes).

    Sibling file /var/lib/dbus/machine-id is shipped as a symlink to
    /etc/machine-id by the dbus package — no additional handling needed
    here.
    """
    mid = uuid.uuid4().hex
    path = Path(target) / "etc" / "machine-id"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(mid + "\n")
    path.chmod(0o444)


def _get_uuid(device):
    """Get filesystem UUID for a device."""
    result = subprocess.run(
        ["blkid", "-s", "UUID", "-o", "value", device],
        capture_output=True, text=True
    )
    return result.stdout.strip() if result.returncode == 0 else None


def _get_partuuid(device):
    """Get GPT/MBR PARTUUID for a partition.

    Required for the ratified PARTUUID + rootwait + no-installed-system-
    initramfs boot path (B-041 resolution). The kernel reads PARTUUID
    from the partition table directly at mount time, without needing
    userland blkid or an initramfs — that is the whole point of choosing
    PARTUUID over filesystem UUID for plain installs.

    Returns None on blkid failure (e.g., MBR partition without disk
    signature) — callers fall back to the raw device path.
    """
    result = subprocess.run(
        ["blkid", "-s", "PARTUUID", "-o", "value", device],
        capture_output=True, text=True
    )
    return result.stdout.strip() if result.returncode == 0 else None
