#!/usr/bin/env python3
"""Stage the reviewed iOS VLCKit payload for CocoaPods without rebuilding it."""

from __future__ import annotations

import argparse
import os
import shutil
import tarfile
import tempfile
from pathlib import Path

import audit_static_modules
import audit_xcframework
import verify_candidate
import verify_release_assets


TOOL_DIR = Path(__file__).resolve().parent
MOBILE_DIR = TOOL_DIR.parents[1]
PINS_PATH = TOOL_DIR / "pins.env"


def _version(pins: dict[str, str]) -> str:
    return f"{pins['BEAM_VLCKIT_VERSION']}-{pins['BEAM_CANDIDATE_REVISION']}"


def _asset_names(pins: dict[str, str]) -> dict[str, str]:
    version = _version(pins)
    return {
        "framework": f"beam-mobilevlckit-ios-{version}.tar.xz",
        "source": f"beam-vlckit-ios-corresponding-source-{version}.tar.xz",
        "audit": f"beam-vlckit-ios-audit-{version}.tar.xz",
        "podspec": "MobileVLCKit.podspec.json",
    }


def _pinned_inputs(
    root: Path,
    *,
    candidate: bool,
    pins: dict[str, str],
) -> dict[str, Path]:
    names = _asset_names(pins)
    if candidate:
        verify_candidate.verify(root)
        manifest = (root / "BUILD-MANIFEST.txt").read_text(encoding="utf-8")
        platforms = verify_candidate.parse_platforms(
            verify_release_assets.one_manifest_value(manifest, "platforms")
        )
        if platforms != ("ios",) or not verify_candidate.verified_marker_state(
            root, platforms
        ):
            raise ValueError("local input must be a fully verified iOS-only candidate")
        artifact_root = root / "artifacts"
    else:
        verify_release_assets.verify(root, ("ios",))
        artifact_root = root

    inputs = {
        "build_manifest": root / "BUILD-MANIFEST.txt",
        "source_manifest": root / "SOURCE-MANIFEST.txt",
        "sums": root / "SHA256SUMS",
        "framework": artifact_root / names["framework"],
        "source": artifact_root / names["source"],
        "audit": artifact_root / names["audit"],
        "podspec": artifact_root / names["podspec"],
    }
    pin_names = {
        "build_manifest": "BEAM_IOS_BUILD_MANIFEST_SHA256",
        "source_manifest": "BEAM_IOS_SOURCE_MANIFEST_SHA256",
        "sums": "BEAM_IOS_SHA256SUMS_SHA256",
        "framework": "BEAM_IOS_FRAMEWORK_ASSET_SHA256",
        "source": "BEAM_IOS_SOURCE_ASSET_SHA256",
        "audit": "BEAM_IOS_AUDIT_ASSET_SHA256",
        "podspec": "BEAM_IOS_PODSPEC_SHA256",
    }
    for label, path in inputs.items():
        if path.is_symlink() or not path.is_file():
            raise ValueError(f"missing or symlinked iOS release input: {path}")
        actual = verify_candidate.sha256(path)
        expected = pins[pin_names[label]]
        if actual != expected:
            raise ValueError(f"pinned iOS {label} hash mismatch: {actual} != {expected}")
    return inputs


def _extract_regular_tar(archive_path: Path, destination: Path) -> None:
    verify_candidate.validate_tar(archive_path, allow_symlinks=False)
    with tarfile.open(archive_path, "r:xz") as archive:
        for member in archive.getmembers():
            relative = verify_candidate.safe_relative(member.name)
            target = destination / relative
            if member.isdir():
                target.mkdir(parents=True, exist_ok=True)
                target.chmod(member.mode & 0o777)
                continue
            target.parent.mkdir(parents=True, exist_ok=True)
            source = archive.extractfile(member)
            if source is None:
                raise ValueError(f"cannot extract archive member: {relative}")
            with target.open("wb") as sink:
                shutil.copyfileobj(source, sink, length=1024 * 1024)
            target.chmod(member.mode & 0o777)


