#!/usr/bin/env python3
"""Verify the Vaultfire V3 history-free source snapshot."""

from __future__ import annotations

import argparse
import hashlib
from pathlib import Path, PurePosixPath
import tarfile
import tempfile

EXPECTED_ARCHIVE_SHA256 = "7a688170e14c542dbfd2724e9af6a0850ba5070a3b871711fc2d6fc02436ae58"


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


def normalize(name: str) -> str:
    path = PurePosixPath(name)
    parts = tuple(part for part in path.parts if part not in ("", "."))
    if name in (".", "./"):
        return "."
    if path.is_absolute() or not parts or ".." in parts:
        raise ValueError(f"unsafe archive member: {name}")
    return PurePosixPath(*parts).as_posix()


def load_manifest(path: Path) -> dict[str, str]:
    entries: dict[str, str] = {}
    for line in path.read_text(encoding="utf-8").splitlines():
        if not line:
            continue
        digest, name = line.split("  ", 1)
        normalized = normalize(name)
        if normalized in entries:
            raise ValueError(f"duplicate manifest path: {normalized}")
        if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
            raise ValueError(f"invalid SHA-256 for {normalized}")
        entries[normalized] = digest
    if not entries:
        raise ValueError("file manifest is empty")
    return entries


def inspect_archive(archive: Path) -> set[str]:
    files: set[str] = set()
    members: set[str] = set()
    with tarfile.open(archive, "r:xz") as bundle:
        for member in bundle.getmembers():
            normalized = normalize(member.name)
            if normalized == "." and member.isdir():
                continue
            if normalized in members:
                raise ValueError(f"duplicate archive member: {normalized}")
            members.add(normalized)
            if member.issym() or member.islnk() or member.isdev() or member.isfifo():
                raise ValueError(f"unsupported archive member type: {member.name}")
            if not member.isdir() and not member.isfile():
                raise ValueError(f"unsupported archive member type: {member.name}")
            if member.isfile():
                files.add(normalized)
    return files


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("archive", type=Path)
    parser.add_argument("--manifest", required=True, type=Path)
    args = parser.parse_args()

    archive_hash = sha256(args.archive)
    if archive_hash != EXPECTED_ARCHIVE_SHA256:
        raise SystemExit(f"archive SHA-256 mismatch: {archive_hash}")

    expected = load_manifest(args.manifest)
    archived = inspect_archive(args.archive)
    if archived != set(expected):
        missing = sorted(set(expected) - archived)
        extra = sorted(archived - set(expected))
        raise SystemExit(f"archive file-set mismatch; missing={missing[:10]} extra={extra[:10]}")

    with tempfile.TemporaryDirectory(prefix="vaultfire-v3-source-") as directory:
        root = Path(directory)
        with tarfile.open(args.archive, "r:xz") as bundle:
            bundle.extractall(root, filter="data")
        for relative, expected_hash in expected.items():
            if sha256(root / relative) != expected_hash:
                raise SystemExit(f"file SHA-256 mismatch: {relative}")

    print(
        f"PASS: archive={archive_hash} files={len(expected)} "
        "mode=history-free-source-integrity-only"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
