#!/usr/bin/env python3
"""Generate callable, failure-only Rust shims from the authoritative catalog."""

from __future__ import annotations

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

import generate_rust_mapping as mapping

ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "api/public-api.json"
LEDGER = ROOT / "api/RUST-MAPPING.tsv"
COVERAGE = ROOT / "api/SHIM-COVERAGE.md"
COMPILE_FIXTURE = ROOT / "tests/api-compile/src/lib.rs"

TARGETS = {
    "LibreMetaverse.Imaging.Abstractions": ROOT / "crates/libremetaverse-imaging/src/generated.rs",
    "LibreMetaverse.Imaging.Skia": ROOT / "crates/libremetaverse-imaging-skia/src/generated.rs",
    "LibreMetaverse.LslTools": ROOT / "crates/libremetaverse-lsl-tools/src/generated.rs",
    "LibreMetaverse.PrimMesher": ROOT / "crates/libremetaverse-prim-mesher/src/generated.rs",
    "LibreMetaverse.RLV": ROOT / "crates/libremetaverse-rlv/src/generated.rs",
    "LibreMetaverse.Rendering.MeshFoundry": ROOT / "crates/libremetaverse-rendering-mesh-foundry/src/generated.rs",
    "LibreMetaverse.Rendering.Simple": ROOT / "crates/libremetaverse-rendering-simple/src/generated.rs",
    "LibreMetaverse.Types": ROOT / "crates/libremetaverse-types/src/generated.rs",
    "LibreMetaverse.StructuredData": ROOT / "crates/libremetaverse-structured-data/src/generated.rs",
    "LibreMetaverse.Utilities": ROOT / "crates/libremetaverse-utilities/src/generated.rs",
    "LibreMetaverse.Voice.Vivox": ROOT / "crates/libremetaverse-voice-vivox/src/generated.rs",
    "LibreMetaverse.Voice.WebRTC": ROOT / "crates/libremetaverse-voice-webrtc/src/generated.rs",
}

