"""Provider/model inventory — shared substrate for the dashboard ``/api/model/options``, the TUI
``model.options``/``model.save_key`` RPC handlers, and the interactive picker."""

from __future__ import annotations

import logging
from contextvars import copy_context
from dataclasses import dataclass, replace
from threading import Lock, Thread, current_thread
from typing import Any, Optional
from datetime import UTC

_pricing_prewarm_lock = Lock()
_pricing_prewarm_threads: dict[tuple[str, tuple[tuple[str, str], ...]], Thread] = {}


@dataclass(frozen=True)
class ConfigContext:
    """Disk-config snapshot (``load_picker_context()``); the TUI overlays live agent state via
    ``with_overrides()``."""

    current_provider: str
    current_model: str
    current_base_url: str
    user_providers: dict
    custom_providers: list
    excluded_providers: list = None

    def with_overrides(
        self, *, current_provider: Optional[str] = None, current_model: Optional[str] = None,
        current_base_url: Optional[str] = None,
    ) -> ConfigContext:
        """Copy with TRUTHY overrides applied: the TUI reads agent attributes that may be empty strings
        before an agent is spawned — empties must not clobber the disk-config values."""
        overrides = (("current_provider", current_provider), ("current_model", current_model),
                     ("current_base_url", current_base_url))
        kw = {k: v for k, v in overrides if v}
        return replace(self, **kw) if kw else self


def load_picker_context() -> ConfigContext:
    """Load the disk-config snapshot every consumer needs."""
    from hermes_cli.config import (
        coerce_provider_id, get_compatible_custom_providers, load_config, stringify_provider_map,
    )
    cfg = load_config()
    model_cfg = cfg.get("model", {})
    if isinstance(model_cfg, dict):
        # YAML parses unquoted scalars as int (`provider: 2070`); keep strings so picker/options
        # paths never call `.strip()` on an int.
        current_model = str(model_cfg.get("default", model_cfg.get("name", "")) or "")
        current_provider = coerce_provider_id(model_cfg.get("provider", ""))
        current_base_url = str(model_cfg.get("base_url", "") or "")
    else:  # config.model can be a bare string in older configs
        current_model, current_provider, current_base_url = (str(model_cfg) if model_cfg else ""), "", ""
    excluded = cfg.get("model_catalog", {}).get("excluded_providers") or []
    return ConfigContext(
        current_provider=current_provider, current_model=current_model, current_base_url=current_base_url,
        user_providers=stringify_provider_map(cfg.get("providers")),
        custom_providers=get_compatible_custom_providers(cfg),
        excluded_providers=excluded if isinstance(excluded, list) else [],
    )


def _slug(row: dict) -> str:
    return str(row.get("slug") or "").strip().lower()


def _without_slug(rows: list[dict], slug: str) -> list[dict]:
    return [r for r in rows if _slug(r) != slug]


# ─── Public: payload builder ────────────────────────────────────────────


def build_models_payload(
    ctx: ConfigContext, *, explicit_only: bool = False, include_unconfigured: bool = False,
    picker_hints: bool = False, canonical_order: bool = False, pricing: bool = False,
    pricing_cache_only: bool = False,
    capabilities: bool = False, featured: bool = False, force_fresh_nous_tier: bool = False,
    refresh: bool = False, probe_custom_providers: bool = True, probe_current_custom_provider: bool = False,
    for_picker: bool = False, max_models: int | None = None, non_blocking_catalogs: bool = False,
    fast_custom_probe: bool | None = None,
) -> dict:
    """Build the ``{providers, model, provider}`` shape every consumer needs. ``explicit_only`` keeps
    only providers the user explicitly configured — hides ambient/auto-seeded credentials from
    desktop chat pickers. ``pricing_cache_only``: with ``pricing``, use only values already resident
    in process caches (normal picker opens, while a background worker warms cold endpoints).
    ``non_blocking_catalogs``: provider catalogs come from the disk cache only — a degraded provider
    cannot stall the response (GUI picker opens). ``fast_custom_probe`` overrides the
    custom-endpoint discovery budget ``for_picker`` otherwise implies (1.5s vs 5s) — ``None`` keeps
    the coupling, ``False`` retains the full 5s budget (#103843)."""
    from hermes_cli.model_switch import list_authenticated_providers

    rows = list_authenticated_providers(
        current_provider=ctx.current_provider, current_base_url=ctx.current_base_url,
        current_model=ctx.current_model, user_providers=ctx.user_providers,
        custom_providers=ctx.custom_providers, force_fresh_nous_tier=force_fresh_nous_tier,
        max_models=max_models, refresh=refresh, probe_custom_providers=probe_custom_providers,
        probe_current_custom_provider=probe_current_custom_provider, for_picker=for_picker,
        excluded_providers=ctx.excluded_providers or [],
        non_blocking_catalogs=non_blocking_catalogs, fast_custom_probe=fast_custom_probe,
    )

    # Managed local runtime: staged GGUFs are selectable like any provider's models, but
    # list_authenticated_providers can't know about them (no credential — reachability is the
    # credential), so inject the row here where every picker surface inherits it.
    local_row = _local_runtime_row(ctx)
    if local_row is not None:
        rows = _without_slug(rows, "llamacpp") + [local_row]
        # A live session on the managed server reports provider "custom" (raw base_url label), which
        # would materialize a duplicate "Custom endpoint" row with the same staged models stealing the
        # checkmark. The Local row owns the managed server's identity — drop such custom rows.
        if local_row.get("is_current"):
            staged = set(local_row["models"])

            def _is_managed_custom(row: dict) -> bool:
                models = {str(m) for m in (row.get("models") or [])}
                return _slug(row) == "custom" and bool(models) and models <= staged

            rows = [r for r in rows if not _is_managed_custom(r)]

    moa_row = _moa_provider_row(ctx.current_provider)
    if moa_row is not None:
        rows = [moa_row] + _without_slug(rows, "moa")

    if explicit_only:
        rows = _filter_explicit_provider_rows(rows, ctx)
        # If the current provider lost its credential, list_authenticated_providers() omits it; keep
        # that one row so the UI shows the saved selection + a re-auth affordance instead of appearing
        # to jump providers. Exception: a "custom" current on the managed local server is already
        # represented by the Local row — the skeleton would resurrect the duplicate removed above.
        _local_owns_current = bool(local_row and local_row.get("is_current")
                                   and (ctx.current_provider or "").lower() == "custom")
        if not _local_owns_current:
            rows = list(rows) + _append_unconfigured_rows(rows, ctx, current_only=True)

    # A local proxy serving a model also in an aggregator's catalog would show under both, and picking
    # the aggregator row silently breaks the call — aggregators only list models no specific provider has.
    _strip_aggregator_overlaps(rows)

    if include_unconfigured:
        rows = list(rows) + _without_slug(_append_unconfigured_rows(rows, ctx), "moa")
    if picker_hints:
        _apply_picker_hints(rows)
    if canonical_order:
        rows = _reorder_canonical(rows)
    if pricing:
        _apply_pricing(rows, force_fresh_nous_tier=force_fresh_nous_tier, cached_only=pricing_cache_only)
    # Both metadata decorators consult ``model_overrides``.  Snapshot the
    # read-only config once for this payload rather than letting each model
    # lookup reopen config.yaml through models_dev._cfg_get().
    metadata_config = None
    if capabilities or featured:
        try:
            from hermes_cli.config import load_config_readonly

            metadata_config = load_config_readonly()
        except Exception:
            metadata_config = None
    if capabilities:
        _apply_capabilities(rows, metadata_config=metadata_config)
    if featured:
        _apply_featured(rows, metadata_config=metadata_config)
    _apply_custom_aliases(rows)
    from hermes_cli.models_validate import drop_unofferable_model_ids

    drop_unofferable_model_ids(rows)

    return {"providers": rows, "model": ctx.current_model, "provider": ctx.current_provider}


