#!/usr/bin/env python3
"""Catalog inspection, inventory verification, and safe model discovery.

The bundled catalog is a versioned capability baseline. Discovery never guesses
an execution protocol for an unknown remote model. Codex account discovery can
optionally create a conservative user overlay because Codex itself provides the
route and wire path for built-in models.
"""

from __future__ import annotations

import contextlib
import datetime as dt
import hashlib
import json
import os
import shutil
import subprocess
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

from mmo_catalog_data import user_catalog_root, validate_model_entry, validated_global_catalog
from mmo_inventory_snapshot import (
    DISCOVERY_FIELDS,
    FULL_FINGERPRINT_FIELDS,
    OPENCODE_GO_SOURCE_IDS,
    OPENCODE_ZEN_SOURCE_IDS,
    build_inventory_snapshot,
    build_opencode_go_snapshot,
    build_opencode_zen_snapshot,
    build_openrouter_snapshot,
    codex_runtime_evidence,
    compare_inventory_fingerprints,
    load_inventory_snapshots,
    route_catalog_key,
)
from mmo_profiles import resolve_profile
from mmo_util import (
    atomic_write_json,
    atomic_write_text,
    config_root,
    filtered_environment,
    install_root,
    parse_env_file,
    read_json,
    read_json_object,
    strict_json_loads,
    toml_dumps,
    utc_now,
    valid_absolute_uri,
)
from mmo_version import MMO_SCHEMA_VERSION, PACKAGE_VERSION

CODEX_REASONING_VALUES = {"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"}


def _inventory_endpoint(inventory: str) -> str:
    entry = inventory_baseline().get("inventories", {}).get(inventory, {})
    endpoint = entry.get("endpoint")
    if not isinstance(endpoint, str) or not endpoint:
        raise RuntimeError(f"inventory {inventory!r} has no declarative discovery endpoint")
    return endpoint


def catalog_data(profile: str | None = None) -> dict[str, Any]:
    if profile:
        resolved = resolve_profile(profile)
        return {
            "routes": resolved["routes"],
            "models": resolved["models"],
            "resources": resolved["resources"],
        }
    return validated_global_catalog()


def _matches_query(key: str, value: Mapping[str, Any], query: str | None) -> bool:
    if not query:
        return True
    needle = query.casefold()
    fields = [
        key,
        str(value.get("upstream_id", "")),
        str(value.get("display_name", "")),
        str(value.get("description", "")),
        str(value.get("route", "")),
        str(value.get("maker", "")),
        str(value.get("inventory", "")),
    ]
    return any(needle in field.casefold() for field in fields)


def list_models(
    *,
    profile: str | None = None,
    route: str | None = None,
    inventory: str | None = None,
    query: str | None = None,
    agent_compatible: bool | None = None,
    availability: str | None = None,
) -> dict[str, dict[str, Any]]:
    models = catalog_data(profile)["models"]
    result: dict[str, dict[str, Any]] = {}
    for key, value in sorted(models.items()):
        if route and value.get("route") != route:
            continue
        if inventory and value.get("inventory") != inventory:
            continue
        if agent_compatible is not None and bool(value.get("agent_compatible")) != agent_compatible:
            continue
        if availability and value.get("availability") != availability:
            continue
        if not _matches_query(key, value, query):
            continue
        result[key] = dict(value)
    return result


def find_model(key: str, *, profile: str | None = None) -> dict[str, Any]:
    models = catalog_data(profile)["models"]
    if key not in models:
        raise FileNotFoundError(f"unknown route-qualified catalog model key: {key}")
    return {"key": key, **models[key]}


def catalog_summary(profile: str | None = None) -> dict[str, Any]:
    data = catalog_data(profile)
    models = data["models"]
    by_inventory: dict[str, int] = {}
    by_kind: dict[str, int] = {}
    by_driver: dict[str, int] = {}
    by_maker: dict[str, int] = {}
    by_operator: dict[str, int] = {}
    for model in models.values():
        inventory = str(model.get("inventory") or "unscoped")
        by_inventory[inventory] = by_inventory.get(inventory, 0) + 1
        kind = str(model.get("kind") or "chat")
        by_kind[kind] = by_kind.get(kind, 0) + 1
        route = data["routes"][model["route"]]
        driver = str(route["driver"])
        by_driver[driver] = by_driver.get(driver, 0) + 1
        maker = str(model["maker"])
        by_maker[maker] = by_maker.get(maker, 0) + 1
        operator = str(route["api_operator"])
        by_operator[operator] = by_operator.get(operator, 0) + 1
    return {
        "profile": profile,
        "routes": len(data["routes"]),
        "models": len(models),
        "agent_compatible_models": sum(
            bool(item.get("agent_compatible")) for item in models.values()
        ),
        "resources": len(data["resources"]),
        "models_by_inventory": dict(sorted(by_inventory.items())),
        "models_by_kind": dict(sorted(by_kind.items())),
        "models_by_driver": dict(sorted(by_driver.items())),
        "models_by_maker": dict(sorted(by_maker.items())),
        "models_by_api_operator": dict(sorted(by_operator.items())),
    }


