#!/usr/bin/env python3
"""Patch a copied templates/downstream machine flake with install-time values.

The template is the single source of truth for commented opt-ins and the
starter app suite. The installer copies it, then this script only:

  * replaces known placeholders (hostname, username, locale, keyboard, …)
  * fills the __NOMARCHY_INSTALLER__ region with detected/active config
  * sets hardwareProfile on flake.nix

Usage:
  patch-template.py <flake-dir>   # reads a JSON object from stdin
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

BEGIN = "  # __NOMARCHY_INSTALLER_BEGIN__"
END = "  # __NOMARCHY_INSTALLER_END__"


def nix_str(s: str) -> str:
    """Escape a string for a Nix double-quoted literal."""
    return (
        s.replace("\\", "\\\\")
        .replace('"', '\\"')
        .replace("${", "\\${")
        .replace("\n", "\\n")
    )


def keyboard_variant(v: dict) -> str:
    """Return the XKB value, never the picker's display-only sentinel."""
    variant = v.get("keyboardVariant") or ""
    return "" if variant == "(none)" else variant


def replace_once(text: str, old: str, new: str, label: str) -> str:
    if old not in text:
        sys.exit(f"patch-template: missing placeholder for {label}: {old!r}")
    return text.replace(old, new, 1)


def patch_flake(text: str, v: dict) -> str:
    text = replace_once(
        text,
        'description = "My Nomarchy machine";',
        f'description = "{nix_str(v["hostname"])} — my Nomarchy machine";',
        "flake description",
    )
    text = replace_once(
        text,
        'username = "me"; # <- your login name',
        f'username = "{nix_str(v["username"])}"; # <- your login name',
        "flake username",
    )
    # ISO build bakes the ref it was built from (main vs v1). Must match the
    # option surface the installer writes into system.nix (#124).
    if v.get("flakeUrl"):
        text, n = re.subn(
            r'inputs\.nomarchy\.url = "[^"]*";',
            f'inputs.nomarchy.url = "{nix_str(v["flakeUrl"])}";',
            text,
            count=1,
        )
        if n != 1:
            sys.exit("patch-template: could not patch inputs.nomarchy.url")
    profiles = v.get("hardwareProfiles") or []
    if profiles:
        items = " ".join(f'"{nix_str(p)}"' for p in profiles)
        hw_line = f"      hardwareProfile = [ {items} ];"
    else:
        hw_line = "      # hardwareProfile = null;  # no nixos-hardware profiles selected"
    # Replace the optional hardwareProfile comment block with the install choice.
    text, n = re.subn(
        r"\n      # Optional: a nixos-hardware module name for your machine, e\.g\.\n"
        r"      #   hardwareProfile = \"framework-13-7040-amd\";\n"
        r"      # Names: https://github.com/NixOS/nixos-hardware\n"
        r"      # \(the future installer fills this in automatically from DMI data\)\n",
        f"\n{hw_line}\n"
        f"      # Names: https://github.com/NixOS/nixos-hardware\n",
        text,
        count=1,
    )
    if n != 1:
        sys.exit("patch-template: could not patch hardwareProfile block in flake.nix")
    return text


def patch_home(text: str, v: dict) -> str:
    layout = nix_str(v["keyboardLayout"])
    variant = nix_str(keyboard_variant(v))
    text = replace_once(
        text,
        '  nomarchy.keyboard.layout = "us";',
        f'  nomarchy.keyboard.layout = "{layout}";',
        "home keyboard layout",
    )
    text = replace_once(
        text,
        '  nomarchy.keyboard.variant = "";',
        f'  nomarchy.keyboard.variant = "{variant}";',
        "home keyboard variant",
    )
    return text


def build_installer_region(v: dict) -> str:
    lines: list[str] = [
        BEGIN,
        "  # Written by nomarchy-install from live detection. Safe defaults are",
        "  # active; heavier opt-ins stay in the commented catalog below.",
    ]

    # Auto-login is deliberately NOT emitted here — it is seeded into
    # state.json instead (patch_state). A line in system.nix outranks
    # the state default, which would make the System › Auto-login toggle
    # write JSON that nothing reads.

    if v.get("laptop"):
        lines += [
            "  # Laptop power (PPD + menu/Waybar). Uncomment to cap charge at 80%.",
            "  nomarchy.system.power.laptop = true;",
            "  # nomarchy.system.power.batteryChargeLimit = 80;",
        ]
        if v.get("thermald"):
            lines.append(
                "  nomarchy.system.power.thermal.enable = true; # thermald (Intel)"
            )

    hw = v.get("hardware") or {}
    if any(
        hw.get(k)
        for k in ("intel", "amd", "fingerprint", "cameraIr", "npu", "nvidia")
    ):
        lines.append("  # Hardware enablement (auto-detected).")
    if hw.get("intel"):
        lines.append(
            "  nomarchy.hardware.intel.enable = true;            # GuC/HuC (i915)"
        )
        if hw.get("intelGucOff"):
            lines.append(
                "  nomarchy.hardware.intel.guc = false;              # xe driver → GuC default-on"
            )
        lines.append(
            "  # nomarchy.hardware.intel.computeRuntime = true;  # OpenCL/oneVPL (opt-in)"
        )
    if hw.get("amd"):
        lines += [
            "  nomarchy.hardware.amd.enable = true;              # amd-pstate + VA-API",
            "  # nomarchy.hardware.amd.rocm.enable = true;       # ROCm (multi-GB, opt-in)",
            '  # nomarchy.hardware.amd.rocm.gfxOverride = "";    # e.g. "11.0.0" for unlisted iGPU',
        ]
    if hw.get("fingerprint"):
        lines += [
            "  nomarchy.hardware.fingerprint.enable = true;      # fprintd (enroll: fprintd-enroll)",
            "  # nomarchy.hardware.fingerprint.pam = true;       # login + sudo (opt-in)",
        ]
    if hw.get("cameraIr"):
        lines.append(
            "  nomarchy.hardware.camera.hideIrSensor = true;     # dual-sensor: hide IR node"
        )
    if hw.get("npu"):
        vendor = nix_str(hw["npu"])
        lines += [
            f"  # nomarchy.hardware.npu.enable = true;            # {vendor} NPU (experimental; userspace BYO)",
            "  # nomarchy.hardware.latestKernel = true;          # if the NPU driver needs a newer kernel",
        ]
    # NVIDIA: profile is in flake.nix (common-gpu-nvidia). Hybrid/PRIME
    # knobs are plain NixOS — comment-only guidance, same pattern as ROCm.
    if hw.get("nvidia"):
        lines += [
            "  # NVIDIA: common-gpu-nvidia is in hardwareProfile (flake.nix).",
            "  # Hybrid/PRIME, power, open-module — plain NixOS; see docs/HARDWARE.md §6",
            "  # and https://wiki.nixos.org/wiki/Nvidia (bus IDs are machine-specific).",
            "  # hardware.nvidia.prime = { ... };                 # offload/sync",
            "  # hardware.nvidia.powerManagement.enable = true;   # suspend/resume",
            "  # hardware.nvidia.open = false;                    # true = open module (newer cards)",
        ]

    if v.get("resumeOffset") is not None:
        root_uuid = nix_str(v["rootUuid"])
        offset = v["resumeOffset"]
        lines += [
            "  # Swapfile (hibernation-ready: resume points into it).",
            '  swapDevices = [{ device = "/swap/swapfile"; }];',
            f'  boot.resumeDevice = "/dev/disk/by-uuid/{root_uuid}";',
            f'  boot.kernelParams = [ "resume_offset={offset}" ];',
        ]

    # Always on for installer layout (BTRFS + @snapshots).
    lines += [
        "  # Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.",
        "  nomarchy.system.snapper.enable = true;",
        END,
    ]
    return "\n".join(lines) + "\n"


def patch_system(text: str, v: dict) -> str:
    text = replace_once(
        text,
        '  networking.hostName = "my-nomarchy";',
        f'  networking.hostName = "{nix_str(v["hostname"])}";',
        "hostName",
    )
    text = replace_once(
        text,
        '  time.timeZone = "UTC";',
        f'  time.timeZone = "{nix_str(v["timezone"])}";',
        "timeZone",
    )
    text = replace_once(
        text,
        '  i18n.defaultLocale = "en_US.UTF-8";',
        f'  i18n.defaultLocale = "{nix_str(v["locale"])}";',
        "locale",
    )
    text = replace_once(
        text,
        '  services.xserver.xkb.layout = "us";',
        f'  services.xserver.xkb.layout = "{nix_str(v["keyboardLayout"])}";',
        "xkb layout",
    )
    text = replace_once(
        text,
        '  services.xserver.xkb.variant = "";',
        f'  services.xserver.xkb.variant = "{nix_str(keyboard_variant(v))}";',
        "xkb variant",
    )

    # Inject password into the user attrset (template has no password for flake-init).
    user_block = """  users.users.${username} = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" "video" "input" ];
  };"""
    # HASHED_PASSWORD is sha-512 crypt; alphabet is safe in Nix double quotes.
    hashed = nix_str(v["hashedPassword"])
    user_patched = f"""  users.users.${{username}} = {{
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" "video" "input" ];
    initialHashedPassword = "{hashed}";
  }};"""
    text = replace_once(text, user_block, user_patched, "user password")

    if BEGIN not in text or END not in text:
        sys.exit("patch-template: system.nix missing __NOMARCHY_INSTALLER__ markers")
    region = build_installer_region(v)
    text = re.sub(
        re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?",
        region,
        text,
        count=1,
        flags=re.DOTALL,
    )
    return text


def patch_state(text: str, v: dict) -> str:
    """Seed menu-owned settings into state.json.

    These live in the state rather than system.nix precisely so the menu can
    change them later: a baked Nix assignment would outrank the state default
    and strand the toggle. Auto-login is on when the disk is encrypted — the
    LUKS passphrase already gates the machine, so a greeter password is a
    second prompt for the same thing; without LUKS it stays off, where the
    greeter is the only thing standing between power-on and the desktop.
    """
    state = json.loads(text)
    settings = state.setdefault("settings", {})
    if v.get("autoLogin"):
        settings.setdefault("greeter", {})["autoLogin"] = v["username"]
    return json.dumps(state, indent=2) + "\n"


def main() -> None:
    if len(sys.argv) != 2:
        sys.exit("usage: patch-template.py <flake-dir>")
    flake_dir = Path(sys.argv[1])
    vals = json.load(sys.stdin)

    mapping = {
        "flake.nix": patch_flake,
        "home.nix": patch_home,
        "system.nix": patch_system,
        "state.json": patch_state,
    }
    for name, fn in mapping.items():
        path = flake_dir / name
        path.write_text(fn(path.read_text(), vals))
    print(f"patch-template: patched {', '.join(mapping)} in {flake_dir}")


if __name__ == "__main__":
    main()