def _strip_aggregator_overlaps(rows: list[dict]) -> None:
    """Drop models from TRUE routing aggregators (OpenRouter, custom:* proxies) that a user-defined
    provider also serves. The is_user_defined guard matters: is_routing_aggregator() is True for every
    custom:* slug, so without it the dedup would empty a user's own custom row. Flat-namespace
    resellers (opencode-go/zen) serve every model first-party and keep shared names."""
    try:
        from hermes_cli.providers import is_routing_aggregator
    except Exception:
        return

    builtin_aggregators = {
        _slug(row) for row in rows
        if not row.get("is_user_defined") and is_routing_aggregator(_slug(row))
    }

    def _duplicates_builtin_aggregator(row: dict) -> bool:
        # A user row that IS the same upstream as a built-in aggregator (registered OpenRouter via
        # Settings → Providers, or a ``custom:openrouter`` slug) is that aggregator's twin, not a
        # rival: its catalog is a superset of the built-in row's, so counting it empties the
        # built-in row (openrouter → total=0 beside a live custom:openrouter row).
        row_slug = _slug(row)
        slug_suffix = (
            row_slug.split(":", 1)[1] if row_slug.startswith("custom:") else ""
        )
        if slug_suffix and slug_suffix in builtin_aggregators:
            return True
        from agent.model_metadata import _infer_provider_from_url

        inferred = _infer_provider_from_url(str(row.get("api_url") or ""))
        return inferred is not None and inferred in builtin_aggregators

    user_models: set[str] = set()
    for row in rows:
        if row.get("is_user_defined"):
            if builtin_aggregators and _duplicates_builtin_aggregator(row):
                continue  # the twin IS that aggregator; it must not retro-strip it
            user_models.update(m.lower() for m in (row.get("models") or []))
    if not user_models:
        return
    for row in rows:
        if row.get("is_user_defined") or not is_routing_aggregator(row.get("slug", "")):
            continue
        # Only strip overlaps from TRUE routing aggregators (OpenRouter, custom:* proxies). Flat-namespace
        # resellers (opencode-go / opencode-zen) serve every listed model as a first-party model, so their
        # rows must keep models that a user's proxy happens to share a name with — otherwise a subscription
        # provider's own catalog (minimax-m3, glm-5, deepseek-v4-flash, ...) is silently gutted in the
        # picker. (#47077)
        original = row.get("models") or []
        filtered = [m for m in original if m.lower() not in user_models]
        if len(filtered) < len(original):
            row["models"] = filtered
            row["total_models"] = len(filtered)


def build_model_options_payload(
    ctx: ConfigContext, *, explicit_only: bool = False, include_unconfigured: bool = False,
    refresh: bool = False,
) -> dict:
    """Shared API-server/dashboard/TUI payload. Normal open probes only the current custom provider so
    offline saved endpoints don't block the picker; explicit refresh probes all and busts the cache.

    A normal open (``refresh=False``) is a READ path: provider catalogs come from the disk cache
    only and stale/missing ones warm in the background, so a degraded provider (hanging endpoint,
    failed auth probe) delays neither the other providers' rows nor the response (#114215).

    ``for_picker=True`` keeps providers whose credential pool is entirely rate-limited visible:
    these are human-facing pickers, and hiding a temporarily exhausted pool makes providers vanish
    mid-session even though another model under the same provider may still work (same contract
    as ``/model`` and the aux pickers, #66584 / #66624). Visibility only: ``fast_custom_probe=False``
    keeps the live probe of the current custom endpoint on its full 5s discovery budget."""
    refresh = bool(refresh)
    payload = build_models_payload(
        ctx, explicit_only=bool(explicit_only), include_unconfigured=bool(include_unconfigured),
        picker_hints=True, canonical_order=True, pricing=True, pricing_cache_only=not refresh,
        capabilities=True, featured=True, for_picker=True, fast_custom_probe=False,
        refresh=refresh, probe_custom_providers=refresh, probe_current_custom_provider=not refresh,
        non_blocking_catalogs=not refresh,
    )
    _apply_limits(payload["providers"])
    _apply_usage(payload["providers"])
    if not refresh:
        _prewarm_pricing_async(payload["providers"], current_provider=ctx.current_provider,
                               current_base_url=ctx.current_base_url)
    return payload


