#!/usr/bin/env python3
"""Audit catalog, mapping, exported shims, and the downstream compile fixture."""

from __future__ import annotations

import argparse
import csv
import json
import re
from collections import Counter
from pathlib import Path

import generate_api_shims as shims
import generate_rust_mapping as mapping

ROOT = Path(__file__).resolve().parents[1]
REPORT = ROOT / "api/API-COVERAGE.md"
FIXTURE = ROOT / "tests/api-compile/src/lib.rs"
FORBIDDEN = {
    "ShimValue": r"\bShimValue\b",
    "dynamic argument bag": r"\b(?:DynamicArgumentBag|ArgumentBag)\b",
    "invented variadic": r"\b(?:Variadic|VarArgs)\b|\.\.\.",
    "erased Any argument vector": r"Vec\s*<\s*Box\s*<\s*dyn\s+(?:std::any::)?Any",
}


def require_exact(label: str, expected: set[str], actual: Counter[str]) -> None:
    actual_ids = set(actual)
    missing = expected - actual_ids
    stale = actual_ids - expected
    duplicates = sorted(item for item, count in actual.items() if count != 1)
    if missing or stale or duplicates:
        raise ValueError(
            f"{label}: missing={len(missing)}, stale={len(stale)}, duplicates={len(duplicates)}"
        )


def matches(paths: list[Path], pattern: str) -> Counter[str]:
    found: Counter[str] = Counter()
    regex = re.compile(pattern, re.MULTILINE)
    for path in paths:
        found.update(regex.findall(path.read_text()))
    return found


def audit() -> str:
    catalog = json.loads(mapping.CATALOG.read_text())
    assemblies = {assembly["identity"]["name"]: assembly for assembly in catalog["assemblies"]}
    catalog_types = {
        item["doc_id"]
        for assembly in catalog["assemblies"]
        for item in assembly["types"]
    }
    catalog_members = {
        member["doc_id"]
        for assembly in catalog["assemblies"]
        for item in assembly["types"]
        for member in item["members"]
    }
    external_types = {item["doc_id"] for item in catalog["external_types"]}
    support_types = {f"T:{name}" for name in mapping.SUPPORT_TYPES}

    type_rows = list(csv.DictReader(mapping.TYPE_LEDGER.open(), delimiter="\t"))
    member_rows = list(csv.DictReader(mapping.MEMBER_LEDGER.open(), delimiter="\t"))
    require_exact(
        "type mapping",
        catalog_types | external_types | support_types,
        Counter(row["csharp_type_id"] for row in type_rows),
    )
    require_exact(
        "member mapping",
        catalog_members,
        Counter(row["csharp_id"] for row in member_rows),
    )
    duplicate_destinations = [
        item
        for item, count in Counter(row["rust_item_path"] for row in member_rows).items()
        if count != 1
    ]
    if duplicate_destinations:
        raise ValueError(f"duplicate Rust member destinations: {len(duplicate_destinations)}")

    generated = [shims.MAIN_TARGET, *shims.TARGETS.values()]
    require_exact(
        "exported types",
        catalog_types,
        matches(generated, r"^\s*/// C# type: `(T:.+)`\.$"),
    )
    require_exact(
        "exported members",
        catalog_members,
        matches(generated, r"^\s*/// C# member: `(.+)`\.$"),
    )
    require_exact(
        "fixture types",
        catalog_types,
        matches([FIXTURE], r"^\s*// C# type: `(T:.+)`\.$"),
    )
    require_exact(
        "fixture members",
        catalog_members,
        matches([FIXTURE], r"^\s*// C# member: `(.+)`\.$"),
    )
    fixture_text = FIXTURE.read_text()
    if len(re.findall(r"^fn type_", fixture_text, re.MULTILINE)) != len(catalog_types):
        raise ValueError("fixture type probe count does not match the catalog")
    if len(re.findall(r"^fn member_", fixture_text, re.MULTILINE)) != len(catalog_members):
        raise ValueError("fixture member probe count does not match the catalog")

    scan_text = "\n".join(
        path.read_text() for path in [*generated, FIXTURE, mapping.MEMBER_LEDGER]
    )
    forbidden_hits = [name for name, pattern in FORBIDDEN.items() if re.search(pattern, scan_text)]
    if forbidden_hits:
        raise ValueError("forbidden erased shim patterns: " + ", ".join(forbidden_hits))
    mapping.validate_generated_shims()

    lines = [
        "# Complete API gate coverage",
        "",
        "Generated by `python3 tools/check_api_coverage.py --write`; do not edit by hand.",
        "",
        "| Assembly | Catalog types | Mapped/exported/fixture types | Catalog members | Mapped/exported/fixture members |",
        "|---|---:|---:|---:|---:|",
    ]
    for name, assembly in assemblies.items():
        type_count = len(assembly["types"])
        member_count = sum(len(item["members"]) for item in assembly["types"])
        lines.append(
            f"| `{name}` | {type_count:,} | {type_count:,} | {member_count:,} | {member_count:,} |"
        )
    lines += [
        f"| **Total** | **{len(catalog_types):,}** | **{len(catalog_types):,}** | **{len(catalog_members):,}** | **{len(catalog_members):,}** |",
        "",
        "## Exclusions and boundary records",
        "",
        "- Public catalog type exclusions: **0**.",
        "- Public catalog member exclusions: **0**.",
        f"- External signature types: **{len(external_types):,}** mapped to core/std, adopted cross-platform crates, or project-owned boundary types; their external member APIs are intentionally not copied.",
        f"- Referenced non-public support traits: **{len(support_types):,}** mapped because public inheritance signatures require them.",
        f"- Intentional delegate APM differences: **{sum(row['status'] == 'intentional_difference' for row in member_rows):,}**; every replacement is exported and compiled by the fixture.",
        "- Forbidden erased shim patterns: **0** (`ShimValue`, dynamic argument bags, invented variadics, or `Vec<Box<dyn Any>>` argument bags).",
        "",
        "The fixture is a standalone Rust workspace with path dependencies only. Building it requires neither .NET nor compiled LibreMetaverse assemblies.",
        "",
    ]
    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()
    report = audit()
    if args.write:
        REPORT.write_text(report)
        print(f"generated {REPORT.relative_to(ROOT)}")
    elif not REPORT.exists() or REPORT.read_text() != report:
        raise SystemExit(f"stale coverage report: {REPORT.relative_to(ROOT)}")
    else:
        print("API coverage matches the catalog, mappings, exports, and fixture")


if __name__ == "__main__":
    main()
