"""核心包加密发布脚本。

优先 PyArmor；本机装不上时使用内置 marshal 加密后端（去可读源码）。

用法（仓库根）:
  python scripts/obfuscate_release.py --dry-run
  python scripts/obfuscate_release.py --backend marshal --output dist/obfuscated_lib
  python scripts/obfuscate_release.py --backend marshal --apply

不加密: lib/license/、模板 templates/、非目标包。
"""

from __future__ import annotations

import argparse
import base64
import compileall
import hashlib
import marshal
import py_compile
import random
import shutil
import struct
import subprocess
import sys
import zlib
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]

OBFUSCATE_PACKAGES = [
    "strategy",
    "trade",
    "key_monitor",
    "options",
    "hedge_plan",
]

_LOADER_TEMPLATE = '''# obfuscated module — do not edit
import base64, marshal, zlib
_B = {blob!r}
_K = {key!r}
def _d(b, k):
    raw = zlib.decompress(base64.b64decode(b))
    key = k.encode("utf-8") if isinstance(k, str) else k
    out = bytearray(len(raw))
    for i, c in enumerate(raw):
        out[i] = c ^ key[i % len(key)]
    return bytes(out)
exec(marshal.loads(_d(_B, _K)), globals())
'''


def _xor_compress(data: bytes, key: bytes) -> str:
    out = bytearray(len(data))
    for i, c in enumerate(data):
        out[i] = c ^ key[i % len(key)]
    return base64.b64encode(zlib.compress(bytes(out), 9)).decode("ascii")


def _obfuscate_py_file(src: Path, dest: Path, key: str) -> None:
    source = src.read_text(encoding="utf-8")
    code = compile(source, str(src), "exec", dont_inherit=True)
    blob = _xor_compress(marshal.dumps(code), key.encode("utf-8"))
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.write_text(_LOADER_TEMPLATE.format(blob=blob, key=key), encoding="utf-8")


def _copy_non_py(src_dir: Path, dest_dir: Path) -> None:
    for path in src_dir.rglob("*"):
        if path.is_dir():
            continue
        if path.suffix == ".py":
            continue
        rel = path.relative_to(src_dir)
        target = dest_dir / rel
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(path, target)


def marshal_obfuscate_package(src_pkg: Path, dest_pkg: Path, key: str) -> int:
    if dest_pkg.exists():
        shutil.rmtree(dest_pkg)
    dest_pkg.mkdir(parents=True, exist_ok=True)
    count = 0
    for path in src_pkg.rglob("*.py"):
        rel = path.relative_to(src_pkg)
        # keep __init__.py structure but still obfuscate
        _obfuscate_py_file(path, dest_pkg / rel, key)
        count += 1
    _copy_non_py(src_pkg, dest_pkg)
    return count


def _run(cmd: list[str], cwd: Path) -> None:
    print("+", " ".join(cmd))
    subprocess.check_call(cmd, cwd=str(cwd))


def try_pyarmor(packages: list[str], out: Path) -> bool:
    try:
        import pyarmor  # noqa: F401
    except ImportError:
        return False
    if out.exists():
        shutil.rmtree(out)
    out.mkdir(parents=True, exist_ok=True)
    src_dirs = [str(REPO_ROOT / "lib" / n) for n in packages]
    for cmd in (
        [sys.executable, "-m", "pyarmor.cli", "gen", "-O", str(out), *src_dirs],
        ["pyarmor", "gen", "-O", str(out), *src_dirs],
    ):
        try:
            _run(cmd, REPO_ROOT)
            return True
        except (subprocess.CalledProcessError, FileNotFoundError):
            continue
    return False


def main() -> int:
    parser = argparse.ArgumentParser(description="Obfuscate core lib packages")
    parser.add_argument("--output", default="dist/obfuscated_lib")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--apply", action="store_true", help="覆盖 lib/ 目标包（先备份到 dist/lib_backup_plain）")
    parser.add_argument(
        "--backend",
        choices=("auto", "pyarmor", "marshal"),
        default="auto",
        help="auto=有 pyarmor 用 pyarmor，否则 marshal",
    )
    args = parser.parse_args()

    lib = REPO_ROOT / "lib"
    out = REPO_ROOT / args.output
    packages = [n for n in OBFUSCATE_PACKAGES if (lib / n).is_dir()]
    print("packages:", ", ".join(packages))
    if args.dry_run:
        return 0

    backend = args.backend
    if backend == "auto":
        backend = "pyarmor"
        try:
            import pyarmor  # noqa: F401
        except ImportError:
            backend = "marshal"
            print("pyarmor 未安装，使用 marshal 后端")

    key = hashlib.sha256(b"crypto_monitor_user_release_v1").hexdigest()[:32]

    if backend == "pyarmor":
        ok = try_pyarmor(packages, out)
        if not ok:
            print("pyarmor 失败，回退 marshal", file=sys.stderr)
            backend = "marshal"

    if backend == "marshal":
        if out.exists():
            shutil.rmtree(out)
        out.mkdir(parents=True, exist_ok=True)
        total = 0
        for name in packages:
            n = marshal_obfuscate_package(lib / name, out / name, key)
            print(f"marshal {name}: {n} py files")
            total += n
        print(f"encrypted output: {out} ({total} files)")

    # smoke import from output path
    sys.path.insert(0, str(out.parent))
    # packages live in out/<name>, need lib-like layout for import? 
    # For smoke: add out to path and import strategy if it has __init__
    smoke_root = out
    sys.path.insert(0, str(smoke_root))
    try:
        import importlib

        # packages are top-level under out/
        m = importlib.import_module("trade.trade_policy_lib")
        print("smoke import trade.trade_policy_lib:", getattr(m, "__file__", m))
    except Exception as e:
        print("smoke import note:", e)

    if args.apply:
        for name in packages:
            candidates = [out / name, out / "lib" / name]
            src = next((c for c in candidates if c.exists()), None)
            if src is None:
                print(f"警告: 未找到 {name}")
                continue
            dest = lib / name
            backup = REPO_ROOT / "dist" / "lib_backup_plain" / name
            backup.parent.mkdir(parents=True, exist_ok=True)
            if dest.exists():
                if backup.exists():
                    shutil.rmtree(backup)
                shutil.copytree(dest, backup)
                shutil.rmtree(dest)
            shutil.copytree(src, dest)
            print(f"applied: lib/{name}")
        print("完成。请验证启动后再 push。")
    return 0


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