def _verify_staged_payload(
    output: Path,
    inputs: dict[str, Path],
    pins: dict[str, str],
    *,
    binary_audit: bool,
    require_marker: bool = True,
) -> None:
    expected_entries = {
        "BUILD-MANIFEST.txt",
        "COPYING.txt",
        "MobileVLCKit.podspec.json",
        "MobileVLCKit.xcframework",
        "SHA256SUMS",
        "SOURCE-MANIFEST.txt",
    }
    if require_marker:
        expected_entries.add("VERIFIED")
    if output.is_symlink() or not output.is_dir():
        raise ValueError(f"staged iOS consumer is missing or symlinked: {output}")
    if any(path.is_symlink() for path in output.rglob("*")):
        raise ValueError("staged iOS consumer contains a symlink")
    actual_entries = {path.name for path in output.iterdir()}
    if actual_entries != expected_entries:
        raise ValueError(
            "unexpected staged iOS consumer topology: "
            f"missing={sorted(expected_entries - actual_entries)}, "
            f"extra={sorted(actual_entries - expected_entries)}"
        )
    if require_marker:
        marker = output / "VERIFIED"
        if not marker.is_file() or marker.stat().st_size != 0:
            raise ValueError("staged iOS VERIFIED marker must be an empty regular file")

    copies = {
        "BUILD-MANIFEST.txt": inputs["build_manifest"],
        "SOURCE-MANIFEST.txt": inputs["source_manifest"],
        "SHA256SUMS": inputs["sums"],
        "MobileVLCKit.podspec.json": inputs["podspec"],
    }
    for name, source in copies.items():
        if verify_candidate.sha256(output / name) != verify_candidate.sha256(source):
            raise ValueError(f"staged iOS metadata differs from reviewed input: {name}")

    framework = output / "MobileVLCKit.xcframework"
    expected_payload = verify_candidate.payload_hashes(inputs["framework"], "MobileVLCKit")
    if verify_candidate.tree_hashes(framework) != expected_payload:
        raise ValueError("staged MobileVLCKit framework differs from reviewed archive")
    if verify_candidate.sha256(output / "COPYING.txt") != verify_candidate.tar_member_sha256(
        inputs["framework"], "COPYING.txt"
    ):
        raise ValueError("staged MobileVLCKit license differs from reviewed archive")

    binary = framework / "ios-arm64/MobileVLCKit.framework/MobileVLCKit"
    if verify_candidate.sha256(binary) != pins["BEAM_IOS_DEVICE_BINARY_SHA256"]:
        raise ValueError("staged MobileVLCKit device binary hash is not pinned")

    if not binary_audit:
        return
    audit_root = Path(tempfile.mkdtemp(prefix=".beam-vlckit-audit.", dir=output.parent))
    try:
        _extract_regular_tar(inputs["audit"], audit_root)
        report = audit_root / "staged-ios-xcframework.json"
        audit_xcframework.audit(
            framework,
            "MobileVLCKit",
            "ios",
            pins["BEAM_IOS_DEPLOYMENT_TARGET"],
            audit_root / "audit/modules/ios/iPhone/approved-module-entries.tsv",
            str(MOBILE_DIR),
            report,
        )
        reviewed_report = audit_root / "audit/ios-xcframework.json"
        if report.read_bytes() != reviewed_report.read_bytes():
            raise ValueError("staged MobileVLCKit binary audit differs from reviewed report")
    finally:
        shutil.rmtree(audit_root)


def stage(
    root: Path,
    output: Path,
    *,
    candidate: bool,
    pins: dict[str, str],
    binary_audit: bool = True,
) -> None:
    root = root.resolve()
    output = output.absolute()
    inputs = _pinned_inputs(root, candidate=candidate, pins=pins)
    output.parent.mkdir(parents=True, exist_ok=True)
    if output.exists() or output.is_symlink():
        _verify_staged_payload(
            output, inputs, pins, binary_audit=binary_audit
        )
        return

    temporary = Path(tempfile.mkdtemp(prefix=f".{output.name}.", dir=output.parent))
    installed = False
    try:
        _extract_regular_tar(inputs["framework"], temporary)
        shutil.copyfile(inputs["build_manifest"], temporary / "BUILD-MANIFEST.txt")
        shutil.copyfile(inputs["source_manifest"], temporary / "SOURCE-MANIFEST.txt")
        shutil.copyfile(inputs["sums"], temporary / "SHA256SUMS")
        shutil.copyfile(inputs["podspec"], temporary / "MobileVLCKit.podspec.json")
        _verify_staged_payload(
            temporary,
            inputs,
            pins,
            binary_audit=binary_audit,
            require_marker=False,
        )
        (temporary / "VERIFIED").touch(mode=0o600)
        os.rename(temporary, output)
        installed = True
    finally:
        if not installed and temporary.exists():
            shutil.rmtree(temporary)


def main() -> int:
    parser = argparse.ArgumentParser()
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("--candidate", type=Path)
    source.add_argument("--release-assets", type=Path)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    pins = audit_static_modules.read_assignments(PINS_PATH)
    expected_tag = f"beam-vlckit-ios-{_version(pins)}"
    if pins["BEAM_IOS_RELEASE_TAG"] != expected_tag:
        parser.error("the pinned iOS release tag does not match version/revision pins")
    output = args.output or (
        MOBILE_DIR / "build/vlckit-ios-consumer" / _version(pins)
    )
    try:
        stage(
            args.candidate or args.release_assets,
            output,
            candidate=args.candidate is not None,
            pins=pins,
        )
    except (OSError, ValueError, tarfile.TarError) as error:
        parser.error(str(error))
    print(f"Verified iOS VLCKit consumer staged at {output.absolute()}")
    return 0


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