MAIN_TARGET = ROOT / "crates/libremetaverse/src/generated.rs"
WIRE_NAMESPACES = (
    "LibreMetaverse.Assets",
    "LibreMetaverse.Formatters",
    "LibreMetaverse.Imaging",
    "LibreMetaverse.ImportExport",
    "LibreMetaverse.Materials",
    "LibreMetaverse.Messages",
    "LibreMetaverse.Packets",
    "LibreMetaverse.Rendering",
)
WIRE_ROOT_TYPES = {
    "T:LibreMetaverse.Animation",
    "T:LibreMetaverse.BitPack",
    "T:LibreMetaverse.Bumpiness",
    "T:LibreMetaverse.FieldType",
    "T:LibreMetaverse.GLTFMaterialUpdate",
    "T:LibreMetaverse.Grass",
    "T:LibreMetaverse.GrassDefinition",
    "T:LibreMetaverse.Interfaces.IMessage",
    "T:LibreMetaverse.LiveParticle",
    "T:LibreMetaverse.MappingType",
    "T:LibreMetaverse.MediaControls",
    "T:LibreMetaverse.MediaEntry",
    "T:LibreMetaverse.MediaPermission",
    "T:LibreMetaverse.NameValue",
    "T:LibreMetaverse.NameValue.ClassType",
    "T:LibreMetaverse.NameValue.SendtoType",
    "T:LibreMetaverse.NameValue.ValueType",
    "T:LibreMetaverse.PacketFrequency",
    "T:LibreMetaverse.PermissionMask",
    "T:LibreMetaverse.PermissionWho",
    "T:LibreMetaverse.Permissions",
    "T:LibreMetaverse.Shininess",
    "T:LibreMetaverse.TextureAttributes",
    "T:LibreMetaverse.Tree",
    "T:LibreMetaverse.TreeDefinition",
}
COMPOSED_BASE_TYPES = {
    "T:LibreMetaverse.InventoryAnimation",
    "T:LibreMetaverse.InventoryAttachment",
    "T:LibreMetaverse.InventoryCallingCard",
    "T:LibreMetaverse.InventoryCategory",
    "T:LibreMetaverse.InventoryFolder",
    "T:LibreMetaverse.InventoryGesture",
    "T:LibreMetaverse.InventoryItem",
    "T:LibreMetaverse.InventoryLSL",
    "T:LibreMetaverse.InventoryLandmark",
    "T:LibreMetaverse.InventoryMaterial",
    "T:LibreMetaverse.InventoryNotecard",
    "T:LibreMetaverse.InventoryObject",
    "T:LibreMetaverse.InventorySettings",
    "T:LibreMetaverse.InventorySnapshot",
    "T:LibreMetaverse.InventorySound",
    "T:LibreMetaverse.InventoryTexture",
    "T:LibreMetaverse.InventoryWearable",
    "T:LibreMetaverse.Messages.Linden.ChatSessionAcceptInvitation",
    "T:LibreMetaverse.Messages.Linden.ChatSessionRequestMuteUpdate",
    "T:LibreMetaverse.Messages.Linden.ChatSessionRequestStartConference",
    "T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant",
    "T:LibreMetaverse.Packets.DirPlacesReplyPacket",
    "T:LibreMetaverse.Packets.TestMessagePacket",
    "T:LibreMetaverse.Rendering.Joint",
}
OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"}
TRAIT_SUPERTRAITS = {
    "T:LibreMetaverse.IBakingTextureProvider": "std::any::Any",
    "T:LibreMetaverse.Appearance.ICurrentOutfitPolicy": "Send + Sync",
}
CORE_NAMESPACES = (
    "LibreMetaverse.Http",
    "LibreMetaverse.Stats",
    "LibreMetaverse.Threading",
)
CORE_ROOT_TYPES = {
    "T:LibreMetaverse.CapabilitiesReceivedEventArgs",
    "T:LibreMetaverse.Caps",
    "T:LibreMetaverse.Caps.EventQueueCallback",
    "T:LibreMetaverse.CapsCategory",
    "T:LibreMetaverse.CapsEventDictionary",
    "T:LibreMetaverse.CapsRateLimiter",
    "T:LibreMetaverse.CapsRateLimiterOptions",
    "T:LibreMetaverse.ConnectionSettings",
    "T:LibreMetaverse.DisconnectedEventArgs",
    "T:LibreMetaverse.DisposalHelper.DisposalGuard",
    "T:LibreMetaverse.DisposalHelper",
    "T:LibreMetaverse.EventQueueRunningEventArgs",
    "T:LibreMetaverse.EventSubscription`1",
    "T:LibreMetaverse.EventSubscriptionHelper",
    "T:LibreMetaverse.FileHelper",
    "T:LibreMetaverse.DictionaryChangeCallback",
    "T:LibreMetaverse.DictionaryEventAction",
    "T:LibreMetaverse.GenericStreamingMessageEventArgs",
    "T:LibreMetaverse.GenericStreamingMethod",
    "T:LibreMetaverse.GridClient",
    "T:LibreMetaverse.GridClientBakingTextureProvider",
    "T:LibreMetaverse.GridClientServiceCollectionExtensions",
    "T:LibreMetaverse.Helpers",
    "T:LibreMetaverse.HomeInfo",
    "T:LibreMetaverse.HttpCapsClient",
    "T:LibreMetaverse.IBakingTextureProvider",
    "T:LibreMetaverse.IGridClient",
    "T:LibreMetaverse.ImageCodec",
    "T:LibreMetaverse.ImageDownload",
    "T:LibreMetaverse.ImageReceiveProgressEventArgs",
    "T:LibreMetaverse.ImageRequest",
    "T:LibreMetaverse.ImageType",
    "T:LibreMetaverse.IncomingPacketIDCollection",
    "T:LibreMetaverse.Interfaces.IByteBufferPool",
    "T:LibreMetaverse.Logger",
    "T:LibreMetaverse.Logger.LogCallback",
    "T:LibreMetaverse.LoggingSettings",
    "T:LibreMetaverse.LoginCredential",
    "T:LibreMetaverse.LoginException",
    "T:LibreMetaverse.LoginParams",
    "T:LibreMetaverse.LoginProgressEventArgs",
    "T:LibreMetaverse.LoginResponseData",
    "T:LibreMetaverse.LoginResponseData.ParseMessage",
    "T:LibreMetaverse.LoginResponseData.ParseMessageLevel",
    "T:LibreMetaverse.LoginResponseData.ParseResult",
    "T:LibreMetaverse.LoginState",
    "T:LibreMetaverse.LoginStatus",
    "T:LibreMetaverse.LoggedOutEventArgs",
    "T:LibreMetaverse.NetworkManager",
    "T:LibreMetaverse.NetworkManager.DisconnectType",
    "T:LibreMetaverse.NetworkManager.IncomingPacket",
    "T:LibreMetaverse.NetworkManager.LoginResponseCallback",
    "T:LibreMetaverse.NetworkManager.OutgoingPacket",
    "T:LibreMetaverse.LastExecStatus",
    "T:LibreMetaverse.ObservableDictionary`2",
    "T:LibreMetaverse.PacketEventDictionary",
    "T:LibreMetaverse.PacketReceivedEventArgs",
    "T:LibreMetaverse.PacketSentEventArgs",
    "T:LibreMetaverse.PacketSettings",
    "T:LibreMetaverse.HttpCapsClient.ProgressReport",
    "T:LibreMetaverse.ProtocolManager",
    "T:LibreMetaverse.Repeat",
    "T:LibreMetaverse.Settings",
    "T:LibreMetaverse.SimChangedEventArgs",
    "T:LibreMetaverse.SimConnectedEventArgs",
    "T:LibreMetaverse.SimConnectingEventArgs",
    "T:LibreMetaverse.SimDisconnectedEventArgs",
    "T:LibreMetaverse.Simulator",
    "T:LibreMetaverse.Simulator.SimStats",
    "T:LibreMetaverse.SimulatorDataPool",
    "T:LibreMetaverse.SimulatorFeatures",
    "T:LibreMetaverse.SourceType",
    "T:LibreMetaverse.StatusCode",
    "T:LibreMetaverse.TargetType",
    "T:LibreMetaverse.TexturePipeline",
    "T:LibreMetaverse.TexturePipelineSettings",
    "T:LibreMetaverse.TextureRequestState",
    "T:LibreMetaverse.TimingSettings",
    "T:LibreMetaverse.Transfer",
    "T:LibreMetaverse.TransferError",
    "T:LibreMetaverse.UDPBase",
    "T:LibreMetaverse.UDPPacketBuffer",
    "T:LibreMetaverse.XferDownload",
    "T:LibreMetaverse.XferReceivedEventArgs",
}
AVATAR_NAMESPACES = (
    "LibreMetaverse.Animesh",
    "LibreMetaverse.Appearance",
)
AVATAR_TYPE_PREFIXES = (
    "T:LibreMetaverse.Agent",
    "T:LibreMetaverse.Animations",
    "T:LibreMetaverse.AnimeshSkinning",
    "T:LibreMetaverse.Appearance",
    "T:LibreMetaverse.ArchetypeParam",
    "T:LibreMetaverse.Asset",
    "T:LibreMetaverse.Attention",
    "T:LibreMetaverse.Avatar",
    "T:LibreMetaverse.Inventory",
)
AVATAR_ROOT_TYPES = {
    "T:LibreMetaverse.AISResponseMeta",
    "T:LibreMetaverse.AccountLevelBenefits",
    "T:LibreMetaverse.AlertMessageEventArgs",
    "T:LibreMetaverse.BakeType",
    "T:LibreMetaverse.BalanceEventArgs",
    "T:LibreMetaverse.BinBVHAnimationReader",
    "T:LibreMetaverse.BorderCrossingDirection",
    "T:LibreMetaverse.CallingCardOfferedEventArgs",
    "T:LibreMetaverse.CameraConstraintEventArgs",
    "T:LibreMetaverse.ChannelType",
    "T:LibreMetaverse.ChatAudibleLevel",
    "T:LibreMetaverse.ChatEventArgs",
    "T:LibreMetaverse.ChatSessionMember",
    "T:LibreMetaverse.ChatSessionMemberAddedEventArgs",
    "T:LibreMetaverse.ChatSessionMemberLeftEventArgs",
    "T:LibreMetaverse.ChatSourceType",
    "T:LibreMetaverse.ChatType",
    "T:LibreMetaverse.ClassifiedAd",
    "T:LibreMetaverse.ClassifiedInfoReplyEventArgs",
    "T:LibreMetaverse.DeRezDestination",
    "T:LibreMetaverse.DisplayNameUpdateEventArgs",
    "T:LibreMetaverse.DrivenParamInfo",
    "T:LibreMetaverse.EffectType",
    "T:LibreMetaverse.ExperienceInfoEventArgs",
    "T:LibreMetaverse.ExperiencePreferencesEventArgs",
    "T:LibreMetaverse.FindObjectByPathReplyEventArgs",
    "T:LibreMetaverse.FolderUpdatedEventArgs",
    "T:LibreMetaverse.Genepool",
    "T:LibreMetaverse.GenepoolArchetype",
    "T:LibreMetaverse.GroupChatJoinedEventArgs",
    "T:LibreMetaverse.HandPose",
    "T:LibreMetaverse.InitiateDownloadEventArgs",
    "T:LibreMetaverse.InstantMessage",
    "T:LibreMetaverse.InstantMessageDialog",
    "T:LibreMetaverse.InstantMessageEventArgs",
    "T:LibreMetaverse.InstantMessageOnline",
    "T:LibreMetaverse.ItemReceivedEventArgs",
    "T:LibreMetaverse.LindenAttentions",
    "T:LibreMetaverse.LoadUrlEventArgs",
    "T:LibreMetaverse.LookAtType",
    "T:LibreMetaverse.MeanCollisionEventArgs",
    "T:LibreMetaverse.MeanCollisionType",
    "T:LibreMetaverse.MoneyBalanceReplyEventArgs",
    "T:LibreMetaverse.MoneyTransactionType",
    "T:LibreMetaverse.MuteEntry",
    "T:LibreMetaverse.MuteFlags",
    "T:LibreMetaverse.MuteType",
    "T:LibreMetaverse.NavMeshStatusUpdateEventArgs",
    "T:LibreMetaverse.PayPriceReplyEventArgs",
    "T:LibreMetaverse.PayPriceType",
    "T:LibreMetaverse.PickInfoReplyEventArgs",
    "T:LibreMetaverse.PointAtType",
    "T:LibreMetaverse.ProductInfoEventArgs",
    "T:LibreMetaverse.ProfileFlags",
    "T:LibreMetaverse.ProfilePick",
    "T:LibreMetaverse.RebakeAvatarTexturesEventArgs",
    "T:LibreMetaverse.RegionCrossedEventArgs",
    "T:LibreMetaverse.RegionCrossingPredictionEventArgs",
    "T:LibreMetaverse.RegionExperiencesEventArgs",
    "T:LibreMetaverse.SaveAssetToInventoryEventArgs",
    "T:LibreMetaverse.ScriptControlChange",
    "T:LibreMetaverse.ScriptControlEventArgs",
    "T:LibreMetaverse.ScriptDialogEventArgs",
    "T:LibreMetaverse.ScriptPermission",
    "T:LibreMetaverse.ScriptQuestionEventArgs",
    "T:LibreMetaverse.ScriptRunningReplyEventArgs",
    "T:LibreMetaverse.ScriptSensorReplyEventArgs",
    "T:LibreMetaverse.ScriptSensorTypeFlags",
    "T:LibreMetaverse.SetDisplayNameReplyEventArgs",
    "T:LibreMetaverse.SkeletalBoneInfo",
    "T:LibreMetaverse.TaskInventoryReplyEventArgs",
    "T:LibreMetaverse.TaskItemReceivedEventArgs",
    "T:LibreMetaverse.TeleportEventArgs",
    "T:LibreMetaverse.TeleportFlags",
    "T:LibreMetaverse.TeleportLureFlags",
    "T:LibreMetaverse.TeleportStatus",
    "T:LibreMetaverse.TransactionFlags",
    "T:LibreMetaverse.TransactionInfo",
    "T:LibreMetaverse.UUIDNameReplyEventArgs",
    "T:LibreMetaverse.ViewerBenefitsEventArgs",
    "T:LibreMetaverse.ViewerEffectEventArgs",
    "T:LibreMetaverse.ViewerEffectLookAtEventArgs",
    "T:LibreMetaverse.ViewerEffectPointAtEventArgs",
    "T:LibreMetaverse.VisualAlphaParam",
    "T:LibreMetaverse.VisualColorOperation",
    "T:LibreMetaverse.VisualColorParam",
    "T:LibreMetaverse.VisualParam",
    "T:LibreMetaverse.VisualParams",
    "T:LibreMetaverse.VolumeMorphInfo",
    "T:LibreMetaverse.binBVHJoint",
    "T:LibreMetaverse.binBVHJointKey",
}

