#!/usr/bin/env python3
"""
publish-release -- push a built, signed, sanitized artifact to the SINGLE
authoritative release channel.

    python bin/publish-release.py --channel oci --artifact-dir dist [--dry-run]

AUTHORITATIVE CHANNEL (decision):
    releases.donjonsec.com  -- static object host on the OCI A1 instance
    (129.213.159.23, Always Free). Rationale:
      * One source of truth kills the live/repo drift + the duplicate
        GitHub/Forgejo "releases" remotes that let v7.5.1 ship stale.
      * OCI A1 is already standing (per infra notes); serving static signed
        zips + manifests is near-zero cost and air-gap-customer friendly
        (a customer can mirror one URL tree).
      * Forgejo stays the SCM source of truth; GitHub release remote is retired.

    Layout published per release:
      releases.donjonsec.com/donjon-platform/<version>/
        donjon-platform-<version>.zip
        donjon-platform-<version>.zip.sig          (Ed25519)
        donjon-platform-<version>.zip.pqc.sig       (ML-DSA-65)
        donjon-platform-<version>.zip.manifest.json
      releases.donjonsec.com/donjon-platform/latest.json  (-> newest version)

GATED -- needs Cris (see needs_cris): OCI bucket/host standup, the
releases.donjonsec.com DNS record, and the runner's DONJON_RELEASE_SSH_KEY.
This script runs in --dry-run by default and performs NO network action until
--no-dry-run is passed AND credentials are present.
"""
from __future__ import annotations

import argparse
import glob
import hashlib
import importlib.util
import json
import os
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent


def _load_release_lib():
    spec = importlib.util.spec_from_file_location(
        "release_lib", REPO_ROOT / "bin" / "release_lib.py"
    )
    mod = importlib.util.module_from_spec(spec)
    sys.modules["release_lib"] = mod
    spec.loader.exec_module(mod)
    return mod


def _find_artifact(artifact_dir: Path):
    zips = sorted(glob.glob(str(artifact_dir / "donjon-platform-*.zip")))
    if not zips:
        return None
    # newest by semver in filename
    rel = _load_release_lib()

    def key(p):
        v = Path(p).stem.replace("donjon-platform-", "")
        try:
            return rel.parse_semver(v)
        except Exception:
            return (0, 0, 0)

    return Path(max(zips, key=key))


def plan_publish(artifact: Path):
    """Return the ordered list of (local, remote) upload pairs + the channel
    base path. Pure function -- no side effects (so --dry-run can print it)."""
    version = artifact.stem.replace("donjon-platform-", "")
    base = f"donjon-platform/{version}"
    files = [
        artifact,
        artifact.with_suffix(artifact.suffix + ".sig"),
        artifact.with_suffix(artifact.suffix + ".pqc.sig"),
        artifact.with_suffix(artifact.suffix + ".manifest.json"),
    ]
    pairs = [(f, f"{base}/{f.name}") for f in files]
    return version, base, pairs


def build_latest_pointer(artifact: Path, version: str) -> dict:
    manifest_path = artifact.with_suffix(artifact.suffix + ".manifest.json")
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    return {
        "product": "donjon-platform",
        "latest": version,
        "artifact": f"donjon-platform/{version}/{artifact.name}",
        "sha256": manifest["sha256"],
        "signature": f"donjon-platform/{version}/{artifact.name}.sig",
        "pqc_signature": f"donjon-platform/{version}/{artifact.name}.pqc.sig",
        "manifest": f"donjon-platform/{version}/{artifact.name}.manifest.json",
    }


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description="Publish a release to the authoritative channel")
    ap.add_argument("--channel", default="oci", choices=["oci"],
                    help="authoritative channel (oci = releases.donjonsec.com)")
    ap.add_argument("--artifact-dir", default=str(REPO_ROOT / "dist"))
    ap.add_argument("--no-dry-run", action="store_true",
                    help="actually upload (requires DONJON_RELEASE_HOST + SSH key)")
    args = ap.parse_args(argv)
    dry_run = not args.no_dry_run

    artifact = _find_artifact(Path(args.artifact_dir))
    if artifact is None:
        print(f"publish: ERROR no donjon-platform-*.zip in {args.artifact_dir}", file=sys.stderr)
        return 2

    # Refuse to publish an unverified artifact: run the sanitization gate first.
    rel = _load_release_lib()
    import importlib.util as _ilu
    spec = _ilu.spec_from_file_location("verify_artifact", REPO_ROOT / "bin" / "verify-artifact.py")
    va = _ilu.module_from_spec(spec); sys.modules["verify_artifact"] = va; spec.loader.exec_module(va)
    if va.verify(artifact) != 0:
        print("publish: ABORT artifact failed sanitization gate", file=sys.stderr)
        return 1

    version, base, pairs = plan_publish(artifact)
    latest = build_latest_pointer(artifact, version)
    host = os.environ.get("DONJON_RELEASE_HOST", "")

    print(f"publish: channel={args.channel} (releases.donjonsec.com)")
    print(f"publish: version={version}  base=/{base}/")
    for local, remote in pairs:
        ok = "OK" if local.exists() else "MISSING"
        print(f"  [{ok}] {local.name:42s} -> releases.donjonsec.com/{remote}")
    print(f"  [PLAN] latest.json -> releases.donjonsec.com/donjon-platform/latest.json")
    print(f"         -> {json.dumps(latest)}")

    if dry_run:
        print("publish: DRY-RUN (default). No network action taken. "
              "Pass --no-dry-run with DONJON_RELEASE_HOST + SSH key to upload.")
        return 0

    if not host:
        print("publish: ERROR --no-dry-run set but DONJON_RELEASE_HOST is empty "
              "(needs Cris: OCI host + DNS + SSH key).", file=sys.stderr)
        return 2

    # Real upload path (executed only in the gated release workflow on the
    # runner). Uses scp/ssh to the OCI static host. Kept dependency-free.
    import subprocess
    remote_root = f"{host}:/var/www/releases.donjonsec.com"
    for local, remote in pairs:
        dest = f"{remote_root}/{remote}"
        subprocess.run(["ssh", host, "mkdir", "-p", f"/var/www/releases.donjonsec.com/{base}"], check=True)
        subprocess.run(["scp", str(local), dest], check=True)
    # write + upload latest.json
    latest_path = Path(args.artifact_dir) / "latest.json"
    latest_path.write_text(json.dumps(latest, indent=2), encoding="utf-8")
    subprocess.run(["scp", str(latest_path),
                    f"{remote_root}/donjon-platform/latest.json"], check=True)
    print(f"publish: uploaded {len(pairs)} files + latest.json to {host}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
