#!/usr/bin/env python3
"""Build pinned LibreMetaverse metadata twice and emit a deterministic API catalog."""

from __future__ import annotations

import argparse
import filecmp
import shutil
import subprocess
import tempfile
from pathlib import Path

UPSTREAM_COMMIT = "2aa70bb68513b39795da5d13c88f31b86e85a3ba"
TARGET_FRAMEWORK = "net10.0"
PROJECTS = (
    "LibreMetaverse.Types/LibreMetaverse.Types.csproj",
    "LibreMetaverse.StructuredData/LibreMetaverse.StructuredData.csproj",
    "LibreMetaverse.Imaging.Abstractions/LibreMetaverse.Imaging.Abstractions.csproj",
    "LibreMetaverse.Imaging.Skia/LibreMetaverse.Imaging.Skia.csproj",
    "PrimMesher/LibreMetaverse.PrimMesher.csproj",
    "LibreMetaverse/LibreMetaverse.csproj",
    "LibreMetaverse.Rendering.Simple/LibreMetaverse.Rendering.Simple.csproj",
    "LibreMetaverse.Rendering.MeshFoundry/LibreMetaverse.Rendering.MeshFoundry.csproj",
    "LibreMetaverse.LslTools/LibreMetaverse.LslTools.csproj",
    "LibreMetaverse.RLV/LibreMetaverse.RLV.csproj",
    "LibreMetaverse.Utilities/LibreMetaverse.Utilities.csproj",
    "LibreMetaverse.Voice.Vivox/LibreMetaverse.Voice.Vivox.csproj",
    "LibreMetaverse.Voice.WebRTC/LibreMetaverse.Voice.WebRTC.csproj",
)
ASSEMBLIES = tuple(Path(project).stem for project in PROJECTS)


def run(*command: str, cwd: Path | None = None) -> None:
    subprocess.run(command, cwd=cwd, check=True)


def output(command: list[str], cwd: Path) -> str:
    return subprocess.run(command, cwd=cwd, check=True, text=True, stdout=subprocess.PIPE).stdout.strip()


def validate_upstream(upstream: Path) -> None:
    actual = output(["git", "rev-parse", "HEAD"], upstream)
    if actual != UPSTREAM_COMMIT:
        raise RuntimeError(f"expected upstream {UPSTREAM_COMMIT}, found {actual}")
    status = output(["git", "status", "--porcelain"], upstream)
    if status:
        raise RuntimeError("the pinned upstream checkout must be clean")


def build_tool(root: Path, temporary: Path) -> Path:
    artifacts = temporary / "tool-artifacts"
    run(
        "dotnet", "build", str(root / "tools/api-catalog/ApiCatalog.csproj"),
        "-c", "Release", "--artifacts-path", str(artifacts),
    )
    candidates = sorted((artifacts / "bin").rglob("ApiCatalog.dll"))
    if len(candidates) != 1:
        raise RuntimeError(f"expected one extractor assembly, found {candidates}")
    run("dotnet", str(candidates[0]), "--self-test")
    return candidates[0]


def build_upstream(upstream: Path, temporary: Path) -> tuple[list[Path], Path]:
    artifacts = temporary / "upstream-artifacts"
    binaries = temporary / "bin"
    generated = temporary / "generated"
    for project in PROJECTS:
        command = [
            "dotnet", "build", str(upstream / project), "-c", "Release", "-f", TARGET_FRAMEWORK,
            "--artifacts-path", str(artifacts), "-p:ContinuousIntegrationBuild=true",
            f"-p:OutputPath={binaries}/", "-p:CopyLocalLockFileAssemblies=true",
        ]
        if project == "LibreMetaverse/LibreMetaverse.csproj":
            command.extend([
                "--no-incremental", "-p:EmitCompilerGeneratedFiles=true",
                f"-p:CompilerGeneratedFilesOutputPath={generated}",
            ])
        run(*command)

    paths = []
    for name in ASSEMBLIES:
        path = binaries / f"{name}.dll"
        if not path.is_file():
            raise RuntimeError(f"missing built assembly {path}")
        paths.append(path)
    return paths, generated


def extract(tool: Path, assemblies: list[Path], generated: Path, destination: Path) -> None:
    run(
        "dotnet", str(tool), "--output", str(destination),
        "--upstream-commit", UPSTREAM_COMMIT,
        "--target-framework", TARGET_FRAMEWORK,
        "--generated-root", str(generated),
        *(str(path) for path in assemblies),
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--upstream", type=Path, default=Path("../libremetaverse"))
    parser.add_argument("--output", type=Path, default=Path("api/public-api.json"))
    args = parser.parse_args()
    root = Path(__file__).resolve().parent.parent
    upstream = args.upstream.resolve()
    destination = args.output.resolve()
    validate_upstream(upstream)

    with tempfile.TemporaryDirectory(prefix="metacrate-api-catalog-") as directory:
        temporary = Path(directory)
        tool = build_tool(root, temporary)
        assemblies, generated = build_upstream(upstream, temporary / "upstream")
        outputs = []
        for pass_number in (1, 2):
            catalog = temporary / f"public-api-{pass_number}.json"
            extract(tool, assemblies, generated, catalog)
            outputs.append(catalog)
        if not filecmp.cmp(outputs[0], outputs[1], shallow=False):
            raise RuntimeError("two clean extraction passes produced different bytes")
        validate_upstream(upstream)
        destination.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(outputs[0], destination)
    print(f"wrote deterministic API catalog to {destination}")


if __name__ == "__main__":
    main()