UNDERLYING = {
    "System.SByte": "i8", "System.Byte": "u8", "System.Int16": "i16", "System.UInt16": "u16",
    "System.Int32": "i32", "System.UInt32": "u32", "System.Int64": "i64", "System.UInt64": "u64",
}

PRIVATE_LAYOUTS = {
    "T:LibreMetaverse.CacheDictionary`2": [("entries", "std::collections::HashMap<TKey, TValue>")],
    "T:LibreMetaverse.DoubleDictionary`3": [
        ("by_first", "std::collections::HashMap<TKey1, TValue>"),
        ("by_second", "std::collections::HashMap<TKey2, TValue>"),
    ],
    "T:LibreMetaverse.EmptyRemovalStrategy`1": [("keys", "Vec<TKey>")],
    "T:LibreMetaverse.ExpiringCache`2": [("entries", "std::collections::HashMap<TKey, TValue>")],
    "T:LibreMetaverse.LruRemovalStrategy`1": [("keys", "Vec<TKey>")],
    "T:LibreMetaverse.MruRemovalStrategy`1": [("keys", "Vec<TKey>")],
    "T:LibreMetaverse.MultiValueDictionary`2": [("entries", "std::collections::HashMap<TKey, Vec<TValue>>")],
    "T:LibreMetaverse.TokenBucket": [("content", "i32"), ("max_burst", "i32"), ("drip_rate", "i32")],
    "T:LibreMetaverse.UUID": [("bytes", "[u8; 16]")],
    "T:LibreMetaverse.StructuredData.OSDArray": [("values", "Vec<OSD>")],
    "T:LibreMetaverse.StructuredData.OSDBinary": [("value", "Vec<u8>")],
    "T:LibreMetaverse.StructuredData.OSDBoolean": [("value", "bool")],
    "T:LibreMetaverse.StructuredData.OSDDate": [("value", "std::time::SystemTime")],
    "T:LibreMetaverse.StructuredData.OSDException": [("message", "String")],
    "T:LibreMetaverse.StructuredData.OSDInteger": [("value", "i32")],
    "T:LibreMetaverse.StructuredData.OSDMap": [("values", "std::collections::HashMap<String, OSD>")],
    "T:LibreMetaverse.StructuredData.OSDReal": [("value", "f64")],
    "T:LibreMetaverse.StructuredData.OSDString": [("value", "String")],
    "T:LibreMetaverse.StructuredData.OSDUUID": [("value", "libremetaverse_types::UUID")],
    "T:LibreMetaverse.StructuredData.OSDUri": [("value", "libremetaverse_types::compat::Uri")],
    "T:LibreMetaverse.Imaging.ManagedImage": [("data", "Vec<u8>")],
}
VALUE_DERIVES = {
    "T:LibreMetaverse.Matrix4": "Clone, Copy, Debug",
    "T:LibreMetaverse.Quaternion": "Clone, Copy, Debug",
    "T:LibreMetaverse.UUID": "Clone, Copy, Debug, Default, Eq, Hash, PartialEq",
    "T:LibreMetaverse.Vector3": "Clone, Copy, Debug",
    "T:LibreMetaverse.Vector3d": "Clone, Copy, Debug",
}
def generic_names(item: dict) -> list[str]:
    return [parameter["name"] for parameter in item.get("generic_parameters", [])]