def _apply_limits(rows: list[dict]) -> None:
    """Attach ``limit`` to rows whose credential pool is rate-limited, so a picker can say why and until
    when instead of the row just looking broken. Only providers with a persisted pool are read (no
    seeding), and a pool that fails to load says nothing rather than failing the whole catalog."""
    import logging
    from datetime import datetime, timezone

    from agent.credential_pool import load_pool
    from hermes_cli.auth import read_credential_pool

    def iso(epoch: float) -> str:
        return datetime.fromtimestamp(epoch, UTC).isoformat()

    pooled = {slug for slug, entries in read_credential_pool().items() if entries}
    for row in rows:
        slug = str(row.get("slug") or "")
        # Window plans say their limits in plan_usage (gauges + limit line); a credential cooldown chip
        # beside them would show a second, unrelated reset time.
        if slug not in pooled or row.get("is_user_defined") or row.get("plan_usage"):
            continue
        try:
            state = load_pool(slug).limit_state(row.get("models") or [])
        except Exception:  # an unreadable pool must not fail the whole catalog
            logging.getLogger(__name__).debug("Pool limit read failed for %s", slug, exc_info=True)
            continue
        if state is None:
            continue
        if state["scope"] == "account":
            row["limit"] = {"scope": "account", "resets_at": iso(state["resets_at"])}
        else:
            row["limit"] = {"scope": "models", "models": {m: iso(at) for m, at in state["models"].items()}}


def _apply_usage(rows: list[dict]) -> None:
    """Attach ``usage`` (subscription windows: % spent + reset) to signed-in rows that can report it,
    from the cache only, and ask for a background refresh, so the picker never waits on a usage API
    and a chip can warn before the wall instead of at it.

    Single-account providers keep the legacy gauge (``usage.windows``). A provider with a
    MULTI-ENTRY credential pool reports ``usage.accounts`` instead — one row per account, each with
    its own windows/state/resets_at — because one account's quota says nothing about its siblings;
    a provider-wide percentage there would be a fabricated average. A snapshot older than the
    staleness bound renders ``unknown``/``unavailable``, never a confident stale gauge. Nothing here
    mutates the pool or disables routing: telemetry never benches a credential."""
    from agent.account_usage_cache import (
        cached_account_usage, has_account_usage, refresh_account_usage_async,
        refresh_account_usage_entries_async, snapshot_is_stale,
    )
    from hermes_cli.auth import read_credential_pool

    def _wire_windows(snapshot) -> list[dict]:
        return [
            {"label": w.label, "used_percent": float(w.used_percent),
             "resets_at": w.reset_at.isoformat() if w.reset_at else None,
             "scope": w.scope}
            for w in (snapshot.windows if snapshot else ()) if w.used_percent is not None
        ]

    def _account_resets_at(snapshot, live_cooldown_until: float | None) -> str | None:
        """A quota-exhausted account recovers at the LATEST of its exhausted account-scoped
        windows (all of them must reopen before the account is whole again); a live
        credential-wide cooldown wins when later. Model-scoped windows never reset an account.
        Unknown stays None — the frontend renders its own advisory (e.g. the earliest sibling)."""
        resets: list[str] = []
        if live_cooldown_until:
            resets.append(_iso_from_epoch(live_cooldown_until))
        for window in (snapshot.windows if snapshot else ()):
            if window.scope != "account" or window.used_percent is None or window.reset_at is None:
                continue
            if float(window.used_percent) >= _EXHAUSTED_WINDOW_PERCENT:
                resets.append(window.reset_at.isoformat())
        return max(resets) if resets else None

    wanted: list[str] = []
    entry_requests: list[dict] = []
    pooled_sizes = {slug: len(entries) for slug, entries in read_credential_pool().items() if entries}
    for row in rows:
        slug = str(row.get("slug") or "")
        if not slug or row.get("is_user_defined") or row.get("authenticated") is False:
            continue
        supports_usage = has_account_usage(slug)
        # A single-entry pool (or no pool) is the single-account case: the legacy gauge stays
        # (chip contract unchanged). Only a genuinely multi-entry pool gets per-account rows —
        # there, a provider-wide percentage would be a fabricated average across logins.
        if pooled_sizes.get(slug, 0) < 2:
            if not supports_usage:
                continue
            wanted.append(slug)
            snapshot = cached_account_usage(slug)
            windows = _wire_windows(snapshot)
            if windows and not snapshot_is_stale(snapshot):
                row["usage"] = {"windows": windows}
            continue
        if supports_usage:
            wanted.append(slug)
        accounts = _pool_usage_accounts(slug, _wire_windows, _account_resets_at, entry_requests)
        if accounts is not None:
            row["usage"] = {"accounts": accounts}
    refresh_account_usage_async(wanted)
    refresh_account_usage_entries_async(entry_requests)


# A window this spent counts as exhausted when computing an account's recovery time.
_EXHAUSTED_WINDOW_PERCENT = 100.0


def _iso_from_epoch(epoch: float) -> str:
    from datetime import datetime, timezone

    return datetime.fromtimestamp(float(epoch), UTC).isoformat()


