#!/usr/bin/env python3
"""Assemble self-contained BestPerf release packages (Windows .zip + Linux .tar.gz + macOS .tar.gz).

A release root is exactly what `bestperf_install.py` copies into the user's install dir, plus
the user-facing entry scripts. Layout:

  <root>/
    Install BestPerf.bat | .sh     # double-click to install (-> bestperf_install.py)
    BestPerf.bat | .sh             # double-click to launch  (-> bestperf_bootstrap.py)
    Fix UDP.bat                       # (Windows) firewall helper for inbound UDP
    bin/                              # compiled bperf-* (.exe on Windows)
    runtime/python/                   # (Windows) bundled embeddable Python; Linux uses system python3
    scripts/ gui/ examples/ schemas/  # product files
    README.md

Run on a Mac/dev box. Pulls Windows .exe from build-win/, Linux ELF + Windows embeddable Python
from a staging dir (default /tmp/fr_stage: linux-bin/ and pyembed/).
"""
import argparse
import json
import os
import pathlib
import shutil
import subprocess
import sys
import tarfile
import zipfile

ROOT = pathlib.Path(__file__).resolve().parents[1]

COMMON_DIRS = ["scripts", "gui", "examples", "schemas"]
COMMON_FILES = ["README.md"]
WIN_FILES = ["Install BestPerf.bat", "BestPerf.bat", "BestPerf Fix UDP.bat"]
LINUX_FILES = ["Install BestPerf.sh", "BestPerf.sh"]
MAC_FILES = ["Install BestPerf.command", "BestPerf.command"]
PRUNE = {"__pycache__", ".pytest_cache", ".DS_Store", "node_modules", ".git"}


def _ignore(_dir, names):
    return [n for n in names if n in PRUNE or n.endswith((".pyc", ".pyo"))]


def _git_sha() -> str:
    try:
        r = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"],
                           capture_output=True, text=True, timeout=5)
        return r.stdout.strip() or "unknown"
    except Exception:  # noqa: BLE001
        return "unknown"