def format_rust(source: str) -> str:
    result = subprocess.run(
        ["rustfmt", "--edition", "2024", "--emit", "stdout"],
        input=source,
        text=True,
        capture_output=True,
        check=False,
    )
    if result.returncode:
        raise ValueError(result.stderr.strip())
    return result.stdout


def type_name(type_row: dict) -> str:
    return type_row["rust_path"].rsplit("::", 1)[-1]


def generic_suffix(names: list[str]) -> str:
    return "<" + ", ".join(names) + ">" if names else ""


def generic_declarations(item: dict, mapper: mapping.Mapper, owner_names: list[str]) -> list[str]:
    generics = set(owner_names) | set(generic_names(item))
    declarations = []
    for parameter in item.get("generic_parameters", []):
        bounds = [mapper.constraint(constraint, generics) for constraint in parameter.get("constraints", [])]
        if parameter.get("default_constructor"):
            bounds.append("Default")
        declarations.append(parameter["name"] + ((": " + " + ".join(bounds)) if bounds else ""))
    return declarations


def function_name(signature: str) -> str:
    match = re.search(r"\bfn ([A-Za-z_][A-Za-z0-9_]*)", signature)
    if not match:
        raise ValueError(f"invalid mapped function signature: {signature}")
    return match.group(1)