def _pool_usage_accounts(slug: str, wire_windows, account_resets_at,
                         entry_requests: list[dict]) -> Optional[list[dict]]:
    """Per-account usage rows for one pooled provider, or None when the pool cannot be read.

    Read-only: entries are inspected via ``load_pool(slug)`` properties (no select/rotate/refresh).
    Dedupe is scoped to provider+host+trusted decoded account id where supported (several
    credentials of one Codex account are ONE account), else per-credential token identity — never
    a provider-wide singleton. Providers without a usage fetcher still get one metadata row per
    account with state unknown/limited/unavailable, so the chip can count honestly without bogus
    fetches. DEAD auth rows stay visible as ``unavailable`` (never a quota row). No secrets, raw
    payloads or base URLs ride the wire."""
    import logging
    import time

    from agent.account_usage_cache import _identity_id_for, cached_account_usage, has_account_usage, snapshot_is_stale
    from agent.credential_pool import STATUS_DEAD, _exhausted_until, load_pool

    try:
        pool = load_pool(slug)
        entries = list(pool.entries())
    except Exception:  # an unreadable pool must not fail the whole catalog
        logging.getLogger(__name__).debug("Pool usage read failed for %s", slug, exc_info=True)
        return None
    if not entries:
        return None
    live = [e for e in entries if e.last_status != STATUS_DEAD]
    sole = len(live) <= 1
    supports_usage = has_account_usage(slug)
    now = time.time()
    accounts: list[dict] = []
    seen: dict[tuple[str, str, str], dict] = {}
    for entry in entries:
        identity_id = _identity_id_for(slug, entry)
        host = _pool_entry_host(slug, entry)
        dedupe_key = (slug, host, identity_id)
        is_dead = entry.last_status == STATUS_DEAD
        snapshot = (cached_account_usage(slug, identity_id=identity_id)
                    if supports_usage and not is_dead else None)
        stale = snapshot_is_stale(snapshot)
        windows = wire_windows(snapshot) if not stale else []
        # 'limited' only when a live credential-wide cooldown benches this account or its
        # account-scoped quota windows are exhausted — telemetry alone never benches anything.
        cooldown_until = None if is_dead else _exhausted_until(entry, sole_credential=sole)
        live_cooldown = cooldown_until if cooldown_until and cooldown_until > now else None
        quota_exhausted = bool(windows) and any(
            w.get("scope") == "account" and w.get("used_percent") is not None
            and float(w["used_percent"]) >= _EXHAUSTED_WINDOW_PERCENT for w in windows)
        if is_dead:
            state = "unavailable"
        elif live_cooldown is not None or quota_exhausted:
            state = "limited"
        elif snapshot is None or stale or not windows:
            # No numeric live windows (failed/empty fetch, stale snapshot, non-supporting
            # provider): unknown — including a "ready-looking" account with nothing to show.
            state = "unknown"
        else:
            state = "ready"
        account_row = {
            "id": identity_id, "label": str(entry.label or ""), "windows": windows,
            "state": state, "resets_at": account_resets_at(snapshot, live_cooldown),
        }
        if dedupe_key in seen:
            # Same account under a second credential: keep the row that says more (state rank
            # limited > ready > unknown > unavailable), but never duplicate the account.
            existing = seen[dedupe_key]
            rank = {"limited": 3, "ready": 2, "unknown": 1, "unavailable": 0}
            if rank.get(account_row["state"], 0) > rank.get(existing["state"], 0):
                accounts[accounts.index(existing)] = account_row
                seen[dedupe_key] = account_row
            continue
        seen[dedupe_key] = account_row
        accounts.append(account_row)
        if supports_usage and not is_dead:
            entry_requests.append({
                "provider": slug, "identity_id": identity_id,
                "base_url": _pool_entry_route_base_url(slug, entry), "api_key": entry.runtime_api_key,
            })
    return accounts or None


def _pool_entry_host(slug: str, entry) -> str:
    """Normalized route host of one entry, for account dedupe scoping. Never serialized."""
    from agent.credential_pool import _norm_url

    if slug == "openai-codex":
        from hermes_cli.auth_codex import _codex_pool_route_base_url

        return _norm_url(_codex_pool_route_base_url(entry.runtime_base_url))
    return _norm_url(getattr(entry, "runtime_base_url", None))


def _pool_entry_route_base_url(slug: str, entry) -> Optional[str]:
    """The base URL a usage fetch for *entry* must target (its own route, never a sibling's)."""
    if slug == "openai-codex":
        from hermes_cli.auth_codex import _codex_pool_route_base_url

        return _codex_pool_route_base_url(entry.runtime_base_url)
    return getattr(entry, "runtime_base_url", None) or None


# ─── Public: auxiliary-task pickers ─────────────────────────────────────


def build_aux_picker_rows(
    *, current_provider: str = "", current_model: str = "", current_base_url: str = "",
    max_models: int | None = None,
) -> list[dict]:
    """Provider rows for any auxiliary-task picker (vision, compression, …). Honours
    ``excluded_providers``; exhausted-pool providers stay visible (``for_picker``); only the active
    custom endpoint is probed. ``moa`` is excluded: auxiliary_client unwraps it to its aggregator
    anyway, so offering it would be a choice silently rewritten.

    Aux pickers kept re-deriving their own kwargs and each one silently dropped a different slice of the
    user's configuration. Two independent contributor PRs landed against the same two call sites for exactly
    this: 52642 (user ``providers:`` / ``custom_providers:`` entries never appeared) and #66624 (providers
    with an exhausted credential pool were hidden). Both were per-site kwarg patches, so the next aux picker
    would have reintroduced the same gap. Routing through one function makes the correct behaviour the
    default that a new caller cannot forget:
    """
    ctx = load_picker_context().with_overrides(
        current_provider=current_provider, current_model=current_model, current_base_url=current_base_url,
    )
    rows = build_models_payload(
        ctx, for_picker=True, probe_custom_providers=False, probe_current_custom_provider=True,
        max_models=max_models,
    )["providers"]
    return _without_slug(rows, "moa")


def format_aux_picker_entries(
    rows: list[dict], *, current_provider: str = "", current_base_url: str = "",
) -> list[tuple[str, str, list[str]]]:
    """Render aux-picker rows as ``(slug, label, models)``. A raw ``base_url`` custom endpoint is
    "current" only through that URL, never a slug — so with ``current_base_url`` set no row is marked."""
    entries: list[tuple[str, str, list[str]]] = []
    current_slug = str(current_provider or "").strip().lower()
    has_base_url = bool(str(current_base_url or "").strip())
    for row in rows:
        slug = str(row.get("slug") or "")
        name = row.get("name") or slug
        total = row.get("total_models") or len(row.get("models") or [])
        model_hint = f" — {total} models" if total else ""
        marker = "  ← current" if slug.lower() == current_slug and current_slug and not has_base_url else ""
        entries.append((slug, f"{name}{model_hint}{marker}", list(row.get("models") or [])))
    return entries


def _reasoning_catalog_reader(slug: str):
    """Per-model reasoning-capability reader for aggregators that publish one. Cache-only — the picker
    must never block on HTTP; a cold cache warms in the background and reports no restriction until then."""
    try:
        from hermes_cli.models_reasoning_caps import (
            nous_model_reasoning_capabilities,
            openrouter_model_reasoning_capabilities,
            warm_nous_reasoning_caps_async,
            warm_openrouter_reasoning_caps_async,
        )
    except Exception:
        return None

    readers = {
        "nous": (warm_nous_reasoning_caps_async, nous_model_reasoning_capabilities),
        "openrouter": (warm_openrouter_reasoning_caps_async, openrouter_model_reasoning_capabilities),
    }
    if slug not in readers:
        return None
    warm, read = readers[slug]
    warm()
    return read