def inventory_baseline() -> dict[str, Any]:
    return read_json_object(
        install_root() / "config" / "upstream-inventory.json",
        label="inventory baseline",
    )


def local_inventory_report() -> dict[str, Any]:
    baseline = inventory_baseline()
    catalog = validated_global_catalog()
    snapshot_errors: list[str] = []
    snapshot_map: dict[str, dict[str, Any]] = {}
    try:
        snapshot_map = {
            str(snapshot["inventory"]): snapshot
            for snapshot in load_inventory_snapshots(
                install_root() / "config" / "inventory-snapshots"
            )
        }
    except (OSError, ValueError) as exc:
        snapshot_errors.append(f"{type(exc).__name__}: {exc}")
    report: dict[str, Any] = {
        "as_of": baseline.get("as_of"),
        "sources": baseline.get("sources", {}),
        "inventories": {},
        "snapshot_errors": snapshot_errors,
        "passed": not snapshot_errors,
    }
    for inventory_id, expected in sorted(baseline.get("inventories", {}).items()):
        expected_ids = set(expected.get("models", []))
        catalog_items = {
            key: model
            for key, model in catalog["models"].items()
            if model.get("inventory") == inventory_id
        }
        actual_ids = {str(model["upstream_id"]) for model in catalog_items.values()}
        missing = sorted(expected_ids - actual_ids)
        extra = sorted(actual_ids - expected_ids)
        expected_keys = set(expected.get("catalog_keys", []))
        actual_keys = set(catalog_items)
        missing_keys = sorted(expected_keys - actual_keys)
        extra_keys = sorted(actual_keys - expected_keys)
        expected_count = expected.get("expected_count")
        count_ok = expected_count is None or len(actual_ids) == int(expected_count)
        snapshot = snapshot_map.get(inventory_id)
        record_mismatches: list[str] = []
        if snapshot:
            for key in sorted(set(catalog_items) & set(snapshot["models"])):
                actual_record = catalog_items[key]
                snapshot_record = snapshot["models"][key]["catalog"]
                normalized_snapshot_record = validate_model_entry(
                    key, snapshot_record, catalog["routes"]
                )
                if actual_record != normalized_snapshot_record:
                    record_mismatches.append(key)
        expected_discovery = {key: expected[key] for key in DISCOVERY_FIELDS if key in expected}
        snapshot_ok = bool(
            snapshot
            and snapshot.get("models_sha256") == expected.get("models_sha256")
            and snapshot.get("as_of") == expected.get("as_of")
            and snapshot.get("dynamic") == expected.get("dynamic")
            and snapshot.get("adapter") == expected.get("adapter")
            and snapshot.get("fingerprint_fields") == expected.get("fingerprint_fields")
            and snapshot.get("captures", []) == expected.get("captures", [])
            and snapshot.get("discovery") == expected_discovery
        )
        passed = (
            not missing
            and not extra
            and not missing_keys
            and not extra_keys
            and not record_mismatches
            and count_ok
            and snapshot_ok
        )
        report["inventories"][inventory_id] = {
            "passed": passed,
            "dynamic": bool(expected.get("dynamic", False)),
            "expected_count": expected_count,
            "actual_count": len(actual_ids),
            "missing": missing,
            "extra": extra,
            "missing_catalog_keys": missing_keys,
            "extra_catalog_keys": extra_keys,
            "catalog_record_mismatches": record_mismatches,
            "snapshot": expected.get("snapshot"),
            "models_sha256": expected.get("models_sha256"),
            "snapshot_ok": snapshot_ok,
            "catalog_keys": sorted(catalog_items),
        }
        report["passed"] = report["passed"] and passed
    known_sources = set(baseline.get("sources", {}))
    source_errors: list[str] = []
    for model_key, model in sorted(catalog["models"].items()):
        if model.get("inventory") not in baseline.get("inventories", {}):
            continue
        for field in ("source", "availability_source", "capability_source", "pricing_source"):
            source = model.get(field)
            if source is not None and source not in known_sources:
                source_errors.append(f"{model_key}.{field}: unknown source {source!r}")
    report["source_errors"] = source_errors
    untracked_snapshots = sorted(set(snapshot_map) - set(baseline.get("inventories", {})))
    report["untracked_snapshots"] = untracked_snapshots
    report["passed"] = report["passed"] and not source_errors and not untracked_snapshots
    return report


def _http_bytes(
    url: str,
    timeout: float,
    *,
    headers: Mapping[str, str] | None = None,
    accept: str = "application/json",
) -> bytes:
    if not isinstance(url, str) or not valid_absolute_uri(url):
        raise ValueError("catalog discovery URL must be an absolute HTTP(S) URL")
    try:
        parsed = urllib.parse.urlsplit(url)
        _ = parsed.port
    except ValueError as exc:
        raise ValueError("catalog discovery URL must be an absolute HTTP(S) URL") from exc
    if (
        parsed.scheme.lower() not in {"http", "https"}
        or not parsed.hostname
        or parsed.username is not None
        or parsed.password is not None
        or parsed.fragment
    ):
        raise ValueError("catalog discovery URL must be an absolute HTTP(S) URL")
    request_headers = {
        "Accept": accept,
        "User-Agent": f"codex-mmo/{PACKAGE_VERSION} catalog-discovery",
    }
    request_headers.update(dict(headers or {}))
    request = urllib.request.Request(url, headers=request_headers)
    opener = urllib.request.build_opener(_SameOriginRedirectHandler())
    with opener.open(request, timeout=timeout) as response:
        return response.read()


