#!/usr/bin/env python3
"""Verify a freshly built Nexamas.UI assembly against the approved documentation/API baseline."""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any


def read_json(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8-sig") as handle:
        value = json.load(handle)
    if not isinstance(value, dict):
        raise ValueError(f"Expected a JSON object: {path}")
    return value


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", type=Path, required=True)
    parser.add_argument("--assembly", type=Path, required=True)
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path(".artifacts/documentation-api-drift"),
    )
    parser.add_argument(
        "--allow-newer-sources",
        action="store_true",
        help="Diagnostic-only escape hatch; release/CI gates must not use it.",
    )
    args = parser.parse_args()

    root = args.root.resolve()
    assembly = args.assembly if args.assembly.is_absolute() else root / args.assembly
    assembly = assembly.resolve()
    output_dir = args.output_dir if args.output_dir.is_absolute() else root / args.output_dir
    output_dir = output_dir.resolve()

    baseline_path = root / "eng/tests/Nexamas.UI.DocumentationClosureBaseline.json"
    exporter_path = root / "tools/docs/Export-NexamasUIPublicApiMetadata.py"

    failures: list[str] = []
    if not assembly.is_file():
        failures.append(f"Built assembly was not found: {assembly}")
    if not baseline_path.is_file():
        failures.append(f"Documentation closure baseline was not found: {baseline_path}")
    if not exporter_path.is_file():
        failures.append(f"Public API metadata exporter was not found: {exporter_path}")
    if failures:
        for failure in failures:
            print(f"FAIL: {failure}", file=sys.stderr)
        return 1

    output_dir.mkdir(parents=True, exist_ok=True)
    command = [
        sys.executable,
        str(exporter_path),
        "--root",
        str(root),
        "--assembly",
        str(assembly),
        "--output-dir",
        str(output_dir),
    ]
    completed = subprocess.run(command, cwd=root, text=True, capture_output=True)
    if completed.stdout:
        print(completed.stdout.rstrip())
    if completed.returncode != 0:
        if completed.stderr:
            print(completed.stderr.rstrip(), file=sys.stderr)
        print("FAIL: Public API metadata export failed.", file=sys.stderr)
        return completed.returncode or 1

    baseline = read_json(baseline_path)
    inventory_path = output_dir / "public-api-assembly.json"
    inventory = read_json(inventory_path)

    checks = {
        "publicApiHash": (
            inventory.get("publicApiHash"),
            baseline.get("publicApiHash"),
        ),
        "consumerVisibleTypeCount": (
            inventory.get("externallyVisibleTypeCount"),
            baseline.get("consumerVisibleTypeCount"),
        ),
        "consumerVisibleMemberCount": (
            inventory.get("consumerVisibleDeclaredMemberCount"),
            baseline.get("consumerVisibleMemberCount"),
        ),
    }
    for name, (actual, expected) in checks.items():
        if actual != expected:
            failures.append(f"{name} drifted: actual={actual!r}; approved={expected!r}")

    alignment = inventory.get("projectAlignment") or {}
    compile_count = alignment.get("compileIncludeCount")
    if compile_count != baseline.get("compiledSourceFileCount"):
        failures.append(
            "compiledSourceFileCount drifted: "
            f"actual={compile_count!r}; approved={baseline.get('compiledSourceFileCount')!r}"
        )
    missing_includes = list(alignment.get("missingCompileIncludes") or [])
    uncompiled_sources = list(alignment.get("uncompiledSourceFiles") or [])
    newer_sources = list(alignment.get("compiledSourcesNewerThanAssembly") or [])
    if missing_includes:
        failures.append(f"Missing Compile Include files: {len(missing_includes)}")
    if uncompiled_sources:
        failures.append(f"Uncompiled source files: {len(uncompiled_sources)}")
    if newer_sources and not args.allow_newer_sources:
        failures.append(
            f"Built assembly predates {len(newer_sources)} compiled source file(s); perform a clean rebuild."
        )

    proof = {
        "schema": "nexamas-ui-public-api-drift-proof/v1",
        "status": "PASS" if not failures else "FAIL",
        "assembly": str(assembly.relative_to(root)) if assembly.is_relative_to(root) else str(assembly),
        "assemblySha256": inventory.get("assemblySha256"),
        "moduleMvid": inventory.get("moduleMvid"),
        "actual": {
            "publicApiHash": inventory.get("publicApiHash"),
            "consumerVisibleTypeCount": inventory.get("externallyVisibleTypeCount"),
            "consumerVisibleMemberCount": inventory.get("consumerVisibleDeclaredMemberCount"),
            "compiledSourceFileCount": compile_count,
            "compiledSourcesNewerThanAssembly": len(newer_sources),
        },
        "approved": {
            "publicApiHash": baseline.get("publicApiHash"),
            "consumerVisibleTypeCount": baseline.get("consumerVisibleTypeCount"),
            "consumerVisibleMemberCount": baseline.get("consumerVisibleMemberCount"),
            "compiledSourceFileCount": baseline.get("compiledSourceFileCount"),
        },
        "failures": failures,
        "inventory": str(inventory_path.relative_to(root)) if inventory_path.is_relative_to(root) else str(inventory_path),
    }
    proof_path = output_dir / "public-api-drift-proof.json"
    proof_path.write_text(json.dumps(proof, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    if failures:
        for failure in failures:
            print(f"FAIL: {failure}", file=sys.stderr)
        print(f"Public API drift proof: {proof_path}", file=sys.stderr)
        return 1

    print(
        "Nexamas UI public API drift gate PASS. "
        f"Types={proof['actual']['consumerVisibleTypeCount']}; "
        f"Members={proof['actual']['consumerVisibleMemberCount']}; "
        f"Hash={proof['actual']['publicApiHash']}; Proof={proof_path}"
    )
    return 0


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