def _apply_capabilities(rows: list[dict], *, metadata_config: dict | None = None) -> None:
    """Attach ``{model: {fast, reasoning, ...}}`` per row. ``reasoning`` defaults True when the catalog is
    silent (the dial is a no-op on models that ignore it; hiding it from a capable model is worse). A
    serving aggregator's detail overrides models.dev (adds ``can_disable_reasoning``). ``supported_efforts``
    is deliberately NOT forwarded — it under-reports levels that work."""
    from agent.codex_service_tiers import ultrafast_offered
    from hermes_cli.models import model_supports_ultrafast, resolve_fast_mode_overrides

    try:
        from agent.models_dev import get_model_capabilities
    except Exception:
        get_model_capabilities = None  # type: ignore[assignment]

    for row in rows:
        slug = row.get("slug") or ""
        caps: dict[str, dict[str, Any]] = {}
        read_reasoning_catalog = _reasoning_catalog_reader(slug.lower())
        for model in row.get("models") or []:
            reasoning = True
            if get_model_capabilities is not None and slug:
                try:
                    meta = get_model_capabilities(slug, model, config=metadata_config)
                    if meta is not None and meta.supports_reasoning is not None:
                        reasoning = meta.supports_reasoning
                except Exception:
                    reasoning = True

            fast = resolve_fast_mode_overrides(
                model, provider=slug, base_url=row.get("api_url")) is not None
            entry: dict[str, Any] = {"fast": fast, "reasoning": reasoning}
            if fast and model_supports_ultrafast(model) and ultrafast_offered(
                    model, provider=slug, base_url=row.get("api_url")):
                entry["ultrafast"] = True

            if reasoning and read_reasoning_catalog is not None:
                try:
                    detail = read_reasoning_catalog(model)
                except Exception:
                    detail = None
                if detail and not detail.get("supports_reasoning"):
                    # Aggregator catalog beats models.dev for a route it serves: no reasoning param
                    # means no reasoning controls, so no disable to describe either.
                    entry["reasoning"] = False
                elif detail:
                    entry["can_disable_reasoning"] = not detail.get("mandatory")

            caps[model] = entry

        row["capabilities"] = caps


# Newest N models per lab an aggregator row features by default (older tail behind search/show-all);
# 5 keeps a lab's headliners without letting a prolific vendor flood the view.
_FEATURED_PER_LAB = 5


def _apply_featured(rows: list[dict], *, metadata_config: dict | None = None) -> None:
    """Attach a ``featured_models`` shortlist to each routing-aggregator row: newest
    ``_FEATURED_PER_LAB`` per vendor by models.dev ``release_date`` (ranked within the row, never vs.
    today, so it is stable); ties keep curated order. Non-aggregators — including every user-defined
    row, whose ``models:`` list is an explicit allow-list — get an empty list and keep top-N
    behaviour (#120217)."""
    try:
        from agent.models_dev import get_model_info
    except Exception:
        get_model_info = None  # type: ignore[assignment]

    # "Is this row an aggregator?" is answered canonically by is_routing_aggregator() — the same
    # predicate _strip_aggregator_overlaps() uses. Deriving it from model-id spelling (does any id
    # contain "/" and span >= 2 prefixes?) misread every Org/Model-shaped user provider as a
    # multi-lab aggregator and hid the models its owner configured by hand (#120217).
    try:
        from hermes_cli.providers import is_routing_aggregator
    except Exception:
        is_routing_aggregator = None  # type: ignore[assignment]

    for row in rows:
        slug = str(row.get("slug") or "").strip().lower()
        models = row.get("models") or []

        if row.get("is_user_defined") or not (is_routing_aggregator and is_routing_aggregator(slug)):
            row["featured_models"] = []
            continue

        by_lab: dict[str, list[tuple[int, str, str]]] = {}  # only multi-lab aggregators get a shortlist
        for pos, model in enumerate(models):
            lab = model.split("/", 1)[0] if "/" in model else ""
            if not lab:  # no vendor prefix → single-namespace provider, not an aggregator
                by_lab = {}
                break
            date = ""
            if get_model_info is not None:
                info = (get_model_info(slug, model, config=metadata_config)
                        or get_model_info("openrouter", model, config=metadata_config))
                date = getattr(info, "release_date", "") if info else ""
            by_lab.setdefault(lab, []).append((pos, date, model))

        if len(by_lab) < 2:
            row["featured_models"] = []
            continue

        featured: list[str] = []
        for entries in by_lab.values():
            # Newest release_date first; earlier list position breaks ties (sole key when undated).
            ranked = sorted(entries, key=lambda e: (e[1], -e[0]), reverse=True)
            featured.extend(model for _pos, _date, model in ranked[:_FEATURED_PER_LAB])
        order = {m: i for i, m in enumerate(models)}  # keep the row's model order for stable rendering
        row["featured_models"] = sorted(featured, key=lambda m: order[m])


def _apply_custom_aliases(rows: list[dict]) -> None:
    """Attach the accepted identity set to each user-defined row: ``model.options`` reports the canonical
    ``custom:<key>`` while rows carry the bare key as ``slug``, so GUI exact-match never finds the row.

    GUI pickers compare the two to decide which row is active; exact equality never matches for custom
    providers (#87035). Exposing ``aliases`` — every current and legacy spelling from
    :func:`hermes_cli.providers.custom_provider_aliases` — lets the frontend do a membership check instead.
    """
    from hermes_cli.providers import custom_provider_aliases

    for row in rows:
        if not row.get("is_user_defined"):
            continue
        try:
            row["aliases"] = sorted(
                custom_provider_aliases(str(row.get("name", "")), str(row.get("slug", ""))))
        except Exception:
            continue


# ─── Internal: row post-processing ──────────────────────────────────────


def _provider_auth_hint(slug: str) -> tuple[str, str]:
    """``(auth_type, key_env)`` for a canonical provider (``("api_key", "")`` when unregistered)."""
    from hermes_cli.auth import PROVIDER_REGISTRY

    cfg = PROVIDER_REGISTRY.get(slug)
    auth_type = cfg.auth_type if cfg else "api_key"
    key_env = cfg.api_key_env_vars[0] if (cfg and cfg.api_key_env_vars) else ""
    return auth_type, key_env


def _row(slug: str, name: str, is_current: bool, **extra: Any) -> dict:
    return {"slug": slug, "name": name, "is_current": is_current, "is_user_defined": False, **extra}


def _canonical_row(entry, cur: str, **extra: Any) -> dict:
    from hermes_cli.models import _PROVIDER_LABELS

    return _row(entry.slug, _PROVIDER_LABELS.get(entry.slug, entry.label), entry.slug.lower() == cur, **extra)