def write_manifest(root: pathlib.Path, platform_label: str, system: str, machine: str,
                   version: str) -> dict:
    """Emit release_manifest.json. bestperf_bootstrap/controller read it for version display and
    the OS/arch safety check; without it every package reported 'no release metadata' and the
    platform-match check was dead. `machine` empty -> bootstrap treats arch as always-matching."""
    bin_dir = root / "bin"
    binaries = sorted(p.name for p in bin_dir.glob("*")) if bin_dir.is_dir() else []
    manifest = {
        "schema": "field_replay.release_manifest.v1",
        "name": "BestPerf",
        "version": version,
        "package_id": f"BestPerf-{platform_label}-{version}",
        "artifact_name": root.name,
        "platform_label": platform_label,
        "platform": {"system": system, "machine": machine},
        "bundled_binaries": binaries,
        "git_sha": _git_sha(),
    }
    (root / "release_manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
    return manifest


def copy_common(dst: pathlib.Path):
    dst.mkdir(parents=True, exist_ok=True)
    for d in COMMON_DIRS:
        src = ROOT / d
        if src.is_dir():
            shutil.copytree(src, dst / d, ignore=_ignore, dirs_exist_ok=True)
    for f in COMMON_FILES:
        if (ROOT / f).is_file():
            shutil.copy2(ROOT / f, dst / f)


def stage_windows(stage: pathlib.Path, winexe: pathlib.Path, pyembed: pathlib.Path,
                  version: str = "0.1.0") -> pathlib.Path:
    root = stage / "BestPerf-windows-x64"
    if root.exists():
        shutil.rmtree(root)
    copy_common(root)
    # binaries
    binroot = root / "bin"; binroot.mkdir(parents=True, exist_ok=True)
    n = 0
    for exe in sorted(winexe.glob("bperf-*.exe")):
        shutil.copy2(exe, binroot / exe.name); n += 1
    if n == 0:
        raise SystemExit(f"no bperf-*.exe in {winexe}")
    # bundled python -> runtime/python
    rt = root / "runtime" / "python"; rt.mkdir(parents=True, exist_ok=True)
    shutil.copytree(pyembed, rt, ignore=_ignore, dirs_exist_ok=True)
    if not (rt / "python.exe").is_file():
        raise SystemExit(f"runtime/python/python.exe missing (from {pyembed})")
    for f in WIN_FILES:
        if (ROOT / f).is_file():
            shutil.copy2(ROOT / f, root / f)
        else:
            print(f"  WARN: missing {f}")
    # Friendly "先看我 START HERE" guide at the TOP of the package (highest-visibility for novices:
    # extract-first, double-click Install, SmartScreen "Run anyway", launch).
    win_start = ROOT / "packaging" / "windows"
    if win_start.is_dir():
        for p in sorted(win_start.iterdir()):
            if not p.is_file():
                continue
            dst = root / p.name
            if p.suffix.lower() == ".txt":
                # Legacy Notepad / PowerShell Get-Content read a no-BOM UTF-8 file as the system
                # ANSI codepage (GBK on a Chinese Windows) -> the Chinese turns to mojibake. Ship
                # the guide .txt as UTF-8 *with BOM* + CRLF so it always renders correctly.
                data = p.read_bytes().replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
                if not data.startswith(b"\xef\xbb\xbf"):
                    data = b"\xef\xbb\xbf" + data
                dst.write_bytes(data)
            else:
                shutil.copy2(p, dst)
    # Windows cmd.exe REQUIRES CRLF line endings; an LF .bat is mis-parsed (goto/if/redirects break,
    # install silently falls into the python-not-found branch). Normalize every shipped .bat/.cmd.
    # NOTE: never add a BOM to a .bat/.cmd -- cmd.exe chokes on a leading BOM.
    nbat = 0
    for p in root.rglob("*"):
        if p.suffix.lower() in (".bat", ".cmd") and p.is_file():
            data = p.read_bytes().replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
            p.write_bytes(data); nbat += 1
    write_manifest(root, "windows-x64", "Windows", "AMD64", version)
    print(f"  windows: {n} exes, runtime/python ok, {nbat} .bat/.cmd normalized to CRLF, manifest written")
    return root


def stage_linux(stage: pathlib.Path, linbin: pathlib.Path, version: str = "0.1.0") -> pathlib.Path:
    root = stage / "BestPerf-linux-x64"
    if root.exists():
        shutil.rmtree(root)
    copy_common(root)
    binroot = root / "bin"; binroot.mkdir(parents=True, exist_ok=True)
    n = 0
    for b in sorted(linbin.glob("bperf-*")):
        dst = binroot / b.name
        shutil.copy2(b, dst); os.chmod(dst, 0o755); n += 1
    if n == 0:
        raise SystemExit(f"no bperf-* in {linbin}")
    for f in LINUX_FILES:
        if (ROOT / f).is_file():
            dst = root / f
            shutil.copy2(ROOT / f, dst); os.chmod(dst, 0o755)
    # make the launcher helper executable too
    h = root / "scripts" / "bestperf_launcher_common.sh"
    if h.is_file():
        os.chmod(h, 0o755)
    # Friendly "先看我 START HERE" guide at the top of the package.
    lin_start = ROOT / "packaging" / "linux"
    if lin_start.is_dir():
        for p in sorted(lin_start.iterdir()):
            if p.is_file():
                shutil.copy2(p, root / p.name)
    write_manifest(root, "linux-x64", "Linux", "x86_64", version)
    print(f"  linux: {n} binaries (chmod 755), entry .sh executable, START HERE guide, manifest written")
    return root


def stage_macos(stage: pathlib.Path, macbin: pathlib.Path, version: str = "0.1.0") -> pathlib.Path:
    """Assemble the macOS release root. macbin holds native Mach-O bperf-* — build them
    universal2 (`-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"`) so one package runs on both Apple
    Silicon and Intel. Uses the system python3 (no bundled runtime, same as Linux); the
    `.command` launchers fall back to a friendly 'install Python' message if it's absent."""
    root = stage / "BestPerf-macos-universal"
    if root.exists():
        shutil.rmtree(root)
    copy_common(root)
    binroot = root / "bin"; binroot.mkdir(parents=True, exist_ok=True)
    n = 0
    for b in sorted(macbin.glob("bperf-*")):
        dst = binroot / b.name
        shutil.copy2(b, dst); os.chmod(dst, 0o755); n += 1
    if n == 0:
        raise SystemExit(f"no bperf-* in {macbin}")
    for f in MAC_FILES:
        if (ROOT / f).is_file():
            dst = root / f
            shutil.copy2(ROOT / f, dst); os.chmod(dst, 0o755)
        else:
            print(f"  WARN: missing {f}")
    # make the launcher helper executable too
    h = root / "scripts" / "bestperf_launcher_common.sh"
    if h.is_file():
        os.chmod(h, 0o755)
    # Friendly "先看我 START HERE" guide at the top of the package.
    mac_start = ROOT / "packaging" / "macos"
    if mac_start.is_dir():
        for p in sorted(mac_start.iterdir()):
            if p.is_file():
                shutil.copy2(p, root / p.name)
    # machine="" -> bootstrap treats arch as always-matching, so this ONE universal2 package
    # passes the OS/arch safety check on both arm64 and x86_64 Macs.
    write_manifest(root, "macos-universal", "Darwin", "", version)
    print(f"  macos: {n} binaries (chmod 755), .command entries executable, START HERE guide, manifest written")
    return root


def zip_dir(root: pathlib.Path, out: pathlib.Path):
    if out.exists():
        out.unlink()
    with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
        for p in sorted(root.rglob("*")):
            z.write(p, p.relative_to(root.parent))
    print(f"  -> {out}  ({out.stat().st_size // 1048576} MB)")


def targz_dir(root: pathlib.Path, out: pathlib.Path):
    if out.exists():
        out.unlink()
    with tarfile.open(out, "w:gz") as t:
        t.add(root, arcname=root.name)  # preserves mode bits (exec)
    print(f"  -> {out}  ({out.stat().st_size // 1048576} MB)")


def main() -> int:
    ap = argparse.ArgumentParser(description="Assemble BestPerf release packages")
    ap.add_argument("--out", default="/tmp/fr_release", help="output dir for staged roots + archives")
    ap.add_argument("--winexe", default=str(ROOT / "build-win"), help="dir with cross-compiled bperf-*.exe")
    ap.add_argument("--linbin", default="/tmp/fr_stage/linux-bin", help="dir with Linux bperf-* ELF")
    ap.add_argument("--macbin", default="/tmp/fr_stage/macos-bin", help="dir with macOS universal2 bperf-* Mach-O")
    ap.add_argument("--pyembed", default="/tmp/fr_stage/pyembed", help="Windows embeddable Python dir")
    ap.add_argument("--only", choices=["windows", "linux", "macos"], help="build only one platform")
    ap.add_argument("--version", default="0.1.0", help="release version stamped into release_manifest.json")
    args = ap.parse_args()

    stage = pathlib.Path(args.out); stage.mkdir(parents=True, exist_ok=True)
    print("assembling release roots ...")
    if args.only in (None, "windows"):
        wroot = stage_windows(stage, pathlib.Path(args.winexe), pathlib.Path(args.pyembed), args.version)
        zip_dir(wroot, stage / "BestPerf-windows-x64.zip")
    if args.only in (None, "linux"):
        lroot = stage_linux(stage, pathlib.Path(args.linbin), args.version)
        targz_dir(lroot, stage / "BestPerf-linux-x64.tar.gz")
    if args.only in (None, "macos"):
        mroot = stage_macos(stage, pathlib.Path(args.macbin), args.version)
        targz_dir(mroot, stage / "BestPerf-macos-universal.tar.gz")
    print("done.")
    return 0


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