#!/usr/bin/env python3
"""Fill in any empty service tokens in CoreSystemConfig.json, and write tokens.txt.

Runs before validation on every build (see lib/coreconfig.sh), so a fresh config only
needs the interesting fields filled in — addresses, rooms, which services you want —
and the random secrets appear on their own.

THE CRITICAL PART IS THAT GENERATED TOKENS ARE WRITTEN BACK TO THE CONFIG. A token is
only useful because two machines agree on it: the container host runs `identity` with
IDENTITY_TOKEN, and every kiosk image is built with the same value baked into its URLs.
Generating fresh randomness per build would produce a door panel that cannot talk to
the service it was built for — so this fills empties ONCE, persists them, and never
touches a value that is already set.

tokens.txt is a convenience copy for the human: same values, plus what each one is for
and which of them this script deliberately cannot generate.

Usage: generate-tokens.py [config.json]
"""

from __future__ import annotations

import json
import secrets
import sys
from pathlib import Path

# name -> (enable flag that makes it required, what it's for)
GENERATABLE = {
    "identity_token": ("identity", "identity's API — also baked into the door panel and kitchen display"),
    "pantry_vision_token": ("pantry_vision", "pantry-vision's API — also baked into the kitchen display"),
    "transit_token": ("transit", "transit's API — called by Home Assistant for voice departures"),
}

# Deliberately NOT generated, with the reason. Inventing a value for any of these would
# produce something that looks configured and cannot work.
NOT_GENERATABLE = {
    "ha_token": "a Long-Lived Access Token from Home Assistant's own UI (profile -> Security). "
                "It doesn't exist until HA is running and an account exists, and only HA can mint it.",
    "opnsense_api_key": "issued by OPNsense itself (System -> Access -> Users -> API keys), which "
                        "shows the key and secret once and downloads them as a .txt. Give that user "
                        "only the 'Services: Intrusion Detection' privilege.",
    "opnsense_api_secret": "the other half of the OPNsense API key above — they are minted together "
                           "and only OPNsense has them.",
    "mqtt_password": "must match what Mosquitto is configured to accept. Generating one here "
                     "would just mean nothing can connect.",
    "admin_password_hash": "a crypt(3) hash for the installer — generate with: mkpasswd -m sha-512",
    "kiosk_password": "only needed if a kiosk account should have a password at all.",
    "ssh_authorized_key": "your existing PUBLIC key (~/.ssh/id_ed25519.pub) — a generated one "
                          "would have no private half you hold.",
}


def main(argv: list[str]) -> int:
    path = Path(argv[1]) if len(argv) > 1 else Path(__file__).resolve().parent.parent / "CoreSystemConfig.json"
    if not path.exists():
        print(f"error: {path} not found", file=sys.stderr)
        return 2

    try:
        cfg = json.loads(path.read_text())
    except json.JSONDecodeError as exc:
        print(f"error: {path} is not valid JSON — line {exc.lineno}: {exc.msg}", file=sys.stderr)
        return 2

    secrets_block = cfg.setdefault("secrets", {})
    enable = (cfg.get("container_host", {}) or {}).get("enable", {}) or {}

    generated = []
    for name, (flag, _purpose) in GENERATABLE.items():
        # A token for a service you haven't enabled isn't needed, and filling it in
        # anyway would put a live-looking credential in a file for no reason.
        if not enable.get(flag):
            continue
        if (secrets_block.get(name) or "").strip():
            continue
        secrets_block[name] = secrets.token_hex(32)
        generated.append(name)

    if generated:
        # Written back with the same 2-space indent the template uses, so the diff is
        # just the token lines rather than the whole file reformatting.
        path.write_text(json.dumps(cfg, indent=2) + "\n")
        print(f"  generated {len(generated)} token(s) into {path.name}: {', '.join(generated)}")

    _write_tokens_txt(path.parent / "tokens.txt", cfg, enable)
    return 0


def _write_tokens_txt(dest: Path, cfg: dict, enable: dict) -> None:
    secrets_block = cfg.get("secrets", {}) or {}
    lines = [
        "SmartestHome — service tokens",
        "=" * 60,
        "",
        "GENERATED by tools/generate-tokens.py from CoreSystemConfig.json.",
        "This is a convenience copy for you, not a source of truth — the config is.",
        "Regenerated on every build; edit the config, not this file.",
        "",
        "TREAT THIS FILE AS A CREDENTIAL. It is gitignored, but it is plain text on",
        "disk: every token below grants full API access to the service named, and",
        "identity's in particular can grant a person the right to open a smart lock.",
        "",
    ]

    for name, (flag, purpose) in GENERATABLE.items():
        if not enable.get(flag):
            lines += [f"{name}:", "    (not set — container_host.enable." + flag + " is false)", ""]
            continue
        value = (secrets_block.get(name) or "").strip() or "(EMPTY — run a build to generate)"
        lines += [f"{name}:", f"    {value}", f"    for: {purpose}", ""]

    lines += ["", "Not generated here, and why:", ""]
    for name, why in NOT_GENERATABLE.items():
        value = (secrets_block.get(name) or "").strip()
        state = "set" if value else "EMPTY"
        lines += [f"{name}  [{state}]", f"    {why}", ""]

    dest.write_text("\n".join(lines))
    print(f"  wrote {dest.name}")


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