def _append_unconfigured_rows(
    rows: list[dict], ctx: ConfigContext, *, current_only: bool = False,
) -> list[dict]:
    """Empty setup skeletons for canonical providers missing from ``rows`` — except the *current* one:
    if config.yaml still points at it but credentials are gone, keep a row carrying the saved model so
    GUI pickers don't silently snap to another provider."""
    from hermes_cli.models import _PROVIDER_ALIASES, _model_requires_account_discovery
    from hermes_cli.models_catalog_static import listed_canonical_providers

    seen = {r["slug"].lower() for r in rows}
    cur = (ctx.current_provider or "").lower()
    cur_model = str(ctx.current_model or "").strip()
    # Honor ``model_catalog.excluded_providers`` like ``list_authenticated_providers`` does — unconditionally,
    # even for the current provider — or ``include_unconfigured`` pickers (TUI ``/model``) resurrect every
    # excluded provider as a skeleton row (#68816). A slug is hidden when it or any alias is excluded, so an
    # alias exclusion (``google`` → ``gemini``) can't leak the canonical row back.
    excluded = {str(p).strip().lower() for p in (ctx.excluded_providers or []) if p}
    names_for: dict[str, set[str]] = {}
    for alias, canon in _PROVIDER_ALIASES.items():
        names_for.setdefault(canon.lower(), {canon.lower()}).add(alias.lower())
    extras: list[dict] = []
    for entry in listed_canonical_providers():
        slug = entry.slug.lower()
        if slug in seen:
            continue
        if names_for.get(slug, {slug}) & excluded:
            continue
        if current_only and entry.slug.lower() != cur:
            continue
        if entry.slug.lower() == cur:
            saved_model = "" if _model_requires_account_discovery(entry.slug, cur_model) else cur_model
            auth_type, key_env = _provider_auth_hint(entry.slug)
            tail = (
                "Astra requires successful account-scoped model discovery."
                if cur_model and not saved_model else "Showing the saved model only."
            )
            warning = (
                f"Configured provider missing usable credentials; paste {key_env} to reactivate. {tail}"
                if auth_type == "api_key" and key_env
                else f"Configured provider is not authenticated; run `hermes model` to reactivate. {tail}"
            )
            extras.append(_canonical_row(
                entry, cur, models=[saved_model] if saved_model else [], total_models=1 if saved_model else 0,
                source="configured-current", authenticated=False, auth_type=auth_type, key_env=key_env,
                warning=warning,
            ))
            continue
        extras.append(_canonical_row(entry, cur, models=[], total_models=0, source="canonical"))
    return extras


def _anthropic_oauth_credentials_present() -> bool:
    """True when the user explicitly authenticated Anthropic via OAuth (Hermes device flow or Claude Code
    login) — those leave no trace in active_provider / model.provider / API-key env vars."""
    try:
        from agent.anthropic_credentials import read_claude_code_credentials, read_hermes_oauth_credentials

        readers = (read_hermes_oauth_credentials, read_claude_code_credentials)
        if any((read() or {}).get("accessToken") for read in readers):
            return True
    except Exception:
        return False
    # Pool-only OAuth entries (auth.json credential_pool.anthropic) are equally deliberate — discovery
    # accepts them via pool.has_credentials(), so the filter must too or those rows are built then
    # silently dropped. Read-only (no load_pool) so a picker open never mutates auth.json.
    try:
        from agent.credential_pool import AUTH_TYPE_OAUTH
        from hermes_cli.auth import read_credential_pool

        for entry in read_credential_pool("anthropic"):
            if (isinstance(entry, dict) and entry.get("auth_type") == AUTH_TYPE_OAUTH
                    and str(entry.get("access_token") or "").strip()):
                return True
    except Exception:
        pass
    return False


def _filter_explicit_provider_rows(rows: list[dict], ctx: ConfigContext) -> list[dict]:
    """Keep only rows backed by explicit user configuration — ``list_authenticated_providers`` also
    discovers ambient credentials (e.g. GitHub CLI -> Copilot) Desktop chat pickers must not show."""
    from hermes_cli.auth import is_provider_explicitly_configured

    current_slug = str(ctx.current_provider or "").strip().lower()

    def _is_explicit(row: dict, slug: str) -> bool:
        # Managed local models are explicit configuration by existence (gigabytes downloaded into the
        # machine-scoped dir); there is deliberately no config credential, so without the source clause
        # the row would only survive on the profile where Use was last clicked.
        if (row.get("is_user_defined") or (current_slug and slug == current_slug)
                or row.get("source") == "local-runtime"):
            return True
        if slug == "moa":
            # Virtual routing mode, not a configured provider: hide unless current (above) or the user
            # wrote an enabled preset into RAW config (the DEFAULT_CONFIG preset must not show MoA).
            return _raw_config_has_enabled_moa_preset()
        return (
            # Anthropic OAuth (device flow / Claude Code) and external-process CLIs (copilot-acp) are
            # deliberate sign-ins that leave no trace in config/env; keep the rows discovery accepted.
            (slug == "anthropic" and _anthropic_oauth_credentials_present())
            or _external_process_signed_in(slug)
            or is_provider_explicitly_configured(slug)
        )

    return [row for row in rows
            if (slug := str(row.get("slug", "")).strip().lower()) and _is_explicit(row, slug)]


def _external_process_signed_in(slug: str) -> bool:
    """True when an external-process provider has verified CLI credentials."""
    try:
        from hermes_cli.auth import PROVIDER_REGISTRY, get_external_process_provider_status
        pconfig = PROVIDER_REGISTRY.get(slug)
        return bool(pconfig and pconfig.auth_type == "external_process"
                    and get_external_process_provider_status(slug).get("auth_verified"))
    except Exception:
        return False


def _raw_config_has_enabled_moa_preset() -> bool:
    """True when the user's RAW config enables MoA: ``load_config()`` merges the DEFAULT_CONFIG preset for
    everyone, which is not a user choice; visible once one enabled preset (or legacy flat config) is saved."""
    try:
        from hermes_cli.config import read_raw_config

        raw = read_raw_config()
    except Exception:
        return False

    moa = raw.get("moa") if isinstance(raw, dict) else None
    if not isinstance(moa, dict):
        return False

    presets = moa.get("presets")
    if isinstance(presets, dict):
        return any(
            not isinstance(preset, dict) or preset.get("enabled", True)
            for name, preset in presets.items() if str(name or "").strip()
        )

    legacy_keys = {"reference_models", "aggregator", "reference_temperature", "aggregator_temperature",
                   "max_tokens", "reference_max_tokens", "fanout"}
    return any(key in moa for key in legacy_keys) and bool(moa.get("enabled", True))