def _http_json(
    url: str,
    timeout: float,
    *,
    headers: Mapping[str, str] | None = None,
) -> Any:
    return strict_json_loads(_http_bytes(url, timeout, headers=headers).decode("utf-8"))


class _SameOriginRedirectHandler(urllib.request.HTTPRedirectHandler):
    """Permit discovery redirects without forwarding credentials off-origin."""

    @staticmethod
    def _origin(url: str) -> tuple[str, str, int | None]:
        parsed = urllib.parse.urlsplit(url)
        port = parsed.port
        if port is None:
            port = 443 if parsed.scheme.lower() == "https" else 80
        return parsed.scheme.lower(), (parsed.hostname or "").lower(), port

    def redirect_request(
        self,
        req: urllib.request.Request,
        fp: Any,
        code: int,
        msg: str,
        headers: Any,
        newurl: str,
    ) -> urllib.request.Request | None:
        if self._origin(req.full_url) != self._origin(newurl):
            with contextlib.suppress(Exception):
                fp.close()
            raise urllib.error.URLError(
                f"cross-origin discovery redirect rejected ({code}): {newurl}"
            )
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def _model_rows(document: Any) -> list[dict[str, Any]]:
    if isinstance(document, list):
        rows = document
    elif isinstance(document, Mapping):
        rows = document.get("data") or document.get("models") or []
    else:
        rows = []
    return [dict(item) for item in rows if isinstance(item, Mapping)]


def _model_slug(item: Mapping[str, Any]) -> str | None:
    for key in ("id", "slug", "model", "name"):
        value = item.get(key)
        if isinstance(value, str) and value:
            return value
    return None


def discover_opencode_go(*, url: str | None = None, timeout: float = 10.0) -> dict[str, Any]:
    target = url or os.environ.get("MMO_OPENCODE_MODELS_URL") or _inventory_endpoint("opencode-go")
    started = utc_now()
    try:
        document = _http_json(target, timeout)
        rows = _model_rows(document)
        ids = sorted({slug for item in rows if (slug := _model_slug(item))})
        return {
            "inventory": "opencode-go",
            "url": target,
            "observed_at": started,
            "passed": bool(ids),
            "models": ids,
            "count": len(ids),
            "raw_metadata": rows,
        }
    except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
        return {
            "inventory": "opencode-go",
            "url": target,
            "observed_at": started,
            "passed": False,
            "models": [],
            "count": 0,
            "error": f"{type(exc).__name__}: {exc}",
        }


def discover_opencode_zen(*, url: str | None = None, timeout: float = 10.0) -> dict[str, Any]:
    """Discover the public OpenCode Zen availability listing."""

    target = (
        url or os.environ.get("MMO_OPENCODE_ZEN_MODELS_URL") or _inventory_endpoint("opencode-zen")
    )
    started = utc_now()
    try:
        document = _http_json(target, timeout)
        rows = _model_rows(document)
        ids = sorted({slug for item in rows if (slug := _model_slug(item))})
        return {
            "inventory": "opencode-zen",
            "url": target,
            "observed_at": started,
            "passed": bool(ids),
            "models": ids,
            "count": len(ids),
            "raw_metadata": rows,
        }
    except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
        return {
            "inventory": "opencode-zen",
            "url": target,
            "observed_at": started,
            "passed": False,
            "models": [],
            "count": 0,
            "error": f"{type(exc).__name__}: {exc}",
        }


def _catalog_credentials() -> dict[str, str]:
    values = parse_env_file(config_root() / "credentials.env")
    values.update({key: value for key, value in os.environ.items() if value})
    return values


