"""Package source, built assets and a verified production-only vendor directory.

First install composer.lock with --no-dev --no-scripts in an isolated source copy.
Never use the running application's vendor directory for the production install.
"""
from pathlib import Path
import argparse
import hashlib
import json
import os
import zipfile

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vendor", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
vendor = args.vendor.resolve()
out = args.output.resolve()
if out.parent != root.parent or out.exists():
    raise SystemExit("Choose a new ZIP filename in the parent outputs directory.")
if vendor == (root / "vendor").resolve():
    raise SystemExit("Use an isolated production vendor directory, not the running app.")
installed = json.loads((vendor / "composer/installed.json").read_text(encoding="utf-8"))
locked = json.loads((root / "composer.lock").read_text(encoding="utf-8"))
expected = {p["name"]: (p["version"], p.get("dist", {}).get("reference")) for p in locked["packages"]}
actual = {p["name"]: (p["version"], p.get("dist", {}).get("reference")) for p in installed["packages"]}
if installed.get("dev") is not False or actual != expected:
    raise SystemExit("Vendor must contain exactly the locked production packages, without dev dependencies.")
manifest = root / "public/build/manifest.json"
assets = json.loads(manifest.read_text(encoding="utf-8"))
for item in assets.values():
    for name in [item["file"], *item.get("css", [])]:
        if not (root / "public/build" / name).is_file():
            raise SystemExit("Build assets first with npm ci && npm run build.")
excluded = {".git", ".codex", "node_modules", "vendor", "work", "test-results", "playwright-report", ".phpunit.cache"}
files = []
for base, dirs, names in os.walk(root, followlinks=False):
    dirs[:] = sorted(d for d in dirs if d not in excluded and not (Path(base) / d).is_symlink())
    for name in sorted(names):
        path = Path(base) / name
        rel = path.relative_to(root).as_posix()
        if path.is_symlink():
            continue
        if (rel.startswith("storage/") or rel.startswith("bootstrap/cache/")) and name != ".gitignore":
            continue
        if name.startswith(".env") and name not in {".env.production.example", ".env.example"}:
            continue
        if path.suffix.lower() in {".pem", ".key", ".p12", ".pfx", ".log", ".sqlite", ".sqlite3", ".db", ".sql", ".encrypted", ".zip"}:
            continue
        if name.lower().endswith((".sqlite-wal", ".sqlite-shm", ".sqlite-journal", ".db-wal", ".db-shm", ".db-journal", ".sql.gz", ".sql.bak")):
            continue
        if rel == "public/hot" or name == ".phpunit.result.cache":
            continue
        files.append((path, rel))
for path in sorted(vendor.rglob("*")):
    if path.is_symlink():
        raise SystemExit("Vendor symlinks are not supported in the upload package.")
    if path.is_file():
        files.append((path, "vendor/" + path.relative_to(vendor).as_posix()))
inventory = []
with zipfile.ZipFile(out, "x", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
    for path, rel in files:
        data = path.read_bytes()
        # ZIP/Linux shell scripts must not inherit Windows CRLF line endings.
        if path.suffix == ".sh":
            data = data.replace(b"\r\n", b"\n")
        inventory.append(hashlib.sha256(data).hexdigest() + "  " + rel)
        info = zipfile.ZipInfo("zentrikhrms/" + rel)
        info.external_attr = 0o100644 << 16
        info.compress_type = zipfile.ZIP_DEFLATED
        archive.writestr(info, data)
    archive.writestr("zentrikhrms/SHA256SUMS.txt", "\n".join(inventory) + "\n")
digest = hashlib.sha256(out.read_bytes()).hexdigest()
out.with_suffix(".zip.sha256").write_text(digest + "  " + out.name + "\n", encoding="utf-8")
print(json.dumps({"archive": str(out), "files": len(files), "production_packages": len(actual), "sha256": digest}))
