#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
generate_manifest.py — generuje manifest.json dla launchera TowerMT2.

Dziala tak samo lokalnie (Windows) jak na VPS (FreeBSD, python3 wg CLAUDE.md §25).

Skanuje <patch_dir>/files/** i zapisuje <patch_dir>/manifest.json:
  { version, launcher_min_version, run_path, config_path, base_url, launcher_url, files[], delete[] }

Hash (sha256) liczony TYLKO gdy (rozmiar LUB mtime) pliku sie zmienil wzgledem
poprzedniej generacji (cache: <patch_dir>/manifest.cache.json). Dzieki temu:
  - regeneracja jest natychmiastowa (nie hashuje niezmienionych plikow),
  - podmiana pliku o IDENTYCZNYM rozmiarze (rekompilacja exe, §26) MA nowy mtime
    -> zostaje przeliczona -> nowy hash -> launcher wykryje update (diff hashy).

Uzycie:
  python3 generate_manifest.py --patch-dir /usr/home/mt2/www/patch \
      --base-url http://89.42.135.104/patch/files/ \
      [--version 1010016] [--bump] [--run-path TowerMT2.exe] \
      [--launcher-min 1] [--launcher-url http://.../patch/Launcher.exe] \
      [--full]   # wymus przeliczenie wszystkich hashy (ignoruj cache)

Wersja: --version ustawia wprost; --bump zwieksza poprzednia o 1; brak obu = zachowaj poprzednia.
KOLEJNOSC WDROZENIA: najpierw wgraj pliki do files/, manifest generuj NA KONCU (HANDOFF 5.2).
"""
import argparse, hashlib, json, os, sys

def sha256(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()

def load_json(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return None

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--patch-dir", required=True, help="katalog z podfolderem files/ (tu powstanie manifest.json)")
    ap.add_argument("--base-url", required=True, help="URL do files/, np. http://89.42.135.99/patch/files/")
    ap.add_argument("--version", type=int, default=None)
    ap.add_argument("--bump", action="store_true", help="wersja = poprzednia + 1")
    ap.add_argument("--run-path", default=None)
    ap.add_argument("--config-path", default=None)
    ap.add_argument("--launcher-min", type=int, default=None)
    ap.add_argument("--launcher-url", default=None)
    ap.add_argument("--delete", nargs="*", default=None, help="sciezki do usuniecia u gracza")
    ap.add_argument("--full", action="store_true", help="przelicz wszystkie hashe (ignoruj cache)")
    args = ap.parse_args()

    patch_dir = os.path.abspath(args.patch_dir)
    files_root = os.path.join(patch_dir, "files")
    if not os.path.isdir(files_root):
        print("BLAD: brak katalogu %s" % files_root, file=sys.stderr); sys.exit(2)

    manifest_path = os.path.join(patch_dir, "manifest.json")
    cache_path = os.path.join(patch_dir, "manifest.cache.json")

    prev = load_json(manifest_path) or {}
    cache = load_json(cache_path) or {}   # path -> {size, mtime, hash}
    if args.full:
        cache = {}

    base_url = args.base_url
    if not base_url.endswith("/"):
        base_url += "/"

    # wersja
    if args.version is not None:
        version = args.version
    elif args.bump:
        version = int(prev.get("version", 0)) + 1
    else:
        version = int(prev.get("version", 1))

    files = []
    new_cache = {}
    reused = 0
    hashed = 0
    for dirpath, _dirs, filenames in os.walk(files_root):
        for fn in filenames:
            ap_full = os.path.join(dirpath, fn)
            rel = os.path.relpath(ap_full, files_root).replace(os.sep, "/")
            stat = os.stat(ap_full)
            size = stat.st_size
            mtime = int(stat.st_mtime)
            c = cache.get(rel)
            if c and c.get("size") == size and c.get("mtime") == mtime and c.get("hash"):
                digest = c["hash"]; reused += 1
            else:
                digest = sha256(ap_full); hashed += 1
            files.append({"path": rel, "size": size, "hash": "sha256:" + digest})
            new_cache[rel] = {"size": size, "mtime": mtime, "hash": digest}

    files.sort(key=lambda x: x["path"])

    manifest = {
        "version": version,
        "launcher_min_version": args.launcher_min if args.launcher_min is not None else int(prev.get("launcher_min_version", 1)),
        "run_path": args.run_path or prev.get("run_path", "TowerMT2.exe"),
        "config_path": args.config_path or prev.get("config_path", "config.exe"),
        "base_url": base_url,
        "launcher_url": args.launcher_url if args.launcher_url is not None else prev.get("launcher_url", ""),
        "files": files,
        "delete": args.delete if args.delete is not None else prev.get("delete", []),
    }

    # zapis atomowy (najpierw .tmp, potem rename) — LF, bez BOM
    def atomic_write(path, data):
        tmp = path + ".tmp"
        with open(tmp, "w", encoding="utf-8", newline="\n") as f:
            f.write(data)
        os.replace(tmp, path)

    atomic_write(manifest_path, json.dumps(manifest, ensure_ascii=False, indent=2))
    atomic_write(cache_path, json.dumps(new_cache, ensure_ascii=False))

    total_bytes = sum(f["size"] for f in files)
    print("OK: %d plikow (%.1f MB), wersja=%d, przeliczono hashy=%d, z cache=%d"
          % (len(files), total_bytes / 1048576.0, version, hashed, reused))
    print("    manifest: %s" % manifest_path)

if __name__ == "__main__":
    main()
