#!/usr/bin/env python3
"""sandbroker-vapid-init - generate/persist the Web Push VAPID keypair.

Web Push authenticates the sender ("application server") with a VAPID keypair.
The PRIVATE key signs the push requests; the PUBLIC key (the "application server
key") is handed to the browser at subscribe time and pinned into the resulting
subscription. Regenerating the keypair invalidates every existing subscription,
so this is deliberately GENERATE-ONCE: if a private key already exists it is
left untouched and only the derived public-key text file is (re)materialised.

Layout (relative to SANDBROKER_BASE, default /opt/sandbroker; dev: the dev base):
  etc/vapid_private.pem  - PKCS8 PEM, the signing key. Kept off the agent:
                           prod cutover chowns it 0440 sandbroker:claude-broker so
                           only the broker group (which the sandboxed agent is
                           NOT in) can read it; dev leaves it 0400 gray.
  etc/vapid_public.txt   - the application server key (base64url, unpadded, the
                           uncompressed EC point). Public by definition; 0444.
                           The stdlib-only dashboard serves this verbatim, so it
                           never needs the `cryptography` stack itself.

Needs pywebpush's deps (py-vapid + cryptography) -> run under the repo venv. The
shim below re-execs into it if this interpreter can't import them.

    SANDBROKER_BASE=/opt/sandbroker sandbroker-vapid-init
"""
import base64
import os
import sys
from pathlib import Path


def _reexec_into_venv() -> None:
    """If py-vapid/cryptography aren't importable here, re-exec under the repo
    venv interpreter (bin/../.venv) which has them."""
    try:
        import py_vapid  # noqa: F401
        return
    except ImportError:
        pass
    venv_root = Path(__file__).resolve().parent.parent / ".venv"
    venv_py = venv_root / "bin" / "python"
    # The venv python is often a symlink to the system python3, so compare venv
    # ROOTS (sys.prefix), not resolved binaries, to detect "already in the venv".
    if venv_py.exists() and Path(sys.prefix).resolve() != venv_root.resolve():
        os.execv(str(venv_py), [str(venv_py), os.path.abspath(__file__), *sys.argv[1:]])
    sys.stderr.write(
        "sandbroker-vapid-init: py-vapid/cryptography missing and no repo venv at %s\n"
        "  create it: python3 -m venv %s && %s/bin/pip install pywebpush py-vapid\n"
        % (venv_py, venv_py.parent.parent, venv_py.parent.parent))
    raise SystemExit(2)


_reexec_into_venv()

from cryptography.hazmat.primitives import serialization  # noqa: E402
from py_vapid import Vapid02  # noqa: E402

BASE = os.environ.get("SANDBROKER_BASE", "/opt/sandbroker")
ETC = os.path.join(BASE, "etc")
PRIV = os.path.join(ETC, "vapid_private.pem")
PUB = os.path.join(ETC, "vapid_public.txt")


def application_server_key(vapid: Vapid02) -> str:
    """The browser-facing public key: the uncompressed EC point, base64url,
    no padding. This exact string is what pushManager.subscribe() wants."""
    raw = vapid.public_key.public_bytes(
        serialization.Encoding.X962,
        serialization.PublicFormat.UncompressedPoint)
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")


def _write(path: str, data: bytes, mode: int) -> None:
    tmp = path + ".tmp"
    fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
    with os.fdopen(fd, "wb") as fh:
        fh.write(data)
    os.replace(tmp, path)
    os.chmod(path, mode)


def main() -> int:
    os.makedirs(ETC, exist_ok=True)
    if os.path.exists(PRIV):
        # Never overwrite an existing private key -- that would silently break
        # every subscription already stored in browsers. Just (re)derive the
        # public text file from it so the dashboard always has it.
        v = Vapid02.from_file(PRIV)
        _write(PUB, (application_server_key(v) + "\n").encode(), 0o444)
        sys.stderr.write("sandbroker-vapid-init: kept existing %s; refreshed %s\n" % (PRIV, PUB))
        return 0
    v = Vapid02()
    v.generate_keys()
    pem = v.private_key.private_bytes(
        serialization.Encoding.PEM,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption())
    _write(PRIV, pem, 0o400)
    _write(PUB, (application_server_key(v) + "\n").encode(), 0o444)
    sys.stderr.write("sandbroker-vapid-init: generated %s (0400) and %s (0444)\n" % (PRIV, PUB))
    return 0


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