def _apply_picker_hints(rows: list[dict]) -> None:
    """Add ``authenticated``/``auth_type``/``key_env``/``warning`` per row."""
    for row in rows:
        if "authenticated" in row:
            continue
        # Skeleton rows (_append_unconfigured_rows) have empty `models` AND source="canonical".
        is_skeleton = row.get("source") == "canonical" and not row.get("models")
        row["authenticated"] = not is_skeleton
        if not is_skeleton or row.get("is_user_defined"):
            continue
        auth_type, key_env = _provider_auth_hint(row["slug"])
        row["auth_type"] = auth_type
        row["key_env"] = key_env
        row["warning"] = (f"paste {key_env} to activate" if auth_type == "api_key" and key_env
                          else f"run `hermes model` to configure ({auth_type})")


def _reorder_canonical(rows: list[dict]) -> list[dict]:
    """Canonical slugs in ``CANONICAL_PROVIDERS`` order, truly-custom rows last. Keys on slug membership,
    NOT ``is_user_defined`` — ``providers:`` config rows carry that flag even for canonical slugs."""
    from hermes_cli.models import CANONICAL_PROVIDERS

    order = {e.slug: i for i, e in enumerate(CANONICAL_PROVIDERS)}
    canon = sorted((r for r in rows if r["slug"] in order), key=lambda r: order[r["slug"]])
    extras = [r for r in rows if r["slug"] not in order]
    return canon + extras


def _attach_nous_plan_usage(row: dict, *, warm: bool):
    """Stamp ``plan_usage`` from the portal snapshot (window plans only) and return the PickerUsage (or
    None). A cache-only open reads the last snapshot at any age: the 60s account cache has usually lapsed
    by the time someone opens the composer menu. Accounts without a window plan get nothing."""
    try:
        from hermes_cli.model_picker_usage import nous_picker_usage
        usage = nous_picker_usage(warm=warm, any_age=not warm)
    except Exception:
        logging.getLogger(__name__).debug("Nous plan usage unavailable", exc_info=True)
        return None
    if usage is not None:
        row["plan_usage"] = usage.to_payload()
        # Main's subscription-usage chip (`usage.windows`): the same gauge for a window
        # plan, so it warns near a limit in the existing slot. Once an allowance is full the picker's limit
        # line says it (with what happens next), so the chip would only repeat it; paused plans say so in
        # the plan line.
        if not usage.paused_reason and not row.get("usage") and not any(g.left_fraction <= 0.0 for g in usage.gauges):
            windows = [_plan_usage_window(g) for g in usage.gauges if g.resets_at_ms]
            if windows:
                row["usage"] = {"windows": windows}
    return usage


def _plan_usage_window(g) -> dict:
    """One window-plan allowance in main's ``ProviderUsageWindow`` shape. The label is Hermes's row title
    (``Today``), like the other providers' English window labels."""
    from datetime import datetime
    from agent.usage_gauges import WindowGauge, gauge_title
    return {
        "label": gauge_title(WindowGauge(g.label, g.scope, g.title or g.label, 0)),
        "used_percent": round(g.used_fraction * 100.0, 2),
        "resets_at": datetime.fromtimestamp(g.resets_at_ms / 1000, UTC).isoformat(),
        "scope": "account" if g.scope == "all" else "model",
    }


# health: allow CC -- usage-window plans: one branch attaches plan usage to the Nous row (built in _attach_nous_plan_usage); over target on main
def _apply_pricing(rows: list[dict], *, force_fresh_nous_tier: bool = False, cached_only: bool = False) -> None:
    """Set ``row["pricing"] = {model_id: {input, output, cache | None, free}}``; for Nous also
    ``free_tier`` (account is free-tier) and ``unavailable_models`` (paid models a free user can't pick).
    ``cached_only`` never hits the network: unknown Nous entitlement fails closed (``free_tier_pending``,
    all models locked) and missing pricing is marked ``pricing_pending``."""
    from hermes_cli.models_pricing import (
        _format_price_per_mtok,
        compute_sale_discount,
        get_pricing_for_provider,
    )
    from hermes_cli.models import (
        check_nous_free_tier,
        get_cached_nous_free_tier,
        partition_nous_models_by_tier,
    )

    nous_free_tier: Optional[bool] = None  # resolved once (cached in models.py for the TTL window)

    for row in rows:
        slug = str(row.get("slug", "")).lower()
        models = row.get("models") or []
        if not models:
            continue
        if row.get("free_tier_row"):
            # The free tier's one model has no Portal pricing and no entitlement to read: pricing
            # it would lock the only row a free-tier install can select.
            continue
        try:
            pricing_kwargs = {"cached_only": True} if cached_only else {}
            if slug.startswith("custom:"):
                pricing_kwargs["base_url"] = str(row.get("api_url") or "")
            raw_pricing = get_pricing_for_provider(slug, **pricing_kwargs) or {}
        except Exception:
            raw_pricing = {}
        cached_nous_tier: Optional[bool] = None
        plan_usage = _attach_nous_plan_usage(row, warm=not cached_only) if slug == "nous" else None
        if slug == "nous" and cached_only:
            cached_nous_tier = get_cached_nous_free_tier()
            if cached_nous_tier is None:
                # Entitlement unknown: stay nonblocking but fail closed until the prewarm has populated
                # both caches, else a free account could briefly select paid models on first open.
                row["free_tier_pending"] = True
                row["unavailable_models"] = list(models)
                if not row.get("warning"):  # say why every model renders locked
                    row["warning"] = ("Checking Nous plan entitlement… models unlock on the "
                                      "next picker open or refresh.")
                continue
        if not raw_pricing:
            if slug == "nous":
                row["free_tier"] = bool(cached_nous_tier)
                row["pricing_pending"] = True
                row["unavailable_models"] = list(models) if cached_nous_tier else []
            continue

        formatted: dict[str, dict] = {}
        for mid in models:
            p = raw_pricing.get(mid)
            if not p:
                continue
            inp_raw, out_raw = p.get("prompt", ""), p.get("completion", "")
            cache_raw = p.get("input_cache_read", "")
            inp = _format_price_per_mtok(inp_raw) if inp_raw != "" else ""
            out = _format_price_per_mtok(out_raw) if out_raw != "" else ""
            entry: dict = {
                "input": inp, "output": out,
                "cache": _format_price_per_mtok(cache_raw) if cache_raw else None,
                "free": inp == "free" and out in ("free", ""),  # both input and output cost nothing
            }
            # Sale chrome is Nous Portal-only (other catalogs' nested pricing.original is ignored); free
            # models get flat -100% chrome, was_* only when the gateway served an original.
            if slug == "nous":
                sale = compute_sale_discount(inp_raw, out_raw, p.get("original"))
                if sale is not None:
                    discount_percent, was_prompt_raw, was_out_raw = sale
                    entry["discount_percent"] = discount_percent
                    for key, was_raw in (("was_input", was_prompt_raw), ("was_output", was_out_raw)):
                        if was_raw != "":
                            entry[key] = _format_price_per_mtok(was_raw)
            formatted[mid] = entry

        if formatted:
            row["pricing"] = formatted

        if slug == "nous":
            try:
                if nous_free_tier is None:
                    nous_free_tier = (cached_nous_tier if cached_only
                                      else check_nous_free_tier(force_fresh=force_fresh_nous_tier))
                row["free_tier"] = bool(nous_free_tier)
                row["unavailable_models"] = (
                    partition_nous_models_by_tier(list(models), raw_pricing, free_tier=True)[1]
                    if nous_free_tier else [])
            except Exception:  # tier detection failed — fail open (no gating)
                row["free_tier"] = False
                row["unavailable_models"] = []
            if plan_usage is not None:
                from hermes_cli.model_picker_usage import usage_levels
                # The gateway's per-plan word per model (free / low / medium / top_tier).
                row["usage_levels"] = usage_levels(list(models), raw_pricing)
                # Top-tier marks from the gateway (it decides, not the client).
                row["plan_usage"] = plan_usage.with_pricing({m: raw_pricing.get(m) for m in models}).to_payload()
                # Window plans: the picker shows the usage word, not per-token prices.
                row.pop("pricing", None)


