#!/usr/bin/env python3
"""Verify the flattened assets uploaded from a locally built VLCKit candidate."""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path

import verify_candidate as candidate


def one_manifest_value(content: str, key: str) -> str:
    values = re.findall(rf"^{re.escape(key)}=(.+)$", content, re.MULTILINE)
    if len(values) != 1:
        raise ValueError(f"BUILD-MANIFEST.txt must contain one {key} value")
    return values[0]


def checksum_rows(path: Path) -> dict[str, str]:
    rows: dict[str, str] = {}
    for raw in path.read_text(encoding="utf-8").splitlines():
        match = re.fullmatch(r"([0-9a-f]{64})  (.+)", raw)
        if not match:
            raise ValueError(f"invalid SHA256SUMS row: {raw}")
        digest, relative = match.groups()
        relative = candidate.safe_relative(relative)
        if relative in rows:
            raise ValueError(f"duplicate SHA256SUMS path: {relative}")
        rows[relative] = digest
    return rows


def verify(root: Path, expected_platforms: tuple[str, ...]) -> None:
    if root.is_symlink() or not root.is_dir():
        raise ValueError(f"release asset directory is missing or symlinked: {root}")
    if any(path.is_symlink() for path in root.rglob("*")):
        raise ValueError("release asset directory contains a symlink")

    manifest_path = root / "BUILD-MANIFEST.txt"
    source_manifest_path = root / "SOURCE-MANIFEST.txt"
    sums_path = root / "SHA256SUMS"
    manifest = manifest_path.read_text(encoding="utf-8")
    if "PENDING_" in manifest:
        raise ValueError("release build manifest contains an unreviewed pending pin")
    platforms = candidate.parse_platforms(one_manifest_value(manifest, "platforms"))
    if platforms != expected_platforms:
        raise ValueError(
            f"release platforms {platforms} do not match requested {expected_platforms}"
        )
    scope = candidate.artifact_scope(platforms)
    if one_manifest_value(manifest, "artifact_scope") != scope:
        raise ValueError("release artifact scope does not match its platforms")

    base_version = one_manifest_value(manifest, "vlckit_version")
    revision = one_manifest_value(manifest, "candidate_revision")
    if not re.fullmatch(r"[0-9]+(?:\.[0-9]+){2}", base_version):
        raise ValueError("release manifest has an invalid VLCKit version")
    if not re.fullmatch(r"beam\.[0-9]+", revision):
        raise ValueError("release manifest has an invalid candidate revision")
    version = f"{base_version}-{revision}"

    framework_assets = {
        platform: f"beam-{candidate.PLATFORM_PRODUCTS[platform][0].lower()}-"
        f"{platform}-{version}.tar.xz"
        for platform in platforms
    }
    source_name = f"beam-vlckit-{scope}-corresponding-source-{version}.tar.xz"
    audit_name = f"beam-vlckit-{scope}-audit-{version}.tar.xz"
    podspec_names = {
        f"{candidate.PLATFORM_PRODUCTS[platform][0]}.podspec.json"
        for platform in platforms
    }
    expected_files = {
        "BUILD-MANIFEST.txt",
        "SOURCE-MANIFEST.txt",
        "SHA256SUMS",
        source_name,
        audit_name,
        *framework_assets.values(),
        *podspec_names,
    }
    actual_files = {path.name for path in root.iterdir() if path.is_file()}
    actual_directories = {path.name for path in root.iterdir() if path.is_dir()}
    if actual_directories or actual_files != expected_files:
        raise ValueError(
            "unexpected release asset topology: "
            f"missing={sorted(expected_files - actual_files)}, "
            f"extra={sorted(actual_files - expected_files | actual_directories)}"
        )

    rows = checksum_rows(sums_path)
    for name in sorted(expected_files - {"SHA256SUMS"}):
        candidate_path = root / name
        manifest_name = name if name.endswith("MANIFEST.txt") else f"artifacts/{name}"
        expected_digest = rows.get(manifest_name)
        if expected_digest is None:
            raise ValueError(f"SHA256SUMS does not bind uploaded asset: {name}")
        if candidate.sha256(candidate_path) != expected_digest:
            raise ValueError(f"uploaded release asset differs from SHA256SUMS: {name}")

    build_asset_rows: dict[str, str] = {}
    for raw in manifest.splitlines():
        match = re.fullmatch(r"asset_sha256=([0-9a-f]{64})  (.+)", raw)
        if match:
            digest, name = match.groups()
            if name in build_asset_rows:
                raise ValueError(f"duplicate BUILD-MANIFEST asset row: {name}")
            build_asset_rows[name] = digest
    expected_bound_assets = {source_name, *framework_assets.values()}
    if set(build_asset_rows) != expected_bound_assets:
        raise ValueError("BUILD-MANIFEST does not bind the exact payload/source assets")
    for name, digest in build_asset_rows.items():
        if candidate.sha256(root / name) != digest:
            raise ValueError(f"uploaded asset differs from BUILD-MANIFEST: {name}")

    candidate.validate_tar(root / source_name, allow_symlinks=True)
    candidate.validate_tar(root / audit_name, allow_symlinks=False)
    if candidate.tar_member_sha256(root / audit_name, "BUILD-MANIFEST.txt") != candidate.sha256(
        manifest_path
    ):
        raise ValueError("audit archive contains a different BUILD-MANIFEST.txt")
    if candidate.tar_member_sha256(
        root / audit_name, "SOURCE-MANIFEST.txt"
    ) != candidate.sha256(source_manifest_path):
        raise ValueError("audit archive contains a different SOURCE-MANIFEST.txt")

    tag = f"beam-vlckit-{scope}-{version}"
    asset_base = (
        "https://github.com/PrimeCodeSolution/BeamTV/releases/download/" + tag
    )
    for platform, asset_name in framework_assets.items():
        product, _os_style = candidate.PLATFORM_PRODUCTS[platform]
        asset = root / asset_name
        candidate.validate_tar(asset, allow_symlinks=False)
        minimum = one_manifest_value(manifest, f"{platform}_minimum")
        podspec = root / f"{product}.podspec.json"
        candidate.verify_podspec(
            podspec, asset, product, platform, version, minimum
        )
        source = json.loads(podspec.read_text(encoding="utf-8"))["source"]["http"]
        if source != f"{asset_base}/{asset_name}":
            raise ValueError(f"podspec does not target the immutable release tag: {podspec}")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("assets", type=Path)
    parser.add_argument("--platforms", required=True)
    args = parser.parse_args()
    platforms = candidate.parse_platforms(args.platforms)
    verify(args.assets.absolute(), platforms)
    print(f"Published VLCKit asset verification passed: {args.assets}")
    return 0


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