#!/usr/bin/env python3
"""
Linux-side port of Build-Pack.ps1 + Deploy-Brass.ps1 (publish step).

Reads pack/pack.lock.json + walks pack/overrides/, rebuilds manifest.json,
mirrors every override file to the live web root, and republishes the
manifest. The launcher will then see fresh hashes on its next sync.

Run on glados with passwordless sudo. Idempotent.

Web root: /srv/services/sites/bns (mounted into Caddy as /srv/sites/bns).
"""
import hashlib
import json
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

REPO = Path("/home/matt/dev/brass-and-sigil")
LOCK_PATH = REPO / "pack" / "pack.lock.json"
OVERRIDES = REPO / "pack" / "overrides"
WEB_ROOT = Path("/srv/services/sites/bns/pack")
WEB_FILES = WEB_ROOT / "files"
WEB_MANIFEST = WEB_ROOT / "manifest.json"
SELF_HOST_BASE = "https://bns.sijbers.uk/pack/files"

MANAGED_ROOTS = ["mods", "config", "resourcepacks", "shaderpacks", "kubejs", "defaultconfigs", "patchouli_books"]

# bnstoolkit ships client+server surface (screens, HUD, network bridge),
# so it must NOT inherit the default "server-only" tag that Build-Pack.ps1
# stamps on every pack/overrides/mods/*.jar. brassandsigil_tweaks is
# data-only (worldgen modifiers / recipe overrides) so server-side is
# correct for it.
CLIENT_AND_SERVER_LOCAL_MODS = {"bnstoolkit"}


def sha1_of(p: Path) -> str:
    h = hashlib.sha1()
    with p.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 16), b""):
            h.update(chunk)
    return h.hexdigest()


def main():
    with LOCK_PATH.open() as f:
        lock = json.load(f)

    files = []
    total_bytes = 0

    # 1. Mods from the lockfile (Modrinth-hosted)
    for m in lock["mods"]:
        entry = {
            "path": m["path"],
            "url":  m["url"],
            "sha1": m["sha1"],
            "size": m["size"],
        }
        side = m.get("side")
        if side and side != "both":
            entry["side"] = side
        files.append(entry)
        total_bytes += m["size"]

    # 2. Local overrides
    for root in MANAGED_ROOTS:
        root_dir = OVERRIDES / root
        if not root_dir.exists():
            continue
        for p in sorted(root_dir.rglob("*")):
            if not p.is_file():
                continue
            rel = p.relative_to(OVERRIDES).as_posix()
            sha1 = sha1_of(p)
            size = p.stat().st_size
            entry = {
                "path": rel,
                "url":  f"{SELF_HOST_BASE}/{rel}",
                "sha1": sha1,
                "size": size,
            }
            if root == "mods":
                # See CLIENT_AND_SERVER_LOCAL_MODS — bnstoolkit must be loaded
                # on both sides; the other locally-built mods are server-only.
                stem = p.name.split("-")[0].split("_")[0]  # before first - or _
                if not any(p.name.startswith(prefix) for prefix in CLIENT_AND_SERVER_LOCAL_MODS):
                    entry["side"] = "server"
            files.append(entry)
            total_bytes += size

    # 3. Compose manifest matching the existing schema
    manifest = {
        "name":      lock["name"],
        "version":   lock["version"],
        "minecraft": {"version": lock["minecraft"]},
        "loader":    {"type": lock["loader"]["type"], "version": lock["loader"]["version"]},
        "files":     files,
    }
    for opt in ("defaultServer", "defaultShader", "panelUrl"):
        if opt in lock:
            manifest[opt] = lock[opt]

    # Carry the launcher metadata from the existing published manifest
    # (we can't read a Windows .exe's FileVersion from Linux easily, and
    # the launcher version isn't related to pack content).
    try:
        with WEB_MANIFEST.open() as f:
            existing = json.load(f)
        for k in ("launcherVersion", "launcherUrl"):
            if k in existing:
                manifest[k] = existing[k]
    except Exception:
        pass

    # 4. Mirror override files to web root (uses sudo + cp -a). Only update
    # files whose hash differs from what's already published to keep the
    # rsync footprint tiny.
    print(f"\nMirroring {sum(1 for r in MANAGED_ROOTS if (OVERRIDES / r).exists())} override roots to {WEB_FILES}\n")
    changed = 0
    skipped = 0
    for root in MANAGED_ROOTS:
        root_dir = OVERRIDES / root
        if not root_dir.exists():
            continue
        target_root = WEB_FILES / root
        for p in root_dir.rglob("*"):
            if not p.is_file():
                continue
            rel = p.relative_to(OVERRIDES)
            target = WEB_FILES / rel
            try:
                if target.exists() and sha1_of(target) == sha1_of(p):
                    skipped += 1
                    continue
            except PermissionError:
                # Need sudo to read
                pass
            target.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(p, target)
            changed += 1
    print(f"  changed: {changed}, skipped (matching hash): {skipped}\n")

    # 5. Write the manifest atomically (write to .new, then move)
    new_manifest = WEB_MANIFEST.with_suffix(".json.new")
    bak_manifest = WEB_MANIFEST.with_suffix(".json.bak")
    if WEB_MANIFEST.exists():
        shutil.copy2(WEB_MANIFEST, bak_manifest)
    with new_manifest.open("w") as f:
        json.dump(manifest, f, indent=2, ensure_ascii=False)
        f.write("\n")
    new_manifest.replace(WEB_MANIFEST)

    print(f"Manifest written: {WEB_MANIFEST}")
    print(f"  Version:  {lock['version']}")
    print(f"  Files:    {len(files)}")
    print(f"  Total:    {total_bytes / 1024 / 1024:.1f} MB download")
    print()
    # Quick verify
    rel = "kubejs/server_scripts/economy/00_tier_system.js"
    entry = next((f for f in files if f["path"] == rel), None)
    if entry:
        print(f"  {rel} -> sha1={entry['sha1'][:12]}... size={entry['size']}")


if __name__ == "__main__":
    main()
