#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
发版与版本号：推荐顺序见 scripts/package/release.bat — 先 PyInstaller 打包，再更新版本号并同步到 dist，再 Inno Setup，最后 --post-build。
单独用法:
  仅改版本号（通常在 build.bat 成功之后）:  python scripts/package/release.py 1.0.4 [--notes "说明"]
  打包完成后:          python scripts/package/release.py --post-build [--old-exe 上一版 ServerManager.exe]
  本地测试包:          scripts/package/pack_local_test.bat 或 python scripts/package/pack_local_test.py（见 RELEASE.md）
"""

import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
import zipfile
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
VERSION_JSON = ROOT / "resources" / "version.json"
ROOT_VERSION_JSON = ROOT / "version.json"
OUTPUT_DIR = ROOT / "build" / "output"
PUBLIC_MANIFEST_URL = "https://www.sumi.work/api/software/server-manager/version.json"
PUBLIC_INSTALLER_URL = "https://www.sumi.work/api/software/server-manager/update-download"
PUBLIC_DELTA_URL = "https://www.sumi.work/api/software/server-manager/delta-download?from={from_version}"
APP_DIST_NAME = "ServerManager"
APP_EXE_NAME = "ServerManager.exe"
FILE_DELTA_FORMAT = "server-manager-file-delta-v1"


def read_text(p: Path) -> str:
    return p.read_text(encoding="utf-8")


def write_text(p: Path, s: str) -> None:
    p.write_text(s, encoding="utf-8")


def read_version_from_json(p: Path) -> str:
    if not p.exists():
        return ""
    try:
        data = json.loads(read_text(p))
    except Exception:
        return ""
    return (data.get("version") or "").strip()


def get_current_version() -> str:
    """读取当前已发布版本，以 build/output/version.json 为准。"""
    return read_version_from_json(OUTPUT_DIR / "version.json")


def format_size(bytes_count: int) -> str:
    return f"{bytes_count / 1024 / 1024:.1f} MB"


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest().lower()


def rel_path(path: Path, root: Path) -> str:
    return path.relative_to(root).as_posix()


def collect_dist_files(root: Path) -> dict:
    files = {}
    if not root.is_dir():
        return files
    for p in sorted(root.rglob("*")):
        if not p.is_file():
            continue
        rel = rel_path(p, root)
        if rel.startswith(".update_backup/") or rel.startswith("logs/") or rel.endswith(".old"):
            continue
        stat = p.stat()
        files[rel] = {
            "path": p,
            "sha256": sha256_file(p),
            "size": stat.st_size,
        }
    return files


def generate_file_delta(old_dist_path: Path, old_version: str, new_version: str) -> tuple[Path, dict]:
    old_root = Path(old_dist_path).resolve()
    new_root = ROOT / "src" / "dist" / APP_DIST_NAME
    if not old_root.is_dir():
        raise FileNotFoundError(f"old dist not found: {old_root}")
    if not new_root.is_dir():
        raise FileNotFoundError(f"new dist not found: {new_root}")

    old_files = collect_dist_files(old_root)
    new_files = collect_dist_files(new_root)
    changed = []
    deleted = []

    for rel, meta in sorted(new_files.items()):
        old_meta = old_files.get(rel)
        if not old_meta or old_meta["sha256"] != meta["sha256"]:
            changed.append({
                "path": rel,
                "zip_path": f"files/{rel}",
                "sha256": meta["sha256"],
                "size": meta["size"],
                "old_sha256": old_meta["sha256"] if old_meta else "",
                "old_size": old_meta["size"] if old_meta else 0,
            })

    for rel, meta in sorted(old_files.items()):
        if rel not in new_files:
            deleted.append({
                "path": rel,
                "old_sha256": meta["sha256"],
                "old_size": meta["size"],
            })

    patch_dir = OUTPUT_DIR / "patches"
    patch_dir.mkdir(parents=True, exist_ok=True)
    delta_file = patch_dir / f"v{old_version}_to_v{new_version}.smdelta.zip"
    manifest = {
        "format": FILE_DELTA_FORMAT,
        "base_version": old_version,
        "target_version": new_version,
        "app": APP_DIST_NAME,
        "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
        "files": changed,
        "delete": deleted,
    }

    with zipfile.ZipFile(delta_file, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
        zf.writestr("delta_manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
        for entry in changed:
            zf.write(new_files[entry["path"]]["path"], entry["zip_path"])

    size = delta_file.stat().st_size
    return delta_file, {
        "sha256": sha256_file(delta_file),
        "sizeBytes": size,
        "size": format_size(size),
        "fileCount": len(changed),
        "deleteCount": len(deleted),
    }


def save_dist_snapshot(version: str) -> None:
    src = ROOT / "src" / "dist" / APP_DIST_NAME
    if not src.is_dir():
        return
    dst = OUTPUT_DIR / "snapshots" / f"{APP_DIST_NAME}-{version}"
    dst.parent.mkdir(parents=True, exist_ok=True)
    if dst.exists():
        shutil.rmtree(dst)
    shutil.copytree(src, dst)
    print(f"  [OK] 已保存 dist 快照 -> {dst.relative_to(ROOT)}")


def ensure_compat_manifest(data: dict) -> dict:
    """补齐客户端更新字段，并固定到 build/output 发布路径。"""
    data["update_url"] = PUBLIC_MANIFEST_URL
    data["full_installer_url"] = PUBLIC_INSTALLER_URL
    data["download_url"] = PUBLIC_INSTALLER_URL
    return data


def write_version_json(data: dict) -> None:
    data = ensure_compat_manifest(data)
    text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
    write_text(VERSION_JSON, text)
    write_text(ROOT_VERSION_JSON, text)


def set_version_pre(new_version: str, release_notes: str = None, with_delta: bool = False, delta_kind: str = "file-zip") -> str:
    """更新版本号并同步 manifest。默认全量更新；显式 --with-delta 才生成 delta 项。"""
    vj = VERSION_JSON
    if not vj.exists():
        print("错误: 未找到 version.json", file=sys.stderr)
        sys.exit(1)
    data = json.loads(read_text(vj))
    old_version = read_version_from_json(OUTPUT_DIR / "version.json")
    if with_delta and not old_version:
        print("错误: 未找到 build/output/version.json 或其中无 version 字段", file=sys.stderr)
        sys.exit(1)
    if old_version:
        print(f"  [i] 旧版本以 build/output/version.json 为准: {old_version}")

    # build_installer.iss
    iss = ROOT / "scripts" / "package" / "build_installer.iss"
    if iss.exists():
        t = read_text(iss)
        t = re.sub(r'(#define MyAppVersion\s+")[^"]+(")', r'\g<1>' + new_version + r'\g<2>', t)
        write_text(iss, t)
        print(f"  [OK] build_installer.iss -> {new_version}")

    # Tk demo/updater examples
    for rel in ("src/entrypoints/tk_update_demo.py", "scripts/update/tk_updater.py"):
        main_py = ROOT / rel
        if not main_py.exists():
            continue
        t = read_text(main_py)
        t = re.sub(r'(LOCAL_VERSION\s*=\s*")[^"]+(")', r'\g<1>' + new_version + r'\g<2>', t)
        write_text(main_py, t)
        print(f"  [OK] {rel} -> LOCAL_VERSION {new_version}")

    # version.json
    data["version"] = new_version
    if release_notes is not None:
        data["release_notes"] = release_notes
    else:
        data["release_notes"] = data.get("release_notes", "") or f"Server Manager {new_version} 更新。"

    data = ensure_compat_manifest(data)
    if with_delta:
        # delta_updates: 只保留 上一版本 -> 当前版本 一项。默认使用文件级增量 zip。
        if delta_kind == "exe-patch":
            patch_url = PUBLIC_DELTA_URL.format(from_version=old_version)
            delta_entry = {
                "type": "bsdiff-exe",
                "patch_url": patch_url,
                "new_exe_sha256": "",
            }
        else:
            delta_url = PUBLIC_DELTA_URL.format(from_version=old_version)
            delta_entry = {
                "type": "file-zip",
                "format": FILE_DELTA_FORMAT,
                "fromVersion": old_version,
                "from_version": old_version,
                "targetVersion": new_version,
                "target_version": new_version,
                "package_url": delta_url,
                "packageUrl": delta_url,
                "patch_url": delta_url,
                "patchUrl": delta_url,
                "sha256": "",
                "sizeBytes": 0,
                "size": "",
            }
        data["delta_updates"] = {old_version: delta_entry}
        data["deltaUpdates"] = data["delta_updates"]
        print(f"  [OK] version.json -> version {new_version}, delta_updates[{old_version}] 已添加")
    else:
        data["delta_updates"] = {}
        data["deltaUpdates"] = {}
        data.pop("delta", None)
        print(f"  [OK] version.json -> version {new_version}，本次使用全量更新")

    write_version_json(data)
    print("  [OK] 已同步 version.json -> resources/version.json 与根目录 version.json")

    # 若已打过 PyInstaller，把新版本同步进 dist，避免安装包内仍是旧 version.json
    for rel in (
        f"src/dist/{APP_DIST_NAME}/_internal/version.json",
        f"src/dist/{APP_DIST_NAME}/version.json",
    ):
        dest = ROOT / rel
        if dest.parent.is_dir():
            shutil.copy2(vj, dest)
            print(f"  [OK] 已同步 version.json -> {dest.relative_to(ROOT)}")
    return old_version


def set_version_post(old_exe_path: str = None, old_dist_path: str = None) -> None:
    """打包完成后：生成文件级增量 zip 或兼容旧 exe patch，并复制 version.json。"""
    vj = VERSION_JSON
    if not vj.exists():
        return
    data = json.loads(read_text(vj))
    data = ensure_compat_manifest(data)
    new_version = (data.get("version") or "").strip()
    delta_updates = data.get("delta_updates") or {}
    if not isinstance(delta_updates, dict):
        return
    # 找到需要补齐元数据的那一项（上一版本）
    old_version = None
    for k, v in delta_updates.items():
        if not isinstance(v, dict):
            continue
        kind = str(v.get("type") or "").strip().lower().replace("_", "-")
        if kind == "bsdiff-exe":
            if not (v.get("new_exe_sha256") or "").strip():
                old_version = k.strip()
                break
        elif not (v.get("sha256") or v.get("package_sha256") or v.get("packageSha256") or "").strip():
            old_version = k.strip()
            break
    if not old_version:
        print("  [i] 无需生成增量补丁（new_exe_sha256 已填或无 delta）")
        write_version_json(data)
        save_dist_snapshot(new_version)
        _copy_version_to_output()
        return
    delta_entry = delta_updates.get(old_version) or {}
    delta_kind = str(delta_entry.get("type") or "").strip().lower().replace("_", "-")

    if delta_kind != "bsdiff-exe":
        if not old_dist_path:
            print("  [i] 未提供上一版 dist 目录（使用 --old-dist 指定可生成文件级增量包），本次仅全量")
            data["delta_updates"] = {}
            data.pop("deltaUpdates", None)
            write_version_json(data)
            save_dist_snapshot(new_version)
            _copy_version_to_output()
            return
        try:
            delta_file, delta_meta = generate_file_delta(Path(old_dist_path), old_version, new_version)
        except Exception as e:
            print(f"  [!] 生成文件级增量包失败: {e}，本次仅全量", file=sys.stderr)
            data["delta_updates"] = {}
            data.pop("deltaUpdates", None)
            write_version_json(data)
            save_dist_snapshot(new_version)
            _copy_version_to_output()
            return
        delta_entry.update({
            "type": "file-zip",
            "format": FILE_DELTA_FORMAT,
            "fromVersion": old_version,
            "from_version": old_version,
            "targetVersion": new_version,
            "target_version": new_version,
            "sha256": delta_meta["sha256"],
            "package_sha256": delta_meta["sha256"],
            "packageSha256": delta_meta["sha256"],
            "sizeBytes": delta_meta["sizeBytes"],
            "size": delta_meta["size"],
            "fileCount": delta_meta["fileCount"],
            "deleteCount": delta_meta["deleteCount"],
        })
        delta_updates[old_version] = delta_entry
        data["delta_updates"] = delta_updates
        data["deltaUpdates"] = delta_updates
        write_version_json(data)
        print(
            f"  [OK] 已生成文件级增量包: {delta_file} "
            f"({delta_meta['size']}, {delta_meta['fileCount']} changed, {delta_meta['deleteCount']} deleted)"
        )
        save_dist_snapshot(new_version)
        _copy_version_to_output()
        print("  发版准备完成。")
        return

    # 支持目录模式 (dist/ServerManager/ServerManager.exe) 与单文件 (dist/ServerManager.exe)
    new_exe = ROOT / "src" / "dist" / APP_DIST_NAME / APP_EXE_NAME
    if not new_exe.exists():
        new_exe = ROOT / "src" / "dist" / APP_EXE_NAME
    if not new_exe.exists():
        print(f"  [i] 未找到 src/dist/{APP_EXE_NAME} 或 src/dist/{APP_DIST_NAME}/{APP_EXE_NAME}，跳过增量包")
        write_version_json(data)
        save_dist_snapshot(new_version)
        _copy_version_to_output()
        return

    # 仅当显式传入上一版 exe 路径时生成 patch（与 Cursor 一致，本地不保存 exe）
    old_exe = Path(old_exe_path).resolve() if old_exe_path else None
    if not old_exe_path or not old_exe or not old_exe.is_file():
        print(f"  [i] 未提供上一版 exe（使用 --old-exe 指定路径可生成增量包），仅全量")
        write_version_json(data)
        save_dist_snapshot(new_version)
        _copy_version_to_output()
        return

    try:
        import bsdiff4
    except ImportError:
        print("  [!] 未安装 bsdiff4，跳过增量包。可执行: pip install bsdiff4", file=sys.stderr)
        save_dist_snapshot(new_version)
        _copy_version_to_output()
        return

    patch_dir = OUTPUT_DIR / "patches"
    patch_dir.mkdir(parents=True, exist_ok=True)
    patch_file = patch_dir / f"v{old_version}_to_v{new_version}.patch"

    old_data = old_exe.read_bytes()
    new_data = new_exe.read_bytes()
    patch_data = bsdiff4.diff(old_data, new_data)
    patch_file.write_bytes(patch_data)
    print(f"  [OK] 已生成: {patch_file}")

    new_sha = hashlib.sha256(new_data).hexdigest().lower()
    delta_updates[old_version]["new_exe_sha256"] = new_sha
    data["delta_updates"] = delta_updates
    data["deltaUpdates"] = delta_updates
    write_version_json(data)
    print(f"  [OK] version.json -> delta_updates[\"{old_version}\"].new_exe_sha256 = {new_sha[:16]}...")

    save_dist_snapshot(new_version)
    _copy_version_to_output()
    print("  发版准备完成。")


def _copy_version_to_output() -> None:
    src = VERSION_JSON
    dst = OUTPUT_DIR / "version.json"
    if src.exists() and dst.parent.exists():
        shutil.copy2(src, dst)
        print("  [OK] 已复制 version.json -> build/output/")
    if src.exists():
        shutil.copy2(src, ROOT_VERSION_JSON)
        print("  [OK] 已同步 version.json -> 根目录")


def clean_generated_artifacts() -> None:
    cleanup_script = ROOT / "scripts" / "package" / "clean_artifacts.py"
    if cleanup_script.exists():
        subprocess.run([sys.executable, str(cleanup_script)], cwd=str(ROOT), check=False)


def main():
    parser = argparse.ArgumentParser(description="一键发版：更新版本号 / 打包后生成增量并填 SHA256")
    parser.add_argument("version", nargs="?", help="新版本号，如 1.0.4")
    parser.add_argument("--post-build", action="store_true", help="打包完成后执行：复制 version.json；若提供 --old-dist 则生成文件级增量包")
    parser.add_argument("--old-dist", default=None, help="上一版 dist/ServerManager 目录，用于生成文件级增量 zip")
    parser.add_argument("--old-exe", default=None, help="上一版 ServerManager.exe 路径，用于兼容旧 bsdiff exe patch")
    parser.add_argument("--with-delta", action="store_true", help="生成增量更新项；默认文件级增量 zip")
    parser.add_argument("--delta-kind", choices=("file-zip", "exe-patch"), default="file-zip", help="增量类型，默认 file-zip")
    parser.add_argument("--notes", "-n", default=None, help="可选：release_notes 更新说明")
    args = parser.parse_args()

    if args.post_build:
        set_version_post(old_exe_path=args.old_exe, old_dist_path=args.old_dist)
        clean_generated_artifacts()
        return

    if not args.version or not re.match(r"^\d+\.\d+\.\d+$", args.version.strip()):
        print("用法: python scripts/package/release.py <版本号>  例如: python scripts/package/release.py 1.0.4", file=sys.stderr)
        sys.exit(1)
    new_ver = args.version.strip()
    old_ver = set_version_pre(new_ver, args.notes, with_delta=args.with_delta, delta_kind=args.delta_kind)
    if args.with_delta:
        print(f"\n  当前版本 {old_ver} -> 新版本 {new_ver}。若使用 scripts/package/release.bat，接下来将编译安装包并执行 --post-build。")
    else:
        print(f"\n  新版本 {new_ver} 将通过 build/output 下的完整安装包更新客户端。")


if __name__ == "__main__":
    main()