def render_compile_fixture(
    catalog: dict,
    type_by_id: dict[str, dict[str, str]],
    member_rows: dict[str, dict[str, str]],
    mapper: mapping.Mapper,
) -> str:
    lines = [
        "// @generated by tools/generate_api_shims.py; do not edit by hand.",
        "#![allow(clippy::all, clippy::pedantic, dead_code, unused_variables)]",
        "#![allow(non_camel_case_types, non_snake_case)]",
        "",
        'fn argument<T>() -> T { panic!("compile-only API fixture") }',
        "",
    ]
    type_index = member_index = 0
    for assembly in catalog["assemblies"]:
        for item in assembly["types"]:
            type_index += 1
            type_row = type_by_id[item["doc_id"]]
            owner_names = generic_names(item)
            owner_path = type_row["rust_path"]
            owner_type = owner_path + generic_suffix(owner_names)
            owner_expression = owner_path + (("::<" + ", ".join(owner_names) + ">") if owner_names else "")
            trait = item["kind"] == "interface"
            type_parameters = [*owner_names]
            if trait:
                type_parameters.append(f"__Probe: {owner_type}")
                type_argument = "&__Probe"
            else:
                type_argument = f"&{owner_type}"
            declarations = generic_suffix(type_parameters)
            lines += [
                f"// C# type: `{item['doc_id']}`.",
                f"fn type_{type_index:04}{declarations}(_: {type_argument}) {{}}",
                "",
            ]

            for member in item["members"]:
                member_index += 1
                row = member_rows[member["doc_id"]]
                method_names = generic_names(member)
                parameters = [*owner_names, *generic_declarations(member, mapper, owner_names)]
                if trait:
                    parameters.append(f"__Probe: {owner_type}")
                declaration = generic_suffix(parameters)
                instance = not member.get("static") and member["kind"] not in {
                    "constructor",
                    "constant",
                    "enum_value",
                }
                value_parameter = (
                    f"value: &mut {'__Probe' if trait else owner_type}" if instance else ""
                )
                body = []
                if member["kind"] == "field" and not member.get("static"):
                    field = re.fullmatch(r"pub ([^:]+): .+", row["rust_signature"])
                    if not field:
                        raise ValueError(f"invalid mapped field signature: {row['rust_signature']}")
                    body.append(f"let _ = &value.{field.group(1)};")
                elif member["kind"] in {"constant", "enum_value"}:
                    pattern = r"pub const ([A-Za-z_][A-Za-z0-9_]*)" if member["kind"] == "constant" else r"([A-Za-z_][A-Za-z0-9_]*):"
                    name = re.match(pattern, row["rust_signature"])
                    if not name:
                        raise ValueError(f"invalid mapped value signature: {row['rust_signature']}")
                    body.append(f"let _ = {owner_expression}::{name.group(1)};")
                else:
                    signatures = row["rust_signature"].split(" ; ")
                    property_argument_counts = []
                    if member["kind"] in {"property", "indexer"}:
                        accessors = {accessor["kind"] for accessor in member.get("accessors", [])}
                        if "get" in accessors:
                            property_argument_counts.append(len(member.get("parameters", [])))
                        if "set" in accessors:
                            property_argument_counts.append(len(member.get("parameters", [])) + 1)
                    for signature_index, signature in enumerate(signatures):
                        name = function_name(signature)
                        generic_call = (("::<" + ", ".join(method_names) + ">") if method_names else "")
                        if instance:
                            target = f"value.{name}{generic_call}"
                        elif trait:
                            target = f"<__Probe as {owner_type}>::{name}{generic_call}"
                        else:
                            target = f"{owner_expression}::{name}{generic_call}"
                        argument_count = len(member.get("parameters", []))
                        if member["kind"] == "event":
                            argument_count = 1
                        elif property_argument_counts:
                            argument_count = property_argument_counts[signature_index]
                        arguments = ", ".join("argument()" for _ in range(argument_count))
                        body.append(f"let _ = {target}({arguments});")
                lines += [
                    f"// C# member: `{member['doc_id']}`.",
                    f"fn member_{member_index:05}{declaration}({value_parameter}) {{",
                    *[f"    {line}" for line in body],
                    "}",
                    "",
                ]
    return format_rust("\n".join(lines).rstrip() + "\n")


def flags_enum(item: dict) -> bool:
    return any(attribute["type"] == "System.FlagsAttribute" for attribute in item.get("attributes", []))


def duplicate_enum_values(item: dict) -> bool:
    values = [member["value"]["value"] for member in item["members"] if member["kind"] == "enum_value"]
    return len(values) != len(set(values))


