#!/usr/bin/env python3
"""Generate the bundled route/model catalog and upstream inventory.

The inventories are intentionally explicit and reviewable. `codex-mmo catalog
verify --remote` compares dynamic provider listings against this baseline; it
does not silently add models whose protocol and capabilities are unknown.
"""

from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "libexec"))
from mmo_inventory_snapshot import load_inventory_snapshots  # noqa: E402
from mmo_profiles import validate_catalog_data  # noqa: E402
from mmo_util import atomic_write_json, atomic_write_text, toml_dumps  # noqa: E402
from mmo_version import MMO_SCHEMA_VERSION  # noqa: E402

INVENTORY_SNAPSHOT_ROOT = ROOT / "config" / "inventory-snapshots"


def route(**values: Any) -> dict[str, Any]:
    return values


def model(
    route_key: str,
    upstream_id: str,
    maker: str,
    display_name: str,
    *,
    description: str,
    context: int,
    output: int | None = None,
    reasoning: list[str] | None = None,
    default_reasoning: str | None = None,
    modalities: list[str] | None = None,
    output_modalities: list[str] | None = None,
    tool_calling: bool = True,
    parallel_tool_calls: bool = True,
    summaries: bool = False,
    structured: bool = True,
    resource: str | None = None,
    kind: str = "chat",
    agent_compatible: bool = True,
    availability: str = "current",
    confidence: str = "documented",
    source: str,
    inventory: str | None = None,
    input_cost: float | None = None,
    output_cost: float | None = None,
    cached_input_cost: float | None = None,
    cache_write_input_cost: float | None = None,
    unit_cost: float | None = None,
) -> dict[str, Any]:
    values: dict[str, Any] = {
        "route": route_key,
        "upstream_id": upstream_id,
        "maker": maker,
        "display_name": display_name,
        "description": description,
        "kind": kind,
        "agent_compatible": agent_compatible,
        "context_window": context,
        "reasoning_levels": reasoning or (["none"] if not agent_compatible else ["high"]),
        "default_reasoning": default_reasoning
        or ("none" if not agent_compatible else (reasoning or ["high"])[-1]),
        "modalities": modalities or ["text"],
        "output_modalities": output_modalities or ["text"],
        "tool_calling": tool_calling,
        "parallel_tool_calls": parallel_tool_calls,
        "supports_reasoning_summaries": summaries,
        "structured_output": structured,
        "availability": availability,
        "capability_confidence": confidence,
        "source": source,
    }
    if output is not None:
        values["max_output_tokens"] = output
    if resource:
        values["resource_group"] = resource
    if inventory:
        values["inventory"] = inventory
    if input_cost is not None:
        values["input_cost_per_million"] = input_cost
    if output_cost is not None:
        values["output_cost_per_million"] = output_cost
    if cached_input_cost is not None:
        values["cached_input_cost_per_million"] = cached_input_cost
    if cache_write_input_cost is not None:
        values["cache_write_input_cost_per_million"] = cache_write_input_cost
    if unit_cost is not None:
        values["unit_cost_usd"] = unit_cost
    return values


routes: dict[str, dict[str, Any]] = {
    "zai_coding_responses": route(
        driver="switchyard",
        name="Z.AI Coding Plan Responses",
        api_operator="zai",
        access_product="zai_coding_plan",
        wire_protocol="openai_responses",
        billing_mode="subscription",
        base_url="https://api.z.ai/api/v1",
        credential_envs=["ZAI_CODING_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="zai_coding_plan",
        inventory="zai-coding-plan",
    ),
    "zai_coding_openai_chat": route(
        driver="switchyard",
        name="Z.AI Coding Plan OpenAI-compatible",
        api_operator="zai",
        access_product="zai_coding_plan",
        wire_protocol="openai_chat",
        billing_mode="subscription",
        base_url="https://api.z.ai/api/coding/paas/v4",
        credential_envs=["ZAI_CODING_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="zai_coding_plan",
        inventory="zai-coding-plan",
    ),
    "zai_coding_anthropic_messages": route(
        driver="catalog_only",
        name="Z.AI Coding Plan Anthropic-compatible (catalog only)",
        api_operator="zai",
        access_product="zai_coding_plan",
        wire_protocol="anthropic_messages",
        billing_mode="subscription",
        base_url="https://api.z.ai/api/anthropic",
        credential_envs=["ZAI_CODING_API_KEY"],
        auth="bearer",
        transport_modalities=["text"],
        transport_output_modalities=["text"],
        tool_calling=False,
        parallel_tool_calls=False,
        resource_group="zai_coding_plan",
        inventory="zai-coding-plan",
    ),
    "zai_general_openai_chat": route(
        driver="switchyard",
        name="Z.AI General API",
        api_operator="zai",
        access_product="zai_general_api",
        wire_protocol="openai_chat",
        billing_mode="api",
        base_url="https://api.z.ai/api/paas/v4",
        credential_envs=["ZAI_API_KEY"],
        max_retries=1,
        transport_modalities=["text", "image", "video", "file"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="zai_api",
        inventory="zai-api",
    ),
    "zai_general_catalog": route(
        driver="catalog_only",
        name="Z.AI media and specialist API catalog",
        api_operator="zai",
        access_product="zai_general_api",
        wire_protocol="catalog_only",
        billing_mode="catalog_only",
        transport_modalities=["text", "image", "video", "audio", "file"],
        tool_calling=False,
        parallel_tool_calls=False,
        resource_group="zai_api",
        inventory="zai-api",
    ),
    "opencode_zen_openai_chat": route(
        driver="switchyard",
        name="OpenCode Zen Chat Completions",
        api_operator="opencode",
        access_product="opencode_zen",
        wire_protocol="openai_chat",
        billing_mode="api",
        base_url="https://opencode.ai/zen/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="opencode_zen",
        inventory="opencode-zen",
    ),
    "opencode_zen_responses": route(
        driver="switchyard",
        name="OpenCode Zen Responses",
        api_operator="opencode",
        access_product="opencode_zen",
        wire_protocol="openai_responses",
        billing_mode="api",
        base_url="https://opencode.ai/zen/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="opencode_zen",
        inventory="opencode-zen",
    ),
    "opencode_zen_anthropic_messages": route(
        driver="switchyard",
        name="OpenCode Zen Anthropic Messages",
        api_operator="opencode",
        access_product="opencode_zen",
        wire_protocol="anthropic_messages",
        billing_mode="api",
        base_url="https://opencode.ai/zen/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="opencode_zen",
        inventory="opencode-zen",
    ),
    "opencode_zen_google_catalog": route(
        driver="catalog_only",
        name="OpenCode Zen Google-native models (catalog only)",
        api_operator="opencode",
        access_product="opencode_zen",
        wire_protocol="catalog_only",
        billing_mode="catalog_only",
        transport_modalities=["text", "image", "video", "audio", "file"],
        transport_output_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="opencode_zen",
        inventory="opencode-zen",
    ),
    # OpenCode Go uses different endpoints for different model families.
    "opencode_go_openai_chat": route(
        driver="switchyard",
        name="OpenCode Go Chat Completions",
        api_operator="opencode",
        access_product="opencode_go",
        wire_protocol="openai_chat",
        billing_mode="subscription",
        base_url="https://opencode.ai/zen/go/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="opencode_go",
        inventory="opencode-go",
    ),
    "opencode_go_responses": route(
        driver="switchyard",
        name="OpenCode Go Responses",
        api_operator="opencode",
        access_product="opencode_go",
        wire_protocol="openai_responses",
        billing_mode="subscription",
        base_url="https://opencode.ai/zen/go/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="opencode_go",
        inventory="opencode-go",
    ),
    "opencode_go_anthropic_messages": route(
        driver="switchyard",
        name="OpenCode Go Anthropic Messages",
        api_operator="opencode",
        access_product="opencode_go",
        wire_protocol="anthropic_messages",
        billing_mode="subscription",
        base_url="https://opencode.ai/zen/go/v1",
        credential_envs=["OPENCODE_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="opencode_go",
        inventory="opencode-go",
    ),
    "openrouter_openai_chat": route(
        driver="switchyard",
        name="OpenRouter Chat Completions",
        api_operator="openrouter",
        access_product="openrouter_api",
        wire_protocol="openai_chat",
        billing_mode="api",
        base_url="https://openrouter.ai/api/v1",
        credential_envs=["OPENROUTER_API_KEY"],
        extra_headers={"X-OpenRouter-Metadata": "enabled"},
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="openrouter",
        inventory="openrouter",
    ),
    "llama_cpp_local_openai_chat": route(
        driver="switchyard",
        name="Local llama.cpp",
        api_operator="local",
        access_product="llama_cpp",
        wire_protocol="openai_chat",
        billing_mode="local",
        base_url="http://127.0.0.1:8001/v1",
        max_retries=0,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="local_gpu_0",
    ),
    "codex_chatgpt_builtin": route(
        driver="codex_builtin",
        name="Built-in Codex with ChatGPT authentication",
        api_operator="openai",
        access_product="chatgpt_codex",
        wire_protocol="codex_builtin",
        billing_mode="chatgpt_subscription",
        provider_id="openai",
        auth="chatgpt",
        transport_modalities=["text", "image"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="chatgpt_subscription",
        inventory="openai-codex",
    ),
    "openai_api_responses": route(
        driver="switchyard",
        name="OpenAI API through Switchyard",
        api_operator="openai",
        access_product="openai_api",
        wire_protocol="openai_responses",
        billing_mode="api",
        base_url="https://api.openai.com/v1",
        credential_envs=["OPENAI_API_KEY"],
        max_retries=1,
        transport_modalities=["text", "image"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="openai_api",
    ),
    "anthropic_api_messages": route(
        driver="switchyard",
        name="Anthropic API through Switchyard",
        api_operator="anthropic",
        access_product="anthropic_api",
        wire_protocol="anthropic_messages",
        billing_mode="api",
        base_url="https://api.anthropic.com/v1",
        credential_envs=["ANTHROPIC_API_KEY"],
        max_retries=1,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=True,
        resource_group="anthropic_api",
    ),
    "ollama_local_openai_chat": route(
        driver="switchyard",
        name="Local Ollama through Switchyard",
        api_operator="local",
        access_product="ollama",
        wire_protocol="openai_chat",
        billing_mode="local",
        base_url="http://127.0.0.1:11434/v1",
        max_retries=0,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="local_gpu_0",
    ),
    "lmstudio_local_openai_chat": route(
        driver="switchyard",
        name="Local LM Studio through Switchyard",
        api_operator="local",
        access_product="lmstudio",
        wire_protocol="openai_chat",
        billing_mode="local",
        base_url="http://127.0.0.1:1234/v1",
        max_retries=0,
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="local_gpu_0",
    ),
    "ollama_codex_oss": route(
        driver="codex_oss",
        name="Codex native Ollama OSS mode",
        api_operator="local",
        access_product="ollama",
        wire_protocol="codex_oss",
        billing_mode="local",
        provider_id="ollama",
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="local_gpu_0",
    ),
    "lmstudio_codex_oss": route(
        driver="codex_oss",
        name="Codex native LM Studio OSS mode",
        api_operator="local",
        access_product="lmstudio",
        wire_protocol="codex_oss",
        billing_mode="local",
        provider_id="lmstudio",
        transport_modalities=["text"],
        tool_calling=True,
        parallel_tool_calls=False,
        resource_group="local_gpu_0",
    ),
}

resources: dict[str, dict[str, Any]] = {
    "opencode_zen": {
        "description": "OpenCode Zen request capacity",
        "lock_key": "provider:opencode-zen",
        "max_active": 8,
    },
    "zai_coding_plan": {
        "description": "Z.AI Coding Plan request capacity",
        "lock_key": "provider:zai-coding-plan",
        "max_active": 4,
    },
    "zai_api": {
        "description": "Z.AI general API request capacity",
        "lock_key": "provider:zai-api",
        "max_active": 8,
    },
    "opencode_go": {
        "description": "OpenCode Go request capacity",
        "lock_key": "provider:opencode-go",
        "max_active": 6,
    },
    "openrouter": {
        "description": "OpenRouter request capacity",
        "lock_key": "provider:openrouter",
        "max_active": 8,
    },
    "local_gpu_0": {
        "description": "One local GPU/model-server slot",
        "lock_key": "local-gpu:0",
        "max_active": 1,
    },
    "chatgpt_subscription": {
        "description": "Built-in ChatGPT/Codex account concurrency",
        "lock_key": "provider:chatgpt",
        "max_active": 4,
    },
    "openai_api": {
        "description": "OpenAI API concurrency",
        "lock_key": "provider:openai-api",
        "max_active": 8,
    },
    "anthropic_api": {
        "description": "Anthropic API concurrency",
        "lock_key": "provider:anthropic-api",
        "max_active": 8,
    },
}

models: dict[str, dict[str, Any]] = {}
binding_keys: dict[tuple[str, str], str] = {}
inventory_snapshots = load_inventory_snapshots(INVENTORY_SNAPSHOT_ROOT)
inventory_as_of = max(str(snapshot["as_of"]) for snapshot in inventory_snapshots)
for snapshot in inventory_snapshots:
    inventory_id = str(snapshot["inventory"])
    for key, record in snapshot["models"].items():
        if key in models:
            raise ValueError(f"duplicate catalog model key across inventory snapshots: {key}")
        catalog_record = record["catalog"]
        if catalog_record.get("inventory") != inventory_id:
            raise ValueError(f"snapshot {inventory_id} model {key} has mismatched inventory")
        binding = (str(catalog_record["route"]), str(catalog_record["upstream_id"]))
        if binding in binding_keys:
            raise ValueError(
                f"duplicate route/upstream binding across inventory snapshots: "
                f"{binding_keys[binding]!r} and {key!r} both select {binding!r}"
            )
        binding_keys[binding] = key
        models[key] = dict(catalog_record)

# Project-local deployments are curated configuration, not upstream inventory.
models["llama_cpp_local_openai_chat__qwen3_5_9b"] = model(
    "llama_cpp_local_openai_chat",
    "qwen3.5-9b",
    "qwen",
    "Qwen3.5-9B local",
    description="Project-capped text-only 32K/8K local evidence deployment; the upstream model is natively 262K and multimodal",
    context=32_768,
    output=8_192,
    reasoning=["none"],
    default_reasoning="none",
    parallel_tool_calls=False,
    summaries=False,
    structured=False,
    resource="local_gpu_0",
    source="qwen35-model-card",
    confidence="project-capped-deployment",
)

catalog = {
    "schema_version": MMO_SCHEMA_VERSION,
    "routes": routes,
    "models": models,
    "resources": resources,
}
validate_catalog_data(catalog, label="generated catalog")

inventory: dict[str, Any] = {
    "schema_version": MMO_SCHEMA_VERSION,
    "as_of": inventory_as_of,
    "sources": {
        "openai-api-models": "https://developers.openai.com/api/docs/models",
        "qwen35-model-card": "https://huggingface.co/Qwen/Qwen3.5-9B",
        "llama-cpp-server": "https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md",
    },
    "inventories": {},
}
for snapshot in inventory_snapshots:
    inventory_id = str(snapshot["inventory"])
    for source_id, source_url in snapshot["sources"].items():
        existing = inventory["sources"].get(source_id)
        if existing is not None and existing != source_url:
            raise ValueError(f"conflicting URL for inventory source {source_id}")
        inventory["sources"][source_id] = source_url
    catalog_keys = sorted(snapshot["models"])
    model_ids = sorted(
        {str(snapshot["models"][key]["catalog"]["upstream_id"]) for key in catalog_keys}
    )
    entry: dict[str, Any] = {
        "as_of": snapshot["as_of"],
        "snapshot": f"config/inventory-snapshots/{inventory_id}.json",
        "adapter": snapshot["adapter"],
        "fingerprint_fields": snapshot["fingerprint_fields"],
        "models_sha256": snapshot["models_sha256"],
        "dynamic": snapshot["dynamic"],
        "expected_count": len(model_ids),
        "models": model_ids,
        "catalog_keys": catalog_keys,
    }
    entry.update(snapshot["discovery"])
    if snapshot["captures"]:
        entry["captures"] = snapshot["captures"]
    inventory["inventories"][inventory_id] = entry

HEADER = f"""# Generated by scripts/generate_catalog.py. Do not edit this file directly.\n# Add local routes/models under ~/.config/codex-mmo/catalog.d/*.toml.\n# Inventory baseline: config/upstream-inventory.json (as of {inventory_as_of}).\n\n"""


def _rendered_outputs() -> dict[Path, str]:
    return {
        ROOT / "config" / "catalog.toml": HEADER + toml_dumps(catalog),
        ROOT / "config" / "upstream-inventory.json": json.dumps(
            inventory, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False
        )
        + "\n",
    }


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Generate the bundled model catalog")
    parser.add_argument(
        "--check",
        action="store_true",
        help="fail if generated files differ without modifying the source tree",
    )
    args = parser.parse_args(argv)
    outputs = _rendered_outputs()
    changed = [
        path.relative_to(ROOT).as_posix()
        for path, expected in outputs.items()
        if not path.is_file() or path.read_text(encoding="utf-8") != expected
    ]
    if not args.check:
        atomic_write_text(
            ROOT / "config" / "catalog.toml",
            outputs[ROOT / "config" / "catalog.toml"],
            0o644,
        )
        atomic_write_json(ROOT / "config" / "upstream-inventory.json", inventory, 0o644)
    print(
        json.dumps(
            {
                "routes": len(routes),
                "models": len(models),
                "resources": len(resources),
                "inventory_models": {
                    key: len(value["models"])
                    for key, value in sorted(inventory["inventories"].items())
                },
                "changed": changed,
                "passed": not changed if args.check else True,
            },
            indent=2,
            sort_keys=True,
            allow_nan=False,
        )
    )
    return 1 if args.check and changed else 0


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