#!/usr/bin/env python3
"""
Verify that every .py source file + config is listed in RELEASE_MANIFEST.txt.
Run before every release build. Fail = fix manifest before shipping.

Usage:
  python3 ops/verify_manifest_completeness.py          # full check
  python3 ops/verify_manifest_completeness.py --fix    # auto-add missing entries
"""

import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent

# Only check these directories for source files (exclude runtime data, logs, node_modules)
SOURCE_DIRS = ["src", "bridge", "config", "contracts", "signal_generators",
               "strategy_registry", "strategy_sdk", "strategy_runtime",
               "license_server", "gui_react", "assets", "ops", "runbooks",
               "deploy", "tenants", "website"]

ROOT_NON_SOURCE = [
    "start_trial.py", "config_manager.py", "updater.py",
    "gui_backend.py", "skill_trading_memory.py",
    "setup_server.py", "deploy.py",
    "clean_ledger.py", "analyze_daily_pnl.py", "debug_history.py",
    "test_bars.py",
    "health_check.sh", "keep_alive.sh", "watchdog.sh",
    "run_goldtrader.sh", "start_gui.sh", "start_gui.command",
    "启动.bat", "README.txt",
    "requirements.txt", "VERSION", "README.md",
    "app_config.json", "entitlements.plist",
]



def load_manifest_entries() -> set[str]:
    manifest = ROOT / "RELEASE_MANIFEST.txt"
    entries: set[str] = set()
    for line in manifest.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        entries.add(line)
    return entries


def find_source_files() -> set[str]:
    """Python and critical config files from SOURCE_DIRS + root."""
    src_files: set[str] = set()

    for d in SOURCE_DIRS:
        p = ROOT / d
        if not p.exists():
            continue
        for f in p.rglob("*.py"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.json"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.mq5"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.mqh"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.dll"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.html"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.ts"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.tsx"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.css"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.js"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.sh"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.service"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)
        for f in p.rglob("*.bat"):
            rel = str(f.relative_to(ROOT))
            src_files.add(rel)

    for fname in ROOT_NON_SOURCE:
        src_files.add(fname)

    # Filter out node_modules, dist, and __pycache__
    src_files = {f for f in src_files if "node_modules" not in f and "__pycache__" not in f
                 and "gui_react/dist" not in f}

    return src_files


def main():
    if len(sys.argv) > 1 and sys.argv[1] == "--fix":
        return do_fix()

    manifest = load_manifest_entries()
    sources = find_source_files()

    missing = sources - manifest
    extra = {m for m in manifest if m.endswith(".py") or m.endswith(".json")} - sources

    if missing:
        print("\n\x1b[31m[MANIFEST INCOMPLETE]\x1b[0m These files are NOT in RELEASE_MANIFEST.txt:")
        for f in sorted(missing):
            print(f"  {f}")
        print(f"\n\x1b[33mAction: Run 'python3 ops/verify_manifest_completeness.py --fix' to auto-add them,\x1b[0m")
        print(f"\x1b[33mor manually add {len(missing)} missing file(s) to RELEASE_MANIFEST.txt.\x1b[0m")
        sys.exit(1)

    if extra:
        print(f"\x1b[33m[WARN] {len(extra)} manifest entries are stale (file not found):\x1b[0m")
        for f in sorted(extra):
            print(f"  {f}")

    print(f"\x1b[32m[OK] Manifest complete. {len(sources)} source files tracked, {len(manifest)} in manifest.\x1b[0m")


def do_fix():
    manifest = load_manifest_entries()
    sources = find_source_files()
    missing = sources - manifest

    if not missing:
        print("[OK] Manifest already complete. Nothing to add.")
        return

    manifest_path = ROOT / "RELEASE_MANIFEST.txt"
    content = manifest_path.read_text()

    # Add missing files under appropriate sections
    # Group by directory
    grouped: dict[str, list[str]] = {}
    for f in sorted(missing):
        parts = f.split("/")
        if len(parts) == 1:
            grouped.setdefault("# ── Root-level files", []).append(f)
        else:
            section = f"# ── {parts[0]}"
            grouped.setdefault(section, []).append(f)

    new_entries: list[str] = []
    for section, files in grouped.items():
        new_entries.append(f"\n{section}")
        for f in files:
            new_entries.append(f)

    manifest_path.write_text(content.rstrip() + "\n" + "\n".join(new_entries) + "\n")
    print(f"\x1b[32m[FIXED] Added {len(missing)} missing entries to RELEASE_MANIFEST.txt.\x1b[0m")
    for section, files in grouped.items():
        print(f"  {section}: {len(files)} files")


if __name__ == "__main__":
    main()