def discover_openrouter(
    *,
    url: str | None = None,
    api_key: str | None = None,
    timeout: float = 10.0,
) -> dict[str, Any]:
    """Discover OpenRouter's public model inventory, authenticating when available."""

    official_target = _inventory_endpoint("openrouter")
    target = url or os.environ.get("MMO_OPENROUTER_MODELS_URL") or official_target
    # A configured credential is sent implicitly only to the reviewed official
    # endpoint. Callers that deliberately target another URL must explicitly
    # provide its credential, preventing URL overrides from exfiltrating the
    # user's OpenRouter key.
    configured_token = (
        _catalog_credentials().get("OPENROUTER_API_KEY")
        if api_key is None and target == official_target
        else None
    )
    token = (
        api_key
        if api_key is not None
        else (configured_token if target == official_target else None)
    )
    headers = {"Authorization": f"Bearer {token}"} if token else None
    started = utc_now()
    try:
        document = _http_json(target, timeout, headers=headers)
        if not isinstance(document, Mapping):
            raise ValueError("OpenRouter model listing must be an object")
        rows = _model_rows(document)
        if "total_count" not in document or "links" not in document:
            raise ValueError("OpenRouter model listing lacks required pagination metadata")
        total_count = document["total_count"]
        if (
            not isinstance(total_count, int)
            or isinstance(total_count, bool)
            or total_count != len(rows)
        ):
            raise ValueError(
                "OpenRouter model listing is paginated or incomplete: "
                f"total_count={total_count!r}, rows={len(rows)}"
            )
        links = document["links"]
        if not isinstance(links, Mapping) or "next" not in links:
            raise ValueError("OpenRouter model listing has invalid pagination links")
        if links["next"] not in (None, ""):
            raise ValueError("OpenRouter model listing has an unconsumed next page")
        ids = sorted({slug for item in rows if (slug := _model_slug(item))})
        return {
            "inventory": "openrouter",
            "url": target,
            "observed_at": started,
            "passed": bool(ids),
            "authenticated_request": bool(token),
            "models": ids,
            "count": len(ids),
            "raw_metadata": rows,
        }
    except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
        return {
            "inventory": "openrouter",
            "url": target,
            "observed_at": started,
            "passed": False,
            "authenticated_request": bool(token),
            "models": [],
            "count": 0,
            "error": f"{type(exc).__name__}: {exc}",
        }


def discover_zai(
    inventory: str,
    *,
    url: str | None = None,
    api_key: str | None = None,
    timeout: float = 10.0,
) -> dict[str, Any]:
    """Discover model IDs advertised by one Z.AI OpenAI-compatible endpoint.

    Z.AI's complete catalog also includes media and hosted-agent services that
    are not necessarily returned by ``/models``. Those remain represented by
    the versioned documentation inventory. Live discovery is used to detect
    newly advertised coding/chat IDs without guessing capabilities for unknown
    services.
    """

    if inventory not in {"zai-api", "zai-coding-plan"}:
        raise ValueError("inventory must be zai-api or zai-coding-plan")
    if inventory == "zai-api":
        official_target = _inventory_endpoint("zai-api")
        target = url or os.environ.get("MMO_ZAI_MODELS_URL") or official_target
        credential_names = ["ZAI_API_KEY"]
    else:
        official_target = _inventory_endpoint("zai-coding-plan")
        target = url or os.environ.get("MMO_ZAI_CODING_MODELS_URL") or official_target
        credential_names = ["ZAI_CODING_API_KEY"]
    values = _catalog_credentials()
    configured_token = next(
        (values.get(name) for name in credential_names if values.get(name)), None
    )
    # Never send a configured Z.AI credential to an operator-overridden URL.
    # Custom endpoints require an explicit credential, matching the discovery
    # boundary used for OpenRouter above. An explicit blank disables implicit
    # authentication even for the official endpoint.
    token = (
        api_key
        if api_key is not None
        else (configured_token if target == official_target else None)
    )
    started = utc_now()
    if not token:
        return {
            "inventory": inventory,
            "url": target,
            "observed_at": started,
            "passed": False,
            "skipped": True,
            "authenticated_request": False,
            "models": [],
            "count": 0,
            "error": "missing credential: " + "/".join(credential_names),
        }
    try:
        document = _http_json(
            target,
            timeout,
            headers={"Authorization": f"Bearer {token}"},
        )
        rows = _model_rows(document)
        ids = sorted({slug for item in rows if (slug := _model_slug(item))})
        return {
            "inventory": inventory,
            "url": target,
            "observed_at": started,
            "passed": bool(ids),
            "skipped": False,
            "authenticated_request": True,
            "models": ids,
            "count": len(ids),
            "raw_metadata": rows,
        }
    except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
        return {
            "inventory": inventory,
            "url": target,
            "observed_at": started,
            "passed": False,
            "skipped": False,
            "authenticated_request": True,
            "models": [],
            "count": 0,
            "error": f"{type(exc).__name__}: {exc}",
        }


def _load_json_output(result: subprocess.CompletedProcess[str]) -> Any | None:
    text = result.stdout.strip()
    if not text:
        return None
    with contextlib.suppress(json.JSONDecodeError, ValueError):
        return strict_json_loads(text)
    # Some Codex versions print a short informational line before the JSON.
    for offset, char in enumerate(text):
        if char not in "[{":
            continue
        with contextlib.suppress(json.JSONDecodeError, ValueError):
            return strict_json_loads(text[offset:])
    return None


def _run_codex_models(binary: str, home: Path, *, bundled: bool) -> dict[str, Any]:
    command = [binary, "debug", "models"]
    if bundled:
        command.append("--bundled")
    try:
        result = subprocess.run(
            command,
            env=filtered_environment(extra={"CODEX_HOME": str(home)}),
            text=True,
            capture_output=True,
            timeout=45,
            check=False,
        )
    except (OSError, subprocess.SubprocessError) as exc:
        return {
            "command": command,
            "passed": False,
            "models": [],
            "error": f"{type(exc).__name__}: {exc}",
        }
    document = _load_json_output(result)
    rows = _model_rows(document)
    return {
        "command": command,
        "passed": result.returncode == 0 and bool(rows),
        "exit_code": result.returncode,
        "models": rows,
        "stderr": result.stderr[-4000:],
    }


