"""Create a clean NexaOffice source release zip with a stable root folder.

Run from the project root:
    python tools/create_release_zip.py

The output zip always contains exactly one top-level folder:
    nexaoffice_worksuite/

This keeps overwrite installs simple and prevents accidental flat zips.
"""
from __future__ import annotations

import argparse
import json
import os
import re
import zipfile
from datetime import datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
ROOT_FOLDER_NAME = "nexaoffice_worksuite"
REPORT_PATH = ROOT / "data" / "package_structure_latest.json"

EXCLUDE_DIRS = {
    "__pycache__",
    ".git",
    ".idea",
    ".vscode",
    "build",
    "dist",
    "build_backups",
    "build_logs",
    ".pytest_cache",
}
EXCLUDE_SUFFIXES = {".pyc", ".pyo", ".log", ".tmp", ".bak"}
EXCLUDE_FILES = {
    "portable.flag",
}

# v2026.06.20.52: keep archive/source packages smaller by not shipping
# generated diagnostic snapshots. They are recreated by run_self_check.bat or
# build_exe.bat when needed. server_schema_latest.json is kept because the
# package cleanliness guard uses it as the current schema baseline.
KEEP_LATEST_REPORTS = {"server_schema_latest.json"}


def _version() -> str:
    text = (ROOT / "core" / "constants.py").read_text(encoding="utf-8", errors="ignore")
    match = re.search(r'APP_VERSION\s*=\s*"([^"]+)"', text)
    return match.group(1) if match else "unknown"


def should_include(path: Path) -> bool:
    rel = path.relative_to(ROOT)
    if any(part in EXCLUDE_DIRS for part in rel.parts):
        return False
    if path.name in EXCLUDE_FILES:
        return False
    if rel.parts and rel.parts[0] == "data" and path.name.endswith("_latest.json") and path.name not in KEEP_LATEST_REPORTS:
        return False
    if path.suffix.lower() in EXCLUDE_SUFFIXES:
        return False
    return path.is_file()


def iter_release_files() -> list[Path]:
    files = [p for p in ROOT.rglob("*") if should_include(p)]
    return sorted(files, key=lambda p: p.relative_to(ROOT).as_posix().lower())


def build_zip(output: Path) -> dict:
    output.parent.mkdir(parents=True, exist_ok=True)
    files = iter_release_files()
    with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.writestr(f"{ROOT_FOLDER_NAME}/", "")
        for path in files:
            rel = path.relative_to(ROOT).as_posix()
            zf.write(path, f"{ROOT_FOLDER_NAME}/{rel}")

    return validate_zip(output)


def validate_zip(output: Path) -> dict:
    issues: list[str] = []
    with zipfile.ZipFile(output) as zf:
        names = [n for n in zf.namelist() if n]
    top_levels = sorted({n.split("/")[0] for n in names})
    if top_levels != [ROOT_FOLDER_NAME]:
        issues.append("zip must contain exactly one top-level folder named nexaoffice_worksuite")
    if not any(n == f"{ROOT_FOLDER_NAME}/app.py" for n in names):
        issues.append("zip missing nexaoffice_worksuite/app.py")
    if not any(n == f"{ROOT_FOLDER_NAME}/main.py" for n in names):
        issues.append("zip missing nexaoffice_worksuite/main.py")
    if any("__pycache__/" in n or n.endswith(".pyc") for n in names):
        issues.append("zip contains Python cache files")
    if any(n.split("/")[1:2] in (["build"], ["dist"]) for n in names if "/" in n):
        issues.append("zip contains build/dist artifacts")

    report = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "version": _version(),
        "output": str(output),
        "root_folder": ROOT_FOLDER_NAME,
        "file_count": len(names),
        "top_levels": top_levels,
        "status": "OK" if not issues else "FAILED",
        "issues": issues,
    }
    REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
    REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    return report


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", type=Path, default=None)
    parser.add_argument("--write-report", action="store_true", help="validate default package path when present")
    args = parser.parse_args(argv)

    version = _version()
    output = args.output or (ROOT.parent / f"nexaoffice_worksuite_{version}_full_source.zip")
    report = build_zip(output)
    print("Release zip:", report["status"], output)
    print("Top levels:", ", ".join(report["top_levels"]))
    if report["issues"]:
        for issue in report["issues"]:
            print("-", issue)
    return 0 if report["status"] == "OK" else 1


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