#!/usr/bin/env python3
"""Create a deterministic Pivot Sans source-and-binary release ZIP."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo

ROOT = Path(__file__).resolve().parent.parent
DIST = ROOT / "dist"
SPEC = json.loads((ROOT / "pipeline" / "spec.json").read_text())
VERSION = SPEC["fork_version"]
ZIP_TIMESTAMP = tuple(
    int(part) for part in SPEC["release_date"].split("-")
) + (0, 0, 0)
EXCLUDED_PARTS = {".git", ".pytest_cache", ".venv", "__MACOSX", "__pycache__"}


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def verify_dist_checksums() -> None:
    manifest = DIST / "SHA256SUMS"
    if not manifest.is_file():
        raise FileNotFoundError("dist/SHA256SUMS is missing; run pipeline/build.py")
    for line in manifest.read_text().splitlines():
        if not line.strip():
            continue
        expected, relative = line.split(maxsplit=1)
        path = DIST / relative.removeprefix("*")
        actual = sha256(path) if path.is_file() else "<missing>"
        if actual != expected:
            raise ValueError(
                f"release checksum mismatch for {path}: "
                f"expected {expected}, got {actual}"
            )


def should_include(path: Path) -> bool:
    relative = path.relative_to(ROOT)
    return (
        not any(part in EXCLUDED_PARTS for part in relative.parts)
        and path.name != ".DS_Store"
        and not path.name.startswith("._")
        and path.suffix not in {".pyc", ".pyo"}
    )


def package(output: Path) -> None:
    verify_dist_checksums()
    files = sorted(path for path in ROOT.rglob("*") if path.is_file() and should_include(path))
    output.parent.mkdir(parents=True, exist_ok=True)

    with ZipFile(output, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive:
        for path in files:
            archive_name = (Path(ROOT.name) / path.relative_to(ROOT)).as_posix()
            info = ZipInfo(archive_name, date_time=ZIP_TIMESTAMP)
            info.create_system = 3
            info.compress_type = ZIP_DEFLATED
            info.external_attr = (0o644 & 0xFFFF) << 16
            archive.writestr(info, path.read_bytes(), compresslevel=9)

    print(f"wrote {output} ({output.stat().st_size} bytes)")
    print(f"sha256 {sha256(output)}")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--output",
        type=Path,
        default=ROOT.parent / f"pivot-sans-{VERSION}.zip",
        help="release ZIP path (default: %(default)s)",
    )
    args = parser.parse_args()
    package(args.output.resolve())


if __name__ == "__main__":
    main()
