#!/usr/bin/env python3
"""Build a reproducible release bundle from an exact Git commit."""

from __future__ import annotations

import argparse
import gzip
import hashlib
import io
import json
import re
import subprocess
import tarfile
from dataclasses import dataclass
from pathlib import Path, PurePosixPath


RUNTIME_DIRECTORIES = ("docs", "frontend", "src")
RUNTIME_FILES = (
    "README.md",
    "requirements.txt",
    "scripts/rotate_unfiled_drafts.py",
)
EXCLUDED_PARTS = {"__pycache__", ".pytest_cache"}
EXCLUDED_SUFFIXES = (".pyc", ".pyo")
COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}")


@dataclass(frozen=True)
class RuntimeFile:
    name: str
    data: bytes
    executable: bool


def sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def _git(root: Path, *args: str, text: bool = False) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", "-C", str(root), *args],
        capture_output=True,
        check=False,
        text=text,
    )


def runtime_files_at_commit(root: Path, commit: str) -> list[RuntimeFile]:
    """Read the runtime allowlist directly from the declared Git tree."""
    root = root.resolve()
    if not COMMIT_PATTERN.fullmatch(commit):
        raise ValueError("declared commit must be a full 40-character Git object ID")
    resolved = _git(root, "rev-parse", "--verify", f"{commit}^{{commit}}", text=True)
    if resolved.returncode != 0 or resolved.stdout.strip() != commit:
        raise ValueError("declared commit is unavailable")

    listing = _git(root, "ls-tree", "-rz", "--full-tree", commit)
    if listing.returncode != 0:
        raise ValueError("could not inspect the declared commit")

    files: list[RuntimeFile] = []
    for record in listing.stdout.split(b"\0"):
        if not record:
            continue
        metadata, raw_name = record.split(b"\t", 1)
        mode, object_type, object_id = metadata.decode("ascii").split()
        name = raw_name.decode("utf-8")
        path = PurePosixPath(name)
        selected = name in RUNTIME_FILES or (path.parts and path.parts[0] in RUNTIME_DIRECTORIES)
        if not selected or object_type != "blob":
            continue
        if mode not in {"100644", "100755"}:
            raise ValueError(f"release member is not a regular file: {name}")
        if EXCLUDED_PARTS.intersection(path.parts) or name.endswith(EXCLUDED_SUFFIXES):
            raise ValueError(f"declared commit contains generated release member: {name}")
        blob = _git(root, "cat-file", "blob", object_id)
        if blob.returncode != 0:
            raise ValueError(f"could not read release member: {name}")
        files.append(RuntimeFile(name, blob.stdout, mode == "100755"))

    names = {item.name for item in files}
    missing = [name for name in RUNTIME_FILES if name not in names]
    if missing or not any(name.startswith("src/") for name in names) or not any(
        name.startswith("frontend/") for name in names
    ):
        raise ValueError("declared commit is missing required runtime files")
    return sorted(files, key=lambda item: item.name)


def manifest_for(files: list[RuntimeFile], commit: str) -> dict:
    return {
        "schema_version": 1,
        "commit": commit,
        "files": {
            item.name: {"sha256": sha256(item.data), "size": len(item.data)} for item in files
        },
    }


def json_bytes(payload: dict) -> bytes:
    return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode()


def tar_info(name: str, size: int, epoch: int, executable: bool = False) -> tarfile.TarInfo:
    info = tarfile.TarInfo(name)
    info.size = size
    info.mtime = epoch
    info.mode = 0o755 if executable else 0o644
    info.uid = info.gid = 0
    info.uname = info.gname = "root"
    return info


def build(root: Path, output_dir: Path, commit: str, epoch: int) -> tuple[Path, Path, Path]:
    files = runtime_files_at_commit(root, commit)
    embedded_manifest = manifest_for(files, commit)
    archive_members: dict[str, tuple[bytes, bool]] = {
        item.name: (item.data, item.executable) for item in files
    }
    archive_members["release-manifest.json"] = (json_bytes(embedded_manifest), False)

    output_dir.mkdir(parents=True, exist_ok=True)
    stem = f"stackchain-dashboard-{commit[:12]}"
    archive_path = output_dir / f"{stem}.tar.gz"
    with archive_path.open("wb") as raw:
        with gzip.GzipFile(fileobj=raw, mode="wb", filename="", mtime=epoch) as compressed:
            with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive:
                for name in sorted(archive_members):
                    data, executable = archive_members[name]
                    archive.addfile(tar_info(name, len(data), epoch, executable), io.BytesIO(data))

    archive_data = archive_path.read_bytes()
    digest = sha256(archive_data)
    public_manifest = {
        **embedded_manifest,
        "artifact": {
            "name": archive_path.name,
            "sha256": digest,
            "size": len(archive_data),
        },
    }
    manifest_path = output_dir / f"{stem}.manifest.json"
    checksum_path = output_dir / f"{stem}.sha256"
    manifest_path.write_bytes(json_bytes(public_manifest))
    checksum_path.write_text(f"{digest}  {archive_path.name}\n")
    return archive_path, manifest_path, checksum_path


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=Path, default=Path.cwd())
    parser.add_argument("--output-dir", type=Path, default=Path("dist"))
    parser.add_argument("--commit", required=True)
    parser.add_argument("--source-date-epoch", type=int, required=True)
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    try:
        paths = build(args.root, args.output_dir, args.commit, args.source_date_epoch)
    except (OSError, ValueError) as error:
        raise SystemExit(f"release build failed: {error}") from error
    for path in paths:
        print(path)


if __name__ == "__main__":
    main()