def _local_runtime_row(ctx: ConfigContext) -> dict | None:
    """The ``llamacpp`` row from staged GGUFs (``None`` when none) — downloaded models must be selectable
    before the server runs (selection starts it via the runtime_provider seam). The row's id comes from
    the provider registry's own definition, never a local literal: a row the resolver can't resolve is
    the bug this row's offline-first contract depends on not having."""
    try:
        from hermes_cli.local_runtime.bootstrap import staged_model_ids
        from hermes_cli.providers import LLAMACPP_ALIASES, LLAMACPP_PROVIDER_ID

        staged = staged_model_ids()
        if not staged:
            return None
        current = (ctx.current_provider or "").strip().lower() in LLAMACPP_ALIASES
        if not current:
            # A LIVE session on the managed server reports provider "custom" with the managed base_url;
            # match on the endpoint so the session being chatted in still shows a selection.
            try:
                from hermes_cli.local_runtime.endpoint import _state_endpoint

                managed = _state_endpoint()
                current = bool(managed and (ctx.current_base_url or "").strip().rstrip("/")
                               == managed["base_url"].rstrip("/"))
            except Exception:
                current = False
        # Bare "Local" user-facing (engine name is an implementation detail); authenticated = reachability.
        return _row(LLAMACPP_PROVIDER_ID, "Local", current, models=staged, total_models=len(staged),
                    source="local-runtime", authenticated=True, auth_type="local", warning=None)
    except Exception:
        return None


def _prewarm_pricing_async(
    rows: list[dict], *, current_provider: str = "", current_base_url: str = "",
) -> Optional[Thread]:
    """Warm picker pricing caches without delaying the current payload (one worker per
    profile + endpoint scope; a live worker is reused)."""
    from hermes_constants import hermes_home_key
    from hermes_cli.models_pricing import pricing_cache_scope

    slugs = {
        (
            str(row.get("slug") or "").lower(),
            str(row.get("api_url") or "") if str(row.get("slug") or "").lower().startswith("custom:") else "",
        )
        for row in rows if row.get("slug")
    }
    endpoint_scope = tuple(sorted(
        (slug, pricing_cache_scope(
            slug, base_url=base_url, current_provider=current_provider, current_base_url=current_base_url,
        ))
        for slug, base_url in slugs))
    prewarm_key = (hermes_home_key(), endpoint_scope)

    with _pricing_prewarm_lock:
        current = _pricing_prewarm_threads.get(prewarm_key)
        if current is not None and current.is_alive():
            return current
        # The worker mutates only private copies; the pricing helpers populate shared process caches.
        worker_rows = [{**row, "models": list(row.get("models") or [])} for row in rows]

        def _worker() -> None:
            try:
                _apply_pricing(worker_rows)
            finally:
                with _pricing_prewarm_lock:
                    if _pricing_prewarm_threads.get(prewarm_key) is current_thread():
                        _pricing_prewarm_threads.pop(prewarm_key, None)

        thread = Thread(target=copy_context().run, args=(_worker,),
                        name="hermes-picker-pricing-prewarm", daemon=True)
        _pricing_prewarm_threads[prewarm_key] = thread
        thread.start()
        return thread


def _moa_provider_row(current_provider: str = "") -> dict | None:
    """The virtual ``moa`` row shared by the CLI inventory and gateway picker; ``None`` without presets.

    Strictly opt-in (#63353): the row only appears when the user's raw config.yaml explicitly
    enables at least one MoA preset. The synthesized ``default`` preset from
    ``normalize_moa_config({})`` — which every user gets via DEFAULT_CONFIG defaults — must not
    be treated as a user choice."""
    if not _raw_config_has_enabled_moa_preset():
        return None

    try:
        from hermes_cli.config import load_config
        from hermes_cli.moa_config import normalize_moa_config

        cfg = normalize_moa_config(load_config().get("moa") or {})
        models = list(cfg.get("presets", {}).keys())
        if not models:
            return None
        return _row(
            "moa", "Mixture of Agents", (current_provider or "").lower() == "moa", models=models,
            total_models=len(models), source="virtual", authenticated=True, auth_type="virtual",
            warning="Aggregator is the acting model billed for the run; references only advise once per user turn by default.")
    except Exception:
        return None