def render_enum(item: dict, rust_name: str) -> list[str]:
    underlying = UNDERLYING[item["enum_underlying_type"]]
    values = [member for member in item["members"] if member["kind"] == "enum_value"]
    lines = [f"/// C# type: `{item['doc_id']}`."]
    if item["doc_id"] in OPEN_ENUM_IDS:
        lines += [
            "#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]",
            "#[repr(transparent)]",
            f"pub struct {rust_name}(pub {underlying});",
            "#[allow(non_upper_case_globals)]",
            f"impl {rust_name} {{",
        ]
        for member in values:
            lines += [
                f"    /// C# member: `{member['doc_id']}`.",
                f"    pub const {mapping.pascal(member['name'])}: Self = Self({member['value']['value']});",
            ]
        lines.append("}")
        return lines
    if flags_enum(item) or duplicate_enum_values(item):
        lines += [
            "#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]",
            "#[repr(transparent)]",
            f"pub struct {rust_name}(pub {underlying});",
            f"impl {rust_name} {{",
        ]
        for member in values:
            name = mapping.snake(member["name"]).upper()
            value = member["value"]["value"]
            lines += [f"    /// C# member: `{member['doc_id']}`.", f"    pub const {name}: Self = Self({value});"]
        lines.append("}")
        return lines
    lines += [
        "#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]",
        f"#[repr({underlying})]",
        f"pub enum {rust_name} {{",
    ]
    for member in values:
        lines += [f"    /// C# member: `{member['doc_id']}`.", f"    {mapping.pascal(member['name'])} = {member['value']['value']},"]
    lines.append("}")
    return lines


def render_body(signature: str, member_id: str, error_model: str, asyncness: str, trait: bool) -> str:
    if trait:
        signature = signature.removeprefix("pub ")
    failure = (
        f"libremetaverse_types::not_implemented({json.dumps(member_id)})"
        if error_model.startswith("Result")
        else f"libremetaverse_types::unimplemented_api!({json.dumps(member_id)})"
    )
    if asyncness == "boxed_future" or "std::pin::Pin<Box<dyn std::future::Future" in signature:
        failure = f"Box::pin(async {{ {failure} }})"
    return f"    {signature} {{ {failure} }}"


def rust_constant_value(item: dict) -> str:
    value = item["value"]["value"]
    if item["type"] == "System.String":
        return json.dumps(value)
    if item["type"] == "System.Boolean":
        return value.lower()
    if item["type"] in {"System.Single", "System.Double"} and not any(
        marker in value.lower() for marker in (".", "e")
    ):
        return value + ".0"
    return value


def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
    rust_name = type_name(type_row)
    if item["kind"] == "enum":
        return "\n".join(render_enum(item, rust_name))
    names = generic_names(item)
    suffix = generic_suffix(names)
    trait = item["kind"] == "interface"
    lines = [f"/// C# type: `{item['doc_id']}`."]
    if trait:
        supertrait = TRAIT_SUPERTRAITS.get(item["doc_id"])
        inheritance = f": {supertrait}" if supertrait else ""
        lines.append(f"pub trait {rust_name}{suffix}{inheritance} {{")
    else:
        fields = [member for member in item["members"] if member["kind"] == "field" and not member.get("static")]
        private_fields = PRIVATE_LAYOUTS.get(item["doc_id"], [])
        base_type = item.get("base_type") if item["doc_id"] in COMPOSED_BASE_TYPES else None
        if derives := VALUE_DERIVES.get(item["doc_id"]):
            lines.append(f"#[derive({derives})]")
        if item["doc_id"] == "T:LibreMetaverse.StructuredData.OSD":
            lines += [
                "#[non_exhaustive]",
                "pub enum OSD {",
                "    Undefined, Boolean(bool), Integer(i32), Real(f64), String(String),",
                "    UUID(libremetaverse_types::UUID), Date(std::time::SystemTime),",
                "    Uri(libremetaverse_types::compat::Uri), Binary(Vec<u8>),",
                "    Array(Vec<OSD>), Map(std::collections::HashMap<String, OSD>), LlsdXml(String),",
                "}",
            ]
        elif not fields and not names and not private_fields and not base_type:
            lines.append(f"pub struct {rust_name};")
        else:
            lines.append(f"pub struct {rust_name}{suffix} {{")
            if base_type:
                lines += [
                    f"    /// Rust composition for C# base type `{base_type}`.",
                    f"    pub base: {mapper.type(base_type, None, set(names))},",
                ]
            for field in fields:
                field_signature = member_rows[field["doc_id"]]["rust_signature"]
                field_name_match = re.fullmatch(r"pub ([^:]+): (.+)", field_signature)
                if not field_name_match:
                    raise ValueError(f"invalid mapped field signature: {field_signature}")
                field_name = field_name_match.group(1)
                field_type = field_name_match.group(2)
                lines += [f"    /// C# member: `{field['doc_id']}`.", f"    pub {field_name}: {field_type},"]
            for field_name, field_type in private_fields:
                lines.append(f"    {field_name}: {field_type},")
            if names:
                tuple_type = "(" + ", ".join(names) + ("," if len(names) == 1 else "") + ")"
                lines.append(f"    _marker: std::marker::PhantomData<{tuple_type}>,")
            lines.append("}")
        lines += [f"impl{suffix} {rust_name}{suffix} {{"]

    for member in item["members"]:
        if member["kind"] in {"enum_value"} or (member["kind"] == "field" and not member.get("static")):
            continue
        row = member_rows[member["doc_id"]]
        lines.append(f"    /// C# member: `{member['doc_id']}`.")
        if member["kind"] == "constant":
            lines.append(f"    {row['rust_signature']} = {rust_constant_value(member)};")
            continue
        signatures = row["rust_signature"].split(" ; ")
        for index, signature in enumerate(signatures):
            if index:
                lines.append(f"    /// Setter for C# member: `{member['doc_id']}`.")
            lines.append(
                render_body(
                    signature,
                    member["doc_id"],
                    row["error_model"],
                    row["asyncness"],
                    trait,
                )
            )
    lines.append("}")
    if not trait:
        for interface in item.get("interfaces", []):
            base, arguments, _ = mapping.type_parts(interface)
            if base not in mapper.interfaces or not base.startswith("LibreMetaverse."):
                continue
            mapped_arguments = [mapper.type(argument, None, set(names)) for argument in arguments]
            trait_path = mapper.resolved[base] + generic_suffix(mapped_arguments)
            lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}")
    return "\n".join(lines)


def render_pending_type(item: dict, type_row: dict) -> str:
    rust_name = type_name(type_row)
    names = generic_names(item)
    suffix = generic_suffix(names)
    lines = [f"/// Pending catalog type `{item['doc_id']}`; completed by a later milestone issue."]
    if item["kind"] == "interface":
        lines.append(f"pub trait {rust_name}{suffix} {{}}")
    elif names:
        tuple_type = "(" + ", ".join(names) + ("," if len(names) == 1 else "") + ")"
        lines += [f"pub struct {rust_name}{suffix} {{", f"    _marker: std::marker::PhantomData<{tuple_type}>,", "}"]
    else:
        lines.append(f"pub struct {rust_name};")
    return "\n".join(lines)


def wire_type(item: dict) -> bool:
    return (
        item["doc_id"] in WIRE_ROOT_TYPES
        or item["doc_id"].startswith("T:LibreMetaverse.Primitive")
        or item["namespace"].startswith(WIRE_NAMESPACES)
    )


def core_type(item: dict) -> bool:
    return item["doc_id"] in CORE_ROOT_TYPES or item["namespace"].startswith(CORE_NAMESPACES)


def avatar_type(item: dict) -> bool:
    return (
        item["doc_id"] in AVATAR_ROOT_TYPES
        or item["doc_id"].startswith(AVATAR_TYPE_PREFIXES)
        or item["namespace"].startswith(AVATAR_NAMESPACES)
    )


def world_type(item: dict) -> bool:
    return not wire_type(item) and not core_type(item) and not avatar_type(item)


def render_modules(items: list[tuple[dict, dict, bool]], member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
    tree: dict = {"items": [], "children": {}}
    for item, type_row, complete in items:
        node = tree
        for segment in filter(None, type_row["rust_module"].split("::")):
            node = node["children"].setdefault(segment, {"items": [], "children": {}})
        node["items"].append((item, type_row, complete))

    def render_node(node: dict) -> list[str]:
        lines = []
        for item, type_row, complete in node["items"]:
            rendered = render_type(item, type_row, member_rows, mapper) if complete else render_pending_type(item, type_row)
            lines += [rendered, ""]
        for name, child in sorted(node["children"].items()):
            body = render_node(child)
            lines.append(f"pub mod {name} {{")
            lines += [("    " + line) if line else "" for line in body]
            lines += ["}", ""]
        return lines

    return "\n".join(render_node(tree))


def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[int, int, bool]]]:
    type_rows, resolved = mapping.build_type_rows(catalog)
    type_by_id = {row["csharp_type_id"]: row for row in type_rows}
    member_by_id = {
        member["doc_id"]: member
        for assembly in catalog["assemblies"]
        for item in assembly["types"]
        for member in item["members"]
    }
    configured_main_ids = WIRE_ROOT_TYPES | CORE_ROOT_TYPES | AVATAR_ROOT_TYPES
    missing_configured_ids = configured_main_ids - type_by_id.keys()
    if missing_configured_ids:
        raise ValueError(f"unknown configured main-slice IDs: {sorted(missing_configured_ids)}")
    missing_boxed_fields = mapping.BOXED_FIELD_IDS - member_by_id.keys()
    if missing_boxed_fields:
        raise ValueError(f"unknown configured boxed fields: {sorted(missing_boxed_fields)}")
    if invalid_boxed_fields := [
        member_id
        for member_id in mapping.BOXED_FIELD_IDS
        if member_by_id[member_id]["kind"] != "field"
    ]:
        raise ValueError(f"configured boxed members are not fields: {sorted(invalid_boxed_fields)}")
    interfaces = {
        *{mapping.type_parts(item["signature"])[0] for assembly in catalog["assemblies"] for item in assembly["types"] if item["kind"] == "interface"},
        *{
            mapping.type_parts(item["signature"])[0]
            for item in catalog["external_types"]
            if item["kind"] == "interface" and not item["assembly"]["name"].startswith("System")
        },
    }
    mapper = mapping.Mapper(resolved, interfaces)
    member_rows = {row["csharp_id"]: row for row in csv.DictReader(LEDGER.open(), delimiter="\t")}
    outputs = {}
    coverage = {}
    for assembly in catalog["assemblies"]:
        name = assembly["identity"]["name"]
        if name not in TARGETS and name != "LibreMetaverse":
            continue
        chunks = [
            "// @generated by tools/generate_api_shims.py; do not edit by hand.",
            "#![allow(clippy::missing_errors_doc)]",
            "#![allow(clippy::must_use_candidate)]",
            "#![allow(clippy::all, clippy::pedantic, dead_code, unused_variables)]",
            "#![allow(non_camel_case_types)]",
            "#![allow(non_snake_case)]",
            "",
        ]
        selected = (
            assembly["types"]
            if name in TARGETS
            else [
                item
                for item in assembly["types"]
                if wire_type(item) or core_type(item) or avatar_type(item) or world_type(item)
            ]
        )
        selected_ids = {item["doc_id"] for item in selected}
        rendered_items = [
            (item, type_by_id[item["doc_id"]], item["doc_id"] in selected_ids)
            for item in assembly["types"]
        ]
        chunks.append(render_modules(rendered_items, member_rows, mapper))
        source = format_rust("\n".join(chunks).rstrip() + "\n")
        expected_types = selected_ids
        expected_members = {member["doc_id"] for item in selected for member in item["members"]}
        emitted_types = set(re.findall(r"C# type: `(.*)`\.", source))
        emitted_members = set(re.findall(r"C# member: `(.*)`\.", source))
        if emitted_types != expected_types or emitted_members != expected_members:
            raise ValueError(
                f"generated catalog ID mismatch for {name}: "
                f"types missing/stale={len(expected_types - emitted_types)}/{len(emitted_types - expected_types)}, "
                f"members missing/stale={len(expected_members - emitted_members)}/{len(emitted_members - expected_members)}"
            )
        outputs[TARGETS.get(name, MAIN_TARGET)] = source
        coverage[name] = (len(selected), sum(len(item["members"]) for item in selected), len(selected) == len(assembly["types"]))
    outputs[COMPILE_FIXTURE] = render_compile_fixture(catalog, type_by_id, member_rows, mapper)
    return outputs, coverage


def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) -> str:
    lines = [
        "# Callable shim coverage", "", "Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.", "",
        "| Assembly | Types | Members | Status |", "|---|---:|---:|---|",
    ]
    total_types = total_members = 0
    for assembly in catalog["assemblies"]:
        name = assembly["identity"]["name"]
        types = len(assembly["types"])
        members = sum(len(item["members"]) for item in assembly["types"])
        if name in coverage:
            got_types, got_members, complete = coverage[name]
            if complete and (types, members) != (got_types, got_members):
                raise ValueError(f"incomplete generated slice for {name}")
            status = "callable failure-only shim" if complete else f"partial callable shim ({got_types:,} types / {got_members:,} members)"
            total_types += got_types
            total_members += got_members
        else:
            status = "pending milestone issue"
        lines.append(f"| `{name}` | {types:,} | {members:,} | {status} |")
    if "LibreMetaverse" in coverage:
        main_types = next(
            assembly["types"]
            for assembly in catalog["assemblies"]
            if assembly["identity"]["name"] == "LibreMetaverse"
        )
        wire_types = [item for item in main_types if wire_type(item)]
        core_types = [item for item in main_types if core_type(item) and not wire_type(item)]
        avatar_types = [
            item
            for item in main_types
            if avatar_type(item) and not wire_type(item) and not core_type(item)
        ]
        world_types = [item for item in main_types if world_type(item)]
        complete = coverage["LibreMetaverse"][2]
        lines += [
            "",
            (
                "The `LibreMetaverse` assembly is complete across issue #7 wire/data, issue #8 core runtime/networking,"
                if complete
                else "The partial `LibreMetaverse` row contains the completed issue #7 wire/data, issue #8 core runtime/networking,"
            ),
            (
                "issue #9 avatar-facing manager, and issue #10 world/social/service manager slices."
                if complete
                else "and issue #9 avatar-facing manager slices. World/social/service manager types remain pending for issue #10."
            ),
            "",
            "| Main assembly slice | Types | Members |",
            "|---|---:|---:|",
            f"| Issue #7 wire/data | {len(wire_types):,} | {sum(len(item['members']) for item in wire_types):,} |",
            f"| Issue #8 core runtime/networking | {len(core_types):,} | {sum(len(item['members']) for item in core_types):,} |",
            f"| Issue #9 avatar-facing managers | {len(avatar_types):,} | {sum(len(item['members']) for item in avatar_types):,} |",
            f"| Issue #10 world/social/service managers | {len(world_types):,} | {sum(len(item['members']) for item in world_types):,} |",
        ]
    lines += ["", f"Current callable coverage: **{total_types:,} types / {total_members:,} members**.", ""]
    return "\n".join(lines)


def generate() -> dict[Path, str]:
    catalog = json.loads(CATALOG.read_text())
    sources, coverage = generate_sources(catalog)
    sources[COVERAGE] = coverage_report(catalog, coverage)
    return sources


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true")
    args = parser.parse_args()
    outputs = generate()
    if args.check:
        stale = [str(path.relative_to(ROOT)) for path, text in outputs.items() if not path.exists() or path.read_text() != text]
        if stale:
            raise SystemExit("stale generated API shims: " + ", ".join(stale))
    else:
        for path, text in outputs.items():
            path.write_text(text)
        print("generated " + ", ".join(str(path.relative_to(ROOT)) for path in outputs))


if __name__ == "__main__":
    main()
