"""Release signing — authenticity for the zip update channel.

A checksum proves a download arrived intact; it does not prove **who made it**.
Whoever controls the bytes served at keystone.computer controls the `.sha256`
sidecar too. This module adds the missing half: every release is signed with an
Ed25519 key that only Keystone holds, and every brain carries the matching
public key and refuses any zip it can't verify as authentically Keystone's.

Trust model: the software already running (the current, trusted `.alexandria/app`)
verifies the NEXT zip before installing it — so a compromised website cannot get
a brain to run code Keystone did not sign. The public key is pinned here in the
software (``RELEASE_PUBKEY_HEX``); rotating it is a visible, reviewable change
that ships in a signed release, so an attacker cannot swap the key without
already holding the old one.

Signature file format (`Alexandria.zip.sig`), three text lines:

    alexandria-ed25519 v1
    key <first 16 hex of the public key>
    <base64 of the 64-byte detached signature over the exact zip bytes>

The ``key`` line lets a brain tell "signed by a key I don't pin" apart from
"signature invalid," and makes a key rotation legible. Verification is pure
stdlib + the vendored `_ed25519` — no dependency, works in the no-venv path.
"""
from __future__ import annotations

import base64
import binascii
from pathlib import Path

from . import _ed25519 as ed

SIG_SUFFIX = ".sig"
_MAGIC = "alexandria-ed25519 v1"

#: The pinned Keystone release public key (64 hex = 32 bytes).
#: The private half lives only in the CI secret ``ALEXANDRIA_SIGNING_KEY`` and the
#: maintainer's offline backup. Rotating this is a reviewable change that must ship
#: in a release signed by the OLD key (see docs/RELEASING.md → Key rotation).
RELEASE_PUBKEY_HEX = "67c92a12dd575a98476ba1993e11175bd21f0f1e9009b1798f8419e53d0a4d9c"

#: The pinned BETA public key (F16). Pre-release/test builds are signed by this key so a
#: tester installs them WITHOUT ``--allow-unsigned`` — a beta build is signed, just by a
#: second known key. It is trusted ONLY on a brain that opted into the beta channel
#: (``alexandria.beta_channel: true``); production brains never trust it, because
#: convergence to a signed release is always release-signed, so beta trust would buy them
#: nothing but blast radius if the seed leaked. Its private half lives only in
#: ``ALEXANDRIA_BETA_SIGNING_KEY`` (a maintainer-local secret, not a CI secret). A beta build
#: is LABELED "beta" and is superseded by the release-key build on convergence (F15).
#: Placeholder zeros = not configured. Rotating it is a plain code change (betas are
#: ephemeral) — and the leak-recovery lever: ship a release that re-pins this, and every
#: brain re-pins on its next auto-update.
BETA_PUBKEY_HEX = "d9185039e5cd11d78eb4b84c51b6adc20d1007df7be0b0278126660d302e3770"


class SignatureError(Exception):
    """A verification refusal, message meant for the owner verbatim."""


def key_id(public_key_hex: str) -> str:
    return public_key_hex.strip().lower()[:16]


def sign_bytes(data: bytes, secret_key_hex: str, public_key_hex: str) -> bytes:
    sig = ed.sign(data, bytes.fromhex(secret_key_hex), bytes.fromhex(public_key_hex))
    return sig


def format_sig_file(sig: bytes, public_key_hex: str) -> str:
    return (f"{_MAGIC}\n"
            f"key {key_id(public_key_hex)}\n"
            f"{base64.b64encode(sig).decode('ascii')}\n")


def sign_file(path: Path, secret_key_hex: str, public_key_hex: str) -> Path:
    """Write ``<path>.sig`` for ``path``. Returns the signature path.
    Used by the build/release side, where the secret key is available."""
    path = Path(path)
    data = path.read_bytes()
    sig = sign_bytes(data, secret_key_hex, public_key_hex)
    out = path.with_name(path.name + SIG_SUFFIX)
    out.write_text(format_sig_file(sig, public_key_hex), encoding="utf-8")
    return out


def parse_sig_file(text: str) -> tuple:
    """Return (key_id, signature_bytes) from a .sig file's text, or raise
    SignatureError if it isn't one."""
    lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()]
    if len(lines) < 3 or lines[0] != _MAGIC:
        raise SignatureError("not an Alexandria signature file")
    if not lines[1].startswith("key "):
        raise SignatureError("signature file missing its key id")
    kid = lines[1][4:].strip()
    try:
        sig = base64.b64decode(lines[2], validate=True)
    except (ValueError, binascii.Error):
        raise SignatureError("signature file body is not valid base64")
    if len(sig) != 64:
        raise SignatureError("signature is not 64 bytes")
    return kid, sig


