#!/usr/bin/env python3
"""Verify a Stackchain Dashboard release bundle before promotion."""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
import tarfile
from pathlib import Path, PurePosixPath

from build_release import manifest_for, runtime_files_at_commit


def one(directory: Path, pattern: str) -> Path:
    matches = sorted(directory.glob(pattern))
    if len(matches) != 1:
        raise ValueError(f"expected exactly one {pattern} file, found {len(matches)}")
    return matches[0]


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


def verify(input_dir: Path, commit: str, repository: Path) -> None:
    archive_path = one(input_dir, "*.tar.gz")
    manifest_path = one(input_dir, "*.manifest.json")
    checksum_path = one(input_dir, "*.sha256")
    manifest = json.loads(manifest_path.read_text())

    if manifest.get("commit") != commit:
        raise ValueError("manifest commit does not match the tested commit")
    artifact = manifest.get("artifact", {})
    archive_data = archive_path.read_bytes()
    archive_digest = digest(archive_data)
    if artifact.get("name") != archive_path.name:
        raise ValueError("manifest artifact name does not match the bundle")
    if artifact.get("size") != len(archive_data) or artifact.get("sha256") != archive_digest:
        raise ValueError("bundle does not match its manifest")

    checksum_parts = checksum_path.read_text().strip().split()
    if checksum_parts != [archive_digest, archive_path.name]:
        raise ValueError("checksum file does not match the bundle")

    expected_files = manifest.get("files")
    if not isinstance(expected_files, dict):
        raise ValueError("manifest files must be an object")
    source_files = runtime_files_at_commit(repository, commit)
    source_manifest = manifest_for(source_files, commit)["files"]
    if expected_files != source_manifest:
        raise ValueError("bundle manifest does not match the declared Git commit")
    source_by_name = {item.name: item for item in source_files}
    with tarfile.open(archive_path, "r:gz") as archive:
        members = archive.getmembers()
        for member in members:
            path = PurePosixPath(member.name)
            if path.is_absolute() or ".." in path.parts or not member.isfile():
                raise ValueError("bundle contains an unsafe member")
        names = {member.name for member in members}
        if names != set(expected_files) | {"release-manifest.json"}:
            raise ValueError("bundle contents do not match the manifest")
        embedded_file = archive.extractfile("release-manifest.json")
        if embedded_file is None:
            raise ValueError("bundle manifest is missing")
        embedded = json.load(embedded_file)
        if embedded.get("commit") != commit or embedded.get("files") != expected_files:
            raise ValueError("embedded manifest does not match the public manifest")
        for name, metadata in expected_files.items():
            bundled = archive.extractfile(name)
            if bundled is None:
                raise ValueError(f"bundle member is missing: {name}")
            data = bundled.read()
            if metadata != {"sha256": digest(data), "size": len(data)}:
                raise ValueError(f"bundle member failed verification: {name}")
            if data != source_by_name[name].data:
                raise ValueError(f"bundle member differs from the declared Git commit: {name}")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input-dir", type=Path, required=True)
    parser.add_argument("--commit", required=True)
    parser.add_argument("--repository", type=Path, default=Path.cwd())
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    try:
        verify(args.input_dir, args.commit, args.repository)
    except (OSError, ValueError, json.JSONDecodeError, tarfile.TarError) as error:
        print(f"release verification failed: {error}", file=sys.stderr)
        raise SystemExit(1) from error
    print(f"verified release bundle for {args.commit}")


if __name__ == "__main__":
    main()
