#!/usr/bin/env python3
"""
Build ncii-shield-deploy.zip for cPanel upload.

Excludes (in addition to standard .gitignore intent):
  - node_modules/, .git/, .preview/
  - backups/, uploads/, evidence/, data/evidence/
  - *.log (any)
  - *.sqlite except the production data files we ship
  - test-debug*, test*.png, test3.png
  - cookies.txt, state-snapshot.txt
  - Local shell-misuse junk: ({src, {, document.querySelectorAll*
  - Windows reserved-name leaks: nul, con, prn, aux

INCLUDED data files (the production dataset — Nisali's cases, users,
API keys, audit log, etc.):
  - data/ncii.sqlite
  - data/ncii.sqlite-wal
  - data/ncii.sqlite-shm

NOT included (server-specific, regenerated on boot):
  - data/server.log, data/server-err.log   (log rotation handles these)
  - data/sessions.sqlite                    (live session store — wipes on restart)
  - data/evidence/                          (per-scan MHTMLs; large)
  - data/test*.sqlite*                      (debug artifacts)
"""

import os
import sys
import shutil
import zipfile
from pathlib import Path

ROOT = Path(r"C:\Users\shafp\Downloads\nic\ncii-shield")
OUT  = ROOT / "ncii-shield-deploy.zip"

# (dir_path_part, file_name) — if either side matches, skip.
EXCLUDE_DIRS = {
    "node_modules", ".git", ".preview", ".idea", ".vscode",
    "backups", "uploads", "evidence",
}

# Files in data/ that should ship inside the zip. Sessions and logs
# are server-specific; the production sqlite is what we want.
DATA_KEEP = {"ncii.sqlite", "ncii.sqlite-shm", "ncii.sqlite-wal"}

EXCLUDE_FILE_PATTERNS = [
    # Log files anywhere in the tree.
    "*.log",
    # Server-only sqlite (sessions, debug artifacts).
    "sessions.sqlite", "sessions.sqlite-shm", "sessions.sqlite-wal",
    "test-debug*",
    "test*.png", "test3.png",
    "cookies.txt", "state-snapshot.txt",
    "({src", "{",                                   # shell-misuse junk
    "document.querySelectorAll*",                   # JS-paste-as-filename junk
    "nul", "con", "prn", "aux",                     # Windows reserved names
    "*.zip",                                        # the deploy zip itself
    ".env", ".DS_Store", "Thumbs.db",
    # Debug / scratch files that have leaked into the workspace.
    "_dump-keys.js",                                # ad-hoc key dump script
    "_dump-email.js", "_dump-email2.js",            # ad-hoc email/transport dumps
    # Server-environment config — each cPanel/Apache host has its own
    # PassengerAppRoot / PassengerNodejs / cPanel username baked in.
    # The repo copy is a TEMPLATE with `yourcpanel` placeholder; overwriting
    # a live server's real .htaccess with the template breaks Passenger.
    # Operators manage .htaccess on the server directly.
    ".htaccess",
]

# Files we DO want even though they match patterns above.
KEEP_FILES = {".env.example"}

# Files inside data/ we want to ship (the production dataset).
DATA_KEEP_FILES = {"ncii.sqlite", "ncii.sqlite-shm", "ncii.sqlite-wal"}

def is_excluded_file(rel: str, name: str) -> bool:
    """Decide whether to skip a file. `rel` is the path relative to ROOT."""
    # Files inside data/ get the special-cased allow-list.
    if rel.startswith("data/") and name in DATA_KEEP_FILES:
        return False
    if name in KEEP_FILES:
        return False
    from fnmatch import fnmatch
    for pat in EXCLUDE_FILE_PATTERNS:
        if fnmatch(name, pat):
            return True
    # Exact-name matches (Windows reserved names leak in as plain files)
    if name.lower() in {"nul", "con", "prn", "aux"}:
        return True
    # Generic sqlite catch (anything not in DATA_KEEP_FILES).
    from fnmatch import fnmatch as _fn
    if _fn(name, "*.sqlite") or _fn(name, "*.sqlite-shm") or _fn(name, "*.sqlite-wal"):
        return True
    return False

def main():
    if not ROOT.is_dir():
        print(f"ROOT missing: {ROOT}", file=sys.stderr)
        sys.exit(1)
    if OUT.exists():
        OUT.unlink()

    # Verify the data files exist before we build — fail loud if the
    # operator forgot to point this at a populated checkout.
    missing = [f"data/{f}" for f in DATA_KEEP_FILES if not (ROOT / "data" / f).exists()]
    if missing:
        print(f"WARN: data files missing (zip will be incomplete): {missing}", file=sys.stderr)

    included = []
    skipped_dirs = set()
    skipped_files = []

    with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
        for dirpath, dirnames, filenames in os.walk(ROOT):
            # Filter out excluded dirs in-place so os.walk doesn't descend.
            dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDE_DIRS)
            for fn in filenames:
                full = Path(dirpath) / fn
                rel = full.relative_to(ROOT).as_posix()
                if is_excluded_file(rel, fn):
                    skipped_files.append(rel)
                    continue
                zf.write(full, rel)
                included.append(rel)

    size_mb = OUT.stat().st_size / (1024 * 1024)
    print(f"OK -> {OUT}  ({size_mb:.2f} MB, {len(included)} files)")
    if skipped_dirs:
        print(f"  excluded dirs: {sorted(skipped_dirs)}")
    if skipped_files:
        # Show first 20
        print(f"  excluded files ({len(skipped_files)} total), first 20:")
        for s in skipped_files[:20]:
            print(f"    - {s}")

if __name__ == "__main__":
    main()