def verify_file(path: Path, sig_path: Path, public_key_hex: str = None) -> None:
    """Raise SignatureError unless ``sig_path`` is a valid signature of ``path``
    under the pinned release key. Returns None on success.

    Refuses, in order: a missing/malformed signature file; a signature made by a
    different key than we pin (a clear, distinct message); a signature that does
    not verify over the exact bytes on disk."""
    public_key_hex = (public_key_hex or RELEASE_PUBKEY_HEX).strip().lower()
    path, sig_path = Path(path), Path(sig_path)
    if not sig_path.is_file():
        raise SignatureError(
            f"no signature file ({sig_path.name}) — refusing to install an "
            "unsigned or unverifiable release. Fetch it from "
            "https://keystone.computer/Alexandria.zip (its .sig is published "
            "beside it), or a GitHub release asset.")
    if sig_path.stat().st_size > 64 * 1024:
        raise SignatureError(
            f"{sig_path.name} is implausibly large for a signature file — refusing.")
    kid, sig = parse_sig_file(sig_path.read_text(encoding="utf-8"))
    if kid != key_id(public_key_hex):
        raise SignatureError(
            f"the release is signed by key {kid}, not the key this software "
            f"pins ({key_id(public_key_hex)}) — refusing to install. This is "
            "either a key rotation you need a newer Alexandria to trust, or a "
            "release that is not genuinely Keystone's.")
    if not ed.verify(sig, path.read_bytes(), bytes.fromhex(public_key_hex)):
        raise SignatureError(
            f"the signature on {path.name} does not verify — the file is "
            "corrupted or was not signed by Keystone. Refusing to install.")


def is_configured(public_key_hex: str = None) -> bool:
    """True once a real (non-placeholder) release key is pinned."""
    key = (public_key_hex or RELEASE_PUBKEY_HEX).strip().lower()
    return len(key) == 64 and key != "0" * 64


def trusted_keys(allow_beta: bool = False) -> dict:
    """The authorities a brain accepts for an *install* (not a release-verify): the pinned
    release key always, and the beta key ONLY when the brain has opted into the beta channel
    (F16 opt-in). Production brains never trust the beta key — convergence to a signed release
    is always release-signed, so beta trust buys production nothing but blast radius if the
    beta seed leaks. ``{authority: pubkey_hex}``."""
    keys = {"release": RELEASE_PUBKEY_HEX}
    if allow_beta and is_configured(BETA_PUBKEY_HEX):
        keys["beta"] = BETA_PUBKEY_HEX
    return keys


def verify_any(path: Path, sig_path: Path, *, allow_beta: bool = False) -> str:
    """Verify an install candidate against a trusted authority and return which one signed
    it (``"release"`` / ``"beta"``). The release key is always trusted; the beta key only
    when ``allow_beta`` (the brain opted into the beta channel). Raises ``SignatureError`` if
    none match. The signature's own key id only SELECTS which trusted pubkey to check, so
    this is not weaker than the single-key path: a blob still has to carry a genuine
    signature by a key this build pins (and, for beta, one the brain chose to trust).

    Release-verification (``verify-release``, the official-release gate) stays STRICT — it
    calls ``verify_file`` (release key only). Only the *install/update* path is here."""
    path, sig_path = Path(path), Path(sig_path)
    keys = trusted_keys(allow_beta=allow_beta)
    if sig_path.is_file() and sig_path.stat().st_size <= 64 * 1024:
        try:
            kid, _ = parse_sig_file(sig_path.read_text(encoding="utf-8"))
        except SignatureError:
            kid = None
        for authority, pub in keys.items():
            if kid == key_id(pub):
                verify_file(path, sig_path, pub)         # exact-bytes check under that key
                return authority
        # A genuine BETA build on a brain that hasn't opted in: say exactly how to accept it,
        # rather than the generic wrong-key refusal.
        if (not allow_beta and is_configured(BETA_PUBKEY_HEX)
                and kid == key_id(BETA_PUBKEY_HEX)):
            raise SignatureError(
                f"{path.name} is a BETA build (signed by the beta key), and this brain is not "
                "on the beta channel. To test beta builds, set `alexandria.beta_channel: true` "
                "in Agent/config.json (a one-time opt-in), then update again — or pass "
                "--allow-unsigned to install just this one without verifying.")
    # No trusted key matched the sig's key id (or no sig file): fall through to the
    # release-key verify so the owner gets the precise, familiar refusal message.
    verify_file(path, sig_path, RELEASE_PUBKEY_HEX)
    return "release"                                     # unreachable — verify_file raised
