#!/usr/bin/env python3
"""Build the submission zip correctly.

Produces submission/{repository,transcripts,RELEASE_NOTE.md} at the archive
root, excludes the files that make an archive unreadable to the evaluator, and
prints the lowercase SHA-256 to paste into the Results Form.
"""

from __future__ import annotations

import hashlib
import os
from pathlib import Path
import sys
import zipfile

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "submission.zip"

EXCLUDED_DIRS = {
    "__MACOSX", ".git", ".venv", "venv", "node_modules", "__pycache__",
    ".pytest_cache", ".ruff_cache", ".mypy_cache", ".idea", ".vscode",
    # Native/web build output. The README tells you to run the app, and doing so
    # writes hundreds of megabytes here -- which would blow the evaluator's
    # entry-count and size caps in the last minutes before the deadline. None of
    # it is your work and all of it regenerates.
    "build", ".gradle", ".expo", "dist", ".cxx", "Pods",
}
# celerybeat-schedule: celery beat's persistent schedule file. Confirmed by
# actually running `make up`: beat's default --schedule path is relative to
# its CWD (/app, which docker-compose.yml bind-mounts to this directory), so
# it lands at the repository root on the host the moment the documented `make
# up` workflow runs -- not just something present from checkout, like the
# mobile binaries above. It is a GNU dbm file: not valid UTF-8 and NUL-bearing
# in its first bytes, so it fails verify.py's text check exactly like they do.
# Also redirected out of the bind-mounted tree in docker-compose.yml so it
# stops appearing here at all; kept here too as a safety net.
EXCLUDED_NAMES = {".DS_Store", "submission.zip", "Thumbs.db", "celerybeat-schedule"}
# .jar and .keystore: the supplied React Native tree ships a generated
# mobile/android/gradle/wrapper/gradle-wrapper.jar and a generated
# mobile/android/app/debug.keystore. Both are build tooling the evaluator does
# not need and can regenerate -- and both are unconditionally rejected by
# verify.py regardless (a .jar as a nested archive, a keystore as non-UTF-8
# binary), so excluding the whole suffix at construction time costs nothing
# and also covers any other .jar/.keystore a build produces later (e.g. a
# release keystore) under the same reasoning.
EXCLUDED_SUFFIXES = {
    ".pyc", ".pyo", ".log", ".sqlite3", ".jar", ".keystore", ".jsbundle",
    ".apk", ".aab", ".hprof",
}


def _skip(path: Path) -> bool:
    if any(part in EXCLUDED_DIRS for part in path.parts):
        return True
    if path.name in EXCLUDED_NAMES or path.name.startswith("._"):
        return True
    return path.suffix in EXCLUDED_SUFFIXES


def main() -> int:
    release_note = ROOT / "RELEASE_NOTE.md"
    transcripts = ROOT / "transcripts"
    problems = []
    if not release_note.is_file():
        problems.append("RELEASE_NOTE.md is missing from the repository root")
    if not (transcripts / "INDEX.md").is_file():
        problems.append("transcripts/INDEX.md is missing")
    native = transcripts / "native"
    if not native.is_dir() or not any(p.is_file() for p in native.rglob("*")):
        problems.append("transcripts/native/ has no exported records")

    # transcripts/ maps into the archive verbatim, and the only transcript paths
    # the evaluator accepts are INDEX.md and native/. One stray file here gets
    # the whole archive rejected unread, so it has to stop the build instead.
    strays = []
    for path in sorted(transcripts.rglob("*")):
        if not path.is_file() or _skip(path.relative_to(ROOT)):
            continue
        inner = path.relative_to(transcripts)
        if inner.as_posix() != "INDEX.md" and inner.parts[0] != "native":
            strays.append(path.relative_to(ROOT).as_posix())
    if strays:
        listed = ", ".join(strays[:5]) + (", ..." if len(strays) > 5 else "")
        problems.append(
            f"transcripts/ holds {len(strays)} file(s) that may not be submitted: "
            f"{listed}. Only transcripts/INDEX.md and files under "
            "transcripts/native/ may ship -- move anything else outside "
            "transcripts/ or delete it, then run `make submit` again."
        )

    if problems:
        for problem in problems:
            print(f"[FAIL] {problem}", file=sys.stderr)
        return 1

    OUT.unlink(missing_ok=True)
    count = 0
    with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as archive:
        for path in sorted(ROOT.rglob("*")):
            if not path.is_file() or _skip(path.relative_to(ROOT)):
                continue
            relative = path.relative_to(ROOT)
            if relative.parts[0] == "transcripts":
                arc = Path("submission") / relative
            elif relative.name == "RELEASE_NOTE.md" and len(relative.parts) == 1:
                arc = Path("submission") / relative
            else:
                arc = Path("submission") / "repository" / relative
            archive.write(path, arc.as_posix())
            count += 1

    digest = hashlib.sha256(OUT.read_bytes()).hexdigest()
    print(f"[OK] wrote {OUT.name} ({count} files, {OUT.stat().st_size:,} bytes)")
    print()
    print("Paste this SHA-256 into the Results Form exactly as shown:")
    print(f"  {digest}")
    print()
    print("Now run: make verify")
    return 0


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