def _read_codex_cache(home: Path) -> dict[str, Any]:
    path = home / "models_cache.json"
    if not path.is_file():
        return {"path": str(path), "passed": False, "models": [], "error": "not found"}
    try:
        document = read_json(path)
        rows = _model_rows(document)
        return {
            "path": str(path),
            "passed": bool(rows),
            "models": rows,
            "client_version": document.get("client_version")
            if isinstance(document, Mapping)
            else None,
            "fetched_at": document.get("fetched_at") if isinstance(document, Mapping) else None,
        }
    except (OSError, ValueError, json.JSONDecodeError) as exc:
        return {
            "path": str(path),
            "passed": False,
            "models": [],
            "error": f"{type(exc).__name__}: {exc}",
        }


def discover_codex(
    *,
    binary: str = "codex",
    home: Path | None = None,
    include_bundled: bool = True,
) -> dict[str, Any]:
    codex_home = (home or Path(os.environ.get("CODEX_HOME", "~/.codex"))).expanduser().resolve()
    candidate = Path(binary).expanduser()
    resolved_binary = shutil.which(binary) or (
        str(candidate.resolve()) if candidate.is_file() else None
    )
    sources: dict[str, Any] = {"cache": _read_codex_cache(codex_home)}
    if resolved_binary:
        sources["authenticated"] = _run_codex_models(resolved_binary, codex_home, bundled=False)
        if include_bundled:
            sources["bundled"] = _run_codex_models(resolved_binary, codex_home, bundled=True)
    else:
        sources["authenticated"] = {
            "passed": False,
            "models": [],
            "error": f"Codex binary not found: {binary}",
        }
        if include_bundled:
            sources["bundled"] = {
                "passed": False,
                "models": [],
                "error": f"Codex binary not found: {binary}",
            }

    merged: dict[str, dict[str, Any]] = {}
    availability: dict[str, list[str]] = {}
    for source_name in ("authenticated", "bundled", "cache"):
        source = sources.get(source_name, {})
        for row in source.get("models", []):
            slug = _model_slug(row)
            if not slug:
                continue
            merged.setdefault(slug, dict(row))
            availability.setdefault(slug, []).append(source_name)
    return {
        "inventory": "openai-codex",
        "observed_at": utc_now(),
        "codex_home": str(codex_home),
        "codex_binary": resolved_binary,
        "passed": bool(merged),
        "models": sorted(merged),
        "count": len(merged),
        "availability_sources": {key: sorted(value) for key, value in sorted(availability.items())},
        "metadata": merged,
        "sources": sources,
    }


def _compare(observed: Sequence[str], expected: Sequence[str]) -> dict[str, Any]:
    observed_set = set(observed)
    expected_set = set(expected)
    return {
        "observed_count": len(observed_set),
        "expected_count": len(expected_set),
        "missing_from_observed": sorted(expected_set - observed_set),
        "unknown_to_catalog": sorted(observed_set - expected_set),
        "exact": observed_set == expected_set,
    }


def _snapshot_map() -> dict[str, dict[str, Any]]:
    return {
        str(snapshot["inventory"]): snapshot
        for snapshot in load_inventory_snapshots(install_root() / "config" / "inventory-snapshots")
    }


def _source_bytes(
    snapshot: Mapping[str, Any], source: str, timeout: float, *, override: str | None = None
) -> tuple[str, bytes]:
    sources = snapshot.get("sources", {})
    if not isinstance(sources, Mapping) or not isinstance(sources.get(source), str):
        raise ValueError(f"inventory snapshot lacks source {source!r}")
    url = override or str(sources[source])
    accept = "text/plain" if source.endswith("docs-source") else "application/json"
    return url, _http_bytes(url, timeout, accept=accept)


def _build_observed_public_snapshot(
    inventory: str,
    expected: Mapping[str, Any],
    *,
    timeout: float,
    endpoint_override: str | None,
) -> dict[str, Any]:
    """Rebuild a dynamic public inventory with its declared source adapter."""

    adapter = expected.get("adapter")
    observed_at = dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
    if adapter == "openrouter_models_api":
        source_url, raw = _source_bytes(
            expected, "openrouter-models-api", timeout, override=endpoint_override
        )
        zdr_source_url, zdr_raw = _source_bytes(expected, "openrouter-zdr-endpoints", timeout)
        return build_openrouter_snapshot(
            strict_json_loads(raw),
            strict_json_loads(zdr_raw),
            as_of=str(expected["as_of"]),
            retrieved_at=observed_at,
            response_sha256=hashlib.sha256(raw).hexdigest(),
            zdr_response_sha256=hashlib.sha256(zdr_raw).hexdigest(),
            source_url=source_url,
            zdr_source_url=zdr_source_url,
            endpoint_selections=expected["discovery"]["endpoint_selections"],
        )
    if adapter not in {"opencode_go_join", "opencode_zen_join"}:
        raise ValueError(
            f"inventory {inventory!r} adapter {adapter!r} cannot produce a full live fingerprint"
        )
    if adapter == "opencode_go_join":
        prefix = "opencode-go"
        builder = build_opencode_go_snapshot
    else:
        prefix = "opencode-zen"
        builder = build_opencode_zen_snapshot
    listing_url, listing_raw = _source_bytes(
        expected, f"{prefix}-models", timeout, override=endpoint_override
    )
    models_dev_url, models_dev_raw = _source_bytes(expected, f"models-dev-{prefix}", timeout)
    docs_source_id = f"{prefix}-docs-source"
    live_docs_url = (
        OPENCODE_GO_SOURCE_IDS[docs_source_id]
        if adapter == "opencode_go_join"
        else OPENCODE_ZEN_SOURCE_IDS[docs_source_id]
    )
    # Snapshot captures remain immutable commit URLs. Live verification uses
    # the provider's current branch so a documentation/protocol change cannot
    # hide behind the reviewed capture.
    docs_url, docs_raw = _source_bytes(
        expected,
        docs_source_id,
        timeout,
        override=live_docs_url,
    )
    return builder(
        strict_json_loads(listing_raw),
        strict_json_loads(models_dev_raw),
        docs_raw.decode("utf-8"),
        as_of=str(expected["as_of"]),
        retrieved_at=observed_at,
        listing_sha256=hashlib.sha256(listing_raw).hexdigest(),
        models_dev_sha256=hashlib.sha256(models_dev_raw).hexdigest(),
        docs_sha256=hashlib.sha256(docs_raw).hexdigest(),
        listing_url=listing_url,
        models_dev_url=models_dev_url,
        docs_url=docs_url,
    )


def _id_only_fingerprint_comparison(
    observed: Sequence[str], expected_snapshot: Mapping[str, Any]
) -> dict[str, Any]:
    expected_by_id = {
        str(record["catalog"]["upstream_id"]): str(record["catalog"]["route"])
        for record in expected_snapshot["models"].values()
    }
    ids = sorted(set(observed))
    membership = _compare(ids, sorted(expected_by_id))
    incomplete = [
        {
            "route": expected_by_id[model_id],
            "upstream_id": model_id,
            "fields": list(FULL_FINGERPRINT_FIELDS[1:]),
        }
        for model_id in sorted(set(ids) & set(expected_by_id))
    ]
    return {
        **membership,
        "fingerprint_mismatches": [],
        "incomplete_evidence": incomplete,
        "verified_fields": ["upstream_id"] if not membership["unknown_to_catalog"] else [],
        "exact": False,
    }


def _verify_source_captures(snapshot: Mapping[str, Any], timeout: float) -> dict[str, Any]:
    captures: list[dict[str, Any]] = []
    for capture in snapshot.get("captures", []):
        source = str(capture["source"])
        item: dict[str, Any] = {
            "source": source,
            "expected_sha256": capture["response_sha256"],
        }
        try:
            url, raw = _source_bytes(snapshot, source, timeout)
            observed = hashlib.sha256(raw).hexdigest()
            item.update(
                {
                    "url": url,
                    "observed_sha256": observed,
                    "passed": observed == capture["response_sha256"],
                }
            )
        except (OSError, ValueError, urllib.error.URLError) as exc:
            item.update({"passed": False, "error": f"{type(exc).__name__}: {exc}"})
        captures.append(item)
    return {
        "captures": captures,
        "passed": bool(captures) and all(item["passed"] for item in captures),
    }


def _codex_fingerprint_comparison(
    discovery: Mapping[str, Any], expected: Mapping[str, Any]
) -> dict[str, Any]:
    metadata = discovery.get("metadata", {})
    if not isinstance(metadata, Mapping):
        raise ValueError("Codex discovery metadata is missing")
    records: dict[str, dict[str, Any]] = {}
    for key, expected_record in expected["models"].items():
        slug = str(expected_record["catalog"]["upstream_id"])
        row = metadata.get(slug)
        if not isinstance(row, Mapping):
            continue
        records[str(key)] = {
            "catalog": dict(expected_record["catalog"]),
            "evidence": {
                "codex_runtime": codex_runtime_evidence(row),
                "verified_fingerprint_fields": list(FULL_FINGERPRINT_FIELDS),
            },
        }
    observed = build_inventory_snapshot(
        inventory="openai-codex",
        adapter="codex_installed_models_join",
        fingerprint_fields=FULL_FINGERPRINT_FIELDS,
        as_of=str(expected["as_of"]),
        dynamic=False,
        sources=expected["sources"],
        discovery=expected["discovery"],
        captures=[],
        models=records,
    )
    return compare_inventory_fingerprints(expected, observed)


