#!/usr/bin/env python3
"""svos-front-inventory: regenerate the SVOS front door's credential-free shell paths from the live release.

eshpfi configs/svos-front/, installed at /usr/local/sbin/svos-front-inventory on nh3-svos. Run as root after every
SVOS release (after /opt/svos/current moves), or the service worker's install breaks: its INVENTORY names hashed
asset files, and only those exact paths (plus /sw.js) are served before Basic auth (svos-dev, 2026-10-05).

What it does:
  1. Reads INVENTORY from /opt/svos/current/static/sw.js (a JSON object of path -> sha256).
  2. Refuses unless every path is a plain absolute file path and every file exists with exactly that sha256.
  3. Writes /etc/caddy/svos-public-inventory (one Caddyfile `path` line, root:caddy 0640) atomically.
  4. Validates the whole Caddyfile as the caddy user with the unit's EnvironmentFile (no secret on any argv),
     restores the previous inventory file if validation fails, and otherwise reloads caddy-svos.
--check: steps 1-2 only, plus a diff against the installed file; changes nothing.
"""
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path

STATIC = Path("/opt/svos/current/static")
OUT = Path("/etc/caddy/svos-public-inventory")
SAFE = re.compile(r"^/[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$")
VALIDATE = ["systemd-run", "--quiet", "--wait", "--pipe", "--collect", "-p", "User=caddy", "-p", "Group=caddy",
            "-p", "EnvironmentFile=/etc/caddy/env",
            "-p", "Environment=XDG_DATA_HOME=/var/lib/caddy XDG_CONFIG_HOME=/var/lib/caddy",
            "/usr/local/bin/caddy", "validate", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]


def die(msg):
    sys.exit(f"svos-front-inventory: {msg}")


def inventory():
    src = (STATIC / "sw.js").read_text(encoding="utf-8")
    m = re.search(r"^const INVENTORY = (\{.*?\});$", src, re.M)
    if not m:
        die("no `const INVENTORY = {...};` line in sw.js")
    inv = json.loads(m.group(1))
    if not inv:
        die("INVENTORY is empty")
    for path, digest in inv.items():
        if not SAFE.match(path) or ".." in path.split("/"):
            die(f"unsafe inventory path {path!r}")
        f = STATIC / path.lstrip("/")
        if not f.is_file() or f.is_symlink():
            die(f"{path} is not a regular file in {STATIC}")
        actual = hashlib.sha256(f.read_bytes()).hexdigest()
        if actual != digest:
            die(f"{path} sha256 {actual} != INVENTORY {digest}")
    return sorted(inv)


def render(paths):
    release = os.path.realpath(STATIC.parent)
    return (f"# GENERATED by /usr/local/sbin/svos-front-inventory from {release}/static/sw.js; do not edit.\n"
            f"# Exact credential-free shell paths (hash-checked), plus /sw.js.\n"
            f"path {' '.join(paths + ['/sw.js'])}\n")


def main():
    new = render(inventory())
    old = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
    if "--check" in sys.argv[1:]:
        print("inventory ok;", "installed file is current" if old == new else "installed file DIFFERS:\n" + new)
        return
    if old == new:
        print("unchanged; nothing to do")
        return
    tmp = OUT.with_name(OUT.name + ".new")
    tmp.write_text(new, encoding="utf-8")
    shutil.chown(tmp, "root", "caddy")
    os.chmod(tmp, 0o640)
    backup = OUT.with_name(OUT.name + ".prev")
    if OUT.exists():
        shutil.copy2(OUT, backup)
    os.replace(tmp, OUT)
    if subprocess.run(VALIDATE).returncode != 0:
        if backup.exists():
            os.replace(backup, OUT)
        die("caddy validate FAILED; previous inventory restored, caddy not reloaded")
    subprocess.run(["systemctl", "reload", "caddy-svos"], check=True)
    print("inventory updated and caddy-svos reloaded:\n" + new)


if __name__ == "__main__":
    main()
