#!/usr/bin/env python3
"""Baut ClawdDotNet fuer eine Plattform und veroeffentlicht es im Deploymentcenter.

    python deploy/publish.py --rid win-x64 --channel dev
    python deploy/publish.py --rid linux-x64 --channel dev --dry-run

Drei Schritte, die einzeln zu leicht vergessen werden:

  1. dotnet publish in ein frisches Verzeichnis. Frisch, weil der Packager alles
     einpackt, was er vorfindet - Reste eines aelteren Laufs landeten sonst mit
     im Paket.
  2. update-agent dazulegen. Ohne ihn findet ResolveAgentPath() nichts, und die
     Anwendung kann sich nicht selbst aktualisieren. Die Erstinstallation legt
     ihn NICHT ins Zielverzeichnis - sie laeuft von dort, wo der Benutzer sie
     hingelegt hat.
  3. pack-and-deploy aufrufen.

Die Version kommt aus Directory.Build.props. Sie hier noch einmal anzugeben
waere eine zweite Pflegestelle - und der Packager bricht bei einer Abweichung
zur Assembly ohnehin ab.
"""

import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
PROJECT = REPO / "src" / "ClawdDotNet.Desktop" / "ClawdDotNet.Desktop.csproj"
CONFIG = REPO / "deploy" / "packager.config.json"
PACKAGER = Path(
    "J:/Softwareprojekte/Deploymentcenter/client-dotnet/Deploymentcenter.Packager"
    "/bin/Release/net8.0/pack-and-deploy.exe"
)
INSTALLER_BASE = "https://dc.mhdf.de/installer"


def read_version() -> str:
    text = (REPO / "Directory.Build.props").read_text(encoding="utf-8")
    match = re.search(r"<Version>([^<]+)</Version>", text)
    if not match:
        sys.exit("Directory.Build.props enthaelt kein <Version>.")
    return match.group(1).strip()


def run(cmd: list[str], **kw) -> None:
    print("  $", " ".join(str(c) for c in cmd))
    result = subprocess.run(cmd, **kw)
    if result.returncode != 0:
        sys.exit(f"Abgebrochen (Rueckgabewert {result.returncode}): {cmd[0]}")


def publish(rid: str, out_dir: Path) -> None:
    if out_dir.exists():
        shutil.rmtree(out_dir)
    out_dir.mkdir(parents=True)

    run([
        "dotnet", "publish", str(PROJECT),
        "-c", "Release", "-r", rid, "--self-contained", "false",
        "-o", str(out_dir), "--nologo", "-v", "q",
    ])


def fetch_agent(rid: str, out_dir: Path) -> None:
    """Holt den update-agent und prueft die Pruefsumme.

    Bewusst das ausgelieferte Binary statt eines selbst gebauten: Es ist
    dasselbe, das die Erstinstallation verwendet, und wird zentral gepflegt.
    Ohne Pruefsummenvergleich waere das ein Download, der spaeter fremden Code
    ausfuehrt - genau der Pfad, den eine Signatur schuetzen soll.
    """
    with urllib.request.urlopen(f"{INSTALLER_BASE}/installer.json", timeout=30) as r:
        manifest = json.load(r)

    entry = next((b for b in manifest.get("binaries", []) if b.get("platform") == rid), None)
    if entry is None:
        sys.exit(f"Kein update-agent fuer {rid} im Installer-Manifest.")

    print(f"  update-agent {manifest.get('version')} fuer {rid} ({entry['sizeBytes']} Bytes)")

    with urllib.request.urlopen(f"{INSTALLER_BASE}/{entry['file']}", timeout=180) as r:
        payload = r.read()

    actual = hashlib.sha256(payload).hexdigest()
    if actual != entry["sha256"].lower():
        sys.exit(f"Pruefsumme weicht ab!\n  erwartet: {entry['sha256']}\n  erhalten: {actual}")

    target = out_dir / ("update-agent.exe" if rid.startswith("win") else "update-agent")
    target.write_bytes(payload)

    if not rid.startswith("win"):
        target.chmod(0o755)

    print(f"  Pruefsumme in Ordnung -> {target.name}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--rid", default="win-x64", help="win-x64, linux-x64, ...")
    parser.add_argument("--channel", default="dev", choices=["dev", "beta", "prod"])
    parser.add_argument("--changelog", default="")
    parser.add_argument("--no-agent", action="store_true",
                        help="update-agent nicht mitliefern")
    parser.add_argument("--dry-run", action="store_true",
                        help="nur bauen und pruefen, nicht hochladen")
    args = parser.parse_args()

    version = read_version()
    print(f"ClawdDotNet {version} | {args.rid} | Kanal {args.channel}")

    out_dir = Path(tempfile.gettempdir()) / f"clawddotnet-publish-{args.rid}"

    print("\n[1/3] dotnet publish")
    publish(args.rid, out_dir)

    print("\n[2/3] update-agent")
    if args.no_agent:
        print("  uebersprungen (--no-agent)")
    else:
        fetch_agent(args.rid, out_dir)

    total = sum(f.stat().st_size for f in out_dir.rglob("*") if f.is_file())
    print(f"\n  {len(list(out_dir.rglob('*')))} Eintraege, {total / 1_048_576:.1f} MB")

    if args.dry_run:
        print(f"\n[3/3] uebersprungen (--dry-run). Ergebnis liegt in {out_dir}")
        return

    if not CONFIG.exists():
        sys.exit(f"{CONFIG} fehlt. Vorlage: deploy/packager.config.example.json")

    print("\n[3/3] pack-and-deploy")
    cmd = [
        str(PACKAGER), "--config", str(CONFIG),
        "--project", "clawddotnet", "--version", version,
        "--channel", args.channel, "--platform", args.rid,
        "--publish-dir", str(out_dir),
    ]
    if args.changelog:
        cmd += ["--changelog", args.changelog]

    run(cmd)


if __name__ == "__main__":
    main()