def verify_catalog(
    *,
    remote: bool = False,
    include_codex: bool = False,
    opencode_url: str | None = None,
    opencode_zen_url: str | None = None,
    openrouter_url: str | None = None,
    zai_coding_url: str | None = None,
    codex_binary: str = "codex",
    codex_home: Path | None = None,
    timeout: float = 10.0,
) -> dict[str, Any]:
    baseline = inventory_baseline()
    local = local_inventory_report()
    result: dict[str, Any] = {
        "checked_at": utc_now(),
        "local": local,
        "remote": {},
        "passed": bool(local["passed"]),
    }
    inventories = baseline["inventories"]
    snapshots = _snapshot_map()
    if remote:
        public_overrides = {
            "opencode-go": opencode_url,
            "opencode-zen": opencode_zen_url,
            "openrouter": openrouter_url,
        }
        for inventory_id, endpoint_override in public_overrides.items():
            expected_snapshot = snapshots[inventory_id]
            report: dict[str, Any] = {
                "inventory": inventory_id,
                "adapter": expected_snapshot["adapter"],
                "observed_at": utc_now(),
                "fingerprint_fields": list(expected_snapshot["fingerprint_fields"]),
            }
            try:
                observed_snapshot = _build_observed_public_snapshot(
                    inventory_id,
                    expected_snapshot,
                    timeout=timeout,
                    endpoint_override=endpoint_override,
                )
                comparison = compare_inventory_fingerprints(expected_snapshot, observed_snapshot)
                report.update(
                    {
                        "url": observed_snapshot["discovery"].get("endpoint"),
                        "models": sorted(
                            str(item["catalog"]["upstream_id"])
                            for item in observed_snapshot["models"].values()
                        ),
                        "count": len(observed_snapshot["models"]),
                        "comparison": comparison,
                        "full_fingerprint_verified": bool(comparison["exact"]),
                        "passed": bool(comparison["exact"]),
                    }
                )
            except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
                report.update(
                    {
                        "passed": False,
                        "full_fingerprint_verified": False,
                        "error": f"{type(exc).__name__}: {exc}",
                    }
                )
            result["remote"][inventory_id] = report
            result["passed"] = result["passed"] and bool(report["passed"])
        # The bundled portfolio uses the Coding Plan, not the unrelated Z.AI
        # general API product. Its authenticated model IDs establish live
        # availability while exact official Markdown captures establish the
        # reviewed capability fingerprint.
        inventory_id = "zai-coding-plan"
        zai = discover_zai(inventory_id, url=zai_coding_url, timeout=timeout)
        expected_ids = sorted(
            str(record["catalog"]["upstream_id"])
            for record in snapshots[inventory_id]["models"].values()
        )
        membership = _compare(zai.get("models", []), expected_ids)
        source_verification = _verify_source_captures(snapshots[inventory_id], timeout)
        full_verified = bool(
            zai.get("passed") and membership["exact"] and source_verification["passed"]
        )
        zai.update(
            {
                "comparison": membership,
                "source_capture_verification": source_verification,
                "fingerprint_fields": list(FULL_FINGERPRINT_FIELDS),
                "verified_fields": list(FULL_FINGERPRINT_FIELDS) if full_verified else [],
                "full_fingerprint_verified": full_verified,
            }
        )
        result["remote"][inventory_id] = zai
        result["passed"] = result["passed"] and full_verified
    if include_codex:
        codex = discover_codex(binary=codex_binary, home=codex_home)
        codex_inventory = inventories["openai-codex"]
        observed = [item for item in codex.get("models", []) if isinstance(item, str)]
        ignored_prefixes = tuple(codex_inventory.get("ignored_model_prefixes", []))
        deprecated = set(codex_inventory.get("known_deprecated_models", []))
        codex["ignored_internal_models"] = sorted(
            item for item in observed if ignored_prefixes and item.startswith(ignored_prefixes)
        )
        codex["known_deprecated_models_observed"] = sorted(set(observed) & deprecated)
        comparable = [
            item
            for item in observed
            if item not in deprecated
            and not (ignored_prefixes and item.startswith(ignored_prefixes))
        ]
        try:
            codex["comparison"] = _codex_fingerprint_comparison(codex, snapshots["openai-codex"])
        except ValueError as exc:
            codex["comparison"] = _id_only_fingerprint_comparison(
                comparable, snapshots["openai-codex"]
            )
            codex["fingerprint_error"] = f"{type(exc).__name__}: {exc}"
        codex["full_fingerprint_verified"] = bool(codex["comparison"]["exact"])
        codex["runtime_source_ok"] = any(
            bool(codex.get("sources", {}).get(source, {}).get("passed"))
            for source in ("authenticated", "bundled")
        )
        result["remote"]["openai-codex"] = codex
        result["passed"] = (
            result["passed"]
            and bool(codex["passed"])
            and bool(codex["runtime_source_ok"])
            and bool(codex["full_fingerprint_verified"])
        )
    return result


def _reasoning_levels(row: Mapping[str, Any]) -> list[str]:
    raw = row.get("supported_reasoning_levels") or row.get("supportedReasoningLevels") or []
    if not isinstance(raw, list):
        return []
    values: list[str] = []
    for item in raw:
        effort = item.get("effort") if isinstance(item, Mapping) else item
        if isinstance(effort, str) and effort in CODEX_REASONING_VALUES and effort not in values:
            values.append(effort)
    return values


def _metadata_boolean(value: Any, *, default: bool) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, int) and value in {0, 1}:
        return bool(value)
    if isinstance(value, str) and value.casefold() in {"true", "false"}:
        return value.casefold() == "true"
    return default


def build_codex_discovery_overlay(discovery: Mapping[str, Any]) -> dict[str, Any]:
    catalog = validated_global_catalog()
    known_ids = {
        str(model["upstream_id"])
        for model in catalog["models"].values()
        if model.get("inventory") == "openai-codex"
    }
    codex_inventory = inventory_baseline().get("inventories", {}).get("openai-codex", {})
    ignored_prefixes = tuple(codex_inventory.get("ignored_model_prefixes", []))
    deprecated = set(codex_inventory.get("known_deprecated_models", []))
    models: dict[str, Any] = {}
    seen_slugs: set[str] = set()
    metadata = discovery.get("metadata", {})
    if not isinstance(metadata, Mapping):
        metadata = {}
    for slug in discovery.get("models", []):
        if not isinstance(slug, str) or not slug:
            continue
        if (
            slug in seen_slugs
            or slug in known_ids
            or slug in deprecated
            or (ignored_prefixes and slug.startswith(ignored_prefixes))
        ):
            continue
        seen_slugs.add(slug)
        row = metadata.get(slug, {})
        if not isinstance(row, Mapping):
            row = {}
        safe = route_catalog_key("codex_chatgpt_builtin", slug, set(models))
        raw_context = row.get("context_window") or row.get("max_context_window")
        try:
            if isinstance(raw_context, bool) or not isinstance(raw_context, (int, str)):
                raise ValueError
            context = int(raw_context)
        except (TypeError, ValueError, OverflowError):
            continue
        if not 1024 <= context <= 20_000_000:
            continue
        levels = _reasoning_levels(row)
        if not levels:
            continue
        modalities = row.get("input_modalities") or row.get("inputModalities")
        if not isinstance(modalities, list) or not all(
            isinstance(item, str) for item in modalities
        ):
            continue
        if (
            not modalities
            or len(modalities) != len(set(modalities))
            or set(modalities) - {"text", "image", "audio", "video", "file"}
        ):
            continue
        models[safe] = {
            "maker": "openai",
            "route": "codex_chatgpt_builtin",
            "upstream_id": slug,
            "display_name": str(row.get("display_name") or row.get("displayName") or slug),
            "description": "Discovered from the local authenticated Codex model catalog",
            "kind": "vision_chat" if "image" in modalities else "chat",
            # Discovery alone never grants execution authority. An operator
            # must review the generated fragment before opting a model into an
            # agent binding.
            "agent_compatible": False,
            "context_window": context,
            "reasoning_levels": levels,
            "default_reasoning": "high" if "high" in levels else levels[-1],
            "modalities": modalities,
            "output_modalities": ["text"],
            "tool_calling": False,
            "parallel_tool_calls": False,
            "supports_reasoning_summaries": _metadata_boolean(
                row.get("supports_reasoning_summaries"), default=False
            ),
            "structured_output": False,
            "availability": "account-discovered",
            "capability_confidence": "codex-runtime",
            "source": "codex-debug-models",
            "inventory": "openai-codex-discovered",
            "resource_group": "chatgpt_subscription",
        }
    return {"schema_version": MMO_SCHEMA_VERSION, "models": models}


def refresh_discovery(
    *,
    remote: bool = True,
    include_codex: bool = True,
    install_codex_overlay: bool = False,
    opencode_url: str | None = None,
    opencode_zen_url: str | None = None,
    openrouter_url: str | None = None,
    zai_coding_url: str | None = None,
    codex_binary: str = "codex",
    codex_home: Path | None = None,
    timeout: float = 10.0,
) -> dict[str, Any]:
    report = verify_catalog(
        remote=remote,
        include_codex=include_codex,
        opencode_url=opencode_url,
        opencode_zen_url=opencode_zen_url,
        openrouter_url=openrouter_url,
        zai_coding_url=zai_coding_url,
        codex_binary=codex_binary,
        codex_home=codex_home,
        timeout=timeout,
    )
    output = config_root() / "catalog-discovery.json"
    atomic_write_json(output, report, 0o600)
    overlay_path = None
    overlay_models = 0
    if install_codex_overlay and include_codex:
        discovery = report["remote"].get("openai-codex", {})
        if not discovery.get("passed") or not discovery.get("runtime_source_ok"):
            raise RuntimeError("Codex discovery did not produce a live model catalog")
        overlay = build_codex_discovery_overlay(discovery)
        overlay_path = user_catalog_root() / "90-codex-discovered.toml"
        overlay_models = len(overlay.get("models", {}))
        header = (
            "# Generated by codex-mmo catalog refresh --install-codex-overlay.\n"
            "# Remove this file to return to the bundled Codex model baseline.\n\n"
        )
        atomic_write_text(overlay_path, header + toml_dumps(overlay), 0o600)
    return {
        "report_path": str(output),
        "overlay_path": str(overlay_path) if overlay_path else None,
        "overlay_models": overlay_models,
        "report": report,
    }
