"""Minimal WZRD client for model-selection priors.

The feed is momentum-only. We keep the contract small:

- `pick()` returns a model string
- `pick_details()` returns the reasoning record
- `shortlist()` returns ranked candidates
- `compare()` compares two model names

This package intentionally does not execute model calls. It only helps
an agent decide what to call next.
"""

from __future__ import annotations

import base64
import json
import os
import re
import time
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Any, Mapping, Optional, Sequence
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

DEFAULT_API_URL = "https://api.twzrd.xyz/v1/signals/momentum"
DEFAULT_PREMIUM_API_URL = "https://api.twzrd.xyz/v1/signals/momentum/premium"
DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com"
DEFAULT_FEED_SOURCE = "auto"
DEFAULT_TIMEOUT_SECONDS = 10.0
DEFAULT_CACHE_TTL_SECONDS = 300
DEFAULT_FEED_LIMIT = 100

_TRUSTED_CONFIDENCE = {"normal", "high"}
_RELAXED_CONFIDENCE = {"normal", "high", "low"}

# Trend weights: aggressive boost for momentum signals.
# A surging model at 3x beats a stable model unless the stable one has 3x the velocity.
# This makes trend the PRIMARY routing signal, velocity is the tiebreaker.
_TREND_WEIGHTS = {
    "surging": 3.00,
    "accelerating": 2.00,
    "stable": 1.00,
    "onchain": 1.00,
    "insufficient_history": 0.80,
    "decelerating": 0.50,
    "cooling": 0.25,
}

_CONFIDENCE_WEIGHTS = {
    "normal": 1.00,
    "high": 1.00,
    "low": 0.60,
    "insufficient": 0.35,
    "unknown": 0.75,
}

_ACTION_WEIGHTS = {
    "pre_warm_urgent": 1.20,
    "pre_warm": 1.10,
    "candidate": 1.00,
    "recommend": 1.00,
    "onchain": 1.00,
    "route": 1.00,
    "maintain": 0.90,
    "prewarm": 0.95,
    "evaluate": 0.90,
    "watch": 0.60,
    "consider_deprovision": 0.30,
    "observe": 0.25,
}

_TASK_PLATFORM_WEIGHTS = {
    "code": {"github": 1.50, "openrouter": 1.20, "artificial_analysis": 1.10, "huggingface": 1.00},
    "coding": {"github": 1.50, "openrouter": 1.20, "artificial_analysis": 1.10, "huggingface": 1.00},
    "chat": {"openrouter": 1.50, "artificial_analysis": 1.10, "github": 0.80, "huggingface": 1.00},
    "conversation": {"openrouter": 1.50, "artificial_analysis": 1.10, "github": 0.80, "huggingface": 1.00},
    "reasoning": {"artificial_analysis": 1.50, "openrouter": 1.20, "github": 0.80, "huggingface": 1.00},
    "math": {"artificial_analysis": 1.50, "openrouter": 1.20, "github": 0.80, "huggingface": 1.00},
}

# Map task hints to capability requirements.
# If the task is in this map, only models with the matching capability are considered.
_TASK_CAPABILITY_MAP: dict[str, str] = {
    "code": "code",
    "coding": "code",
    "chat": "chat",
    "conversation": "chat",
    "reasoning": "reasoning",
    "math": "reasoning",
    "vision": "vision",
    "image": "vision",
    "audio": "audio",
    "tts": "audio",
    "embedding": "embedding",
}

_SWITCHBOARD_ACCOUNT_DISCRIMINATOR = b"SBOracle"
_SWITCHBOARD_PAYLOAD_DISCRIMINATOR = b"SBOD"
_SWITCHBOARD_FIXED_POINT_DECIMALS = 18
_SWITCHBOARD_FRESH_SLOT_CUTOFF = 25_000
_SWITCHBOARD_NORMAL_SLOT_CUTOFF = 150_000

_SWITCHBOARD_FEEDS: tuple[dict[str, Any], ...] = (
    {
        "model": "Qwen/Qwen3.5-9B",
        "feed_hash": "5db4a3ff6719e48d1f7aafc6b472efe721ac541dff60db5cbb0604890d79f4a1",
        "quote_account": "AepiFwnbfCvXwA5gtAysMaxoqdwsGiYCN6gFBLGqZf1S",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen3.5-35B-A3B",
        "feed_hash": "26b2b7735589b977c9eff764171c27c8396af0a3892800f7e0c4c9b1f3ee739c",
        "quote_account": "CXjbMA27eZ4buCvGJoa1nJfpd3fdm1QgRB4LvzyYqcUj",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen2.5-72B-Instruct",
        "feed_hash": "6b7c0e102d574345eab4985a75fe3704103be854405ab56e06af88c5493e37e5",
        "quote_account": "GK2ihF1cF5g7VzK4dwjnQat7ujvKDAiXAWR84FjL3uBg",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "text"),
    },
    {
        "model": "meta-llama/Llama-3.3-70B-Instruct",
        "feed_hash": "04912993aaa414debf49afb48201f3b54152aa6aa7174b7ad0c80238944f3594",
        "quote_account": "6EgRwhE6db1Aqsxzmp9wj6QH2y5ZEji1xe1YdovwmD9g",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "text"),
    },
    {
        "model": "Lightricks/LTX-2.3",
        "feed_hash": "a89277acc660c218a6a54cb76574be963170d61c722568fe4680c730364cf4b3",
        "quote_account": "AEaTUeiwJAReseFz4HVA2yRSKvt2m7V9x6vjvX15SJxj",
        "platform": "huggingface",
        "capabilities": ("video",),
    },
    {
        "model": "moonshotai/Kimi-K2.5",
        "feed_hash": "6b435ba087d48b0e13d4ea5ab7ff78b7d9f9b6fb0a79b0b7686c8be2079bb830",
        "quote_account": "5xmwRtTgcCz6R2KapxpEXVjCNcZCpe24DnCC295S769w",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "zai-org/GLM-OCR",
        "feed_hash": "47c51bcd675d3703282ee4d8962b270aaf175afaca26dc5a676d6b49cafa0e51",
        "quote_account": "H9N2H75LNKNNzZ77Bzkg6V9SwTZM43noFmeFbcAk47Th",
        "platform": "huggingface",
        "capabilities": ("vision",),
    },
    {
        "model": "unsloth/Qwen3.5-35B-A3B-GGUF",
        "feed_hash": "876e7f5487ceadd2763740112eb52f28a400781838fb0bc4905b96a10f4cd5e9",
        "quote_account": "2HDNZ3d88YdHMnyjUenuZZDAU3KcbRFEURSjYuwqxFEA",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen3.5-27B",
        "feed_hash": "ae9281781b42168da39961c7f48600b40a1761a58111aedeadb479c01720a091",
        "quote_account": "63MSM7ycy7wFqGMzf3QFk77xkhJzvGEfXUbUm9SUG9pk",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen3.5-397B-A17B",
        "feed_hash": "af0dca08ca3c7fed18e8675a7efbe174168b9f924416cd7ab54722f3adc4e8bf",
        "quote_account": "HYZBFKo944a6NzjeKWnfGUJnrHYjDDaRY3q42GW1ECik",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "zai-org/GLM-4.7-Flash",
        "feed_hash": "b53ddbf0bf78af8ab102324538e493977e29ddb4d99cff70d89e7e253ac718c3",
        "quote_account": "AM7oDxioaENVeieMmv9tBtN1X6u9BMcAax4c4ZyA9xbv",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "text"),
    },
    {
        "model": "Qwen/Qwen3.5-4B",
        "feed_hash": "9f74eb6f388052c7ed4a7ad96ddc292bce978eb08cd55df30ed5f7c255d044f6",
        "quote_account": "4uU8eMG5MmT43HixUHrC6jddEXq3MuGbX3gmquq3qc1T",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen3.5-0.8B",
        "feed_hash": "12b836cfda5563611246e64fa94a79d5e475ee55b3a71dec67c3b3504681009a",
        "quote_account": "2MShVkXUD36tRbSDC4DbxzZ6sV1n3MXLhNHdDd7sU247",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
    {
        "model": "Qwen/Qwen3-Coder-Next",
        "feed_hash": "86bc2b82f735e103aaf2004963ed14f7af3e13c860033710a21b3eb05fa796c6",
        "quote_account": "g3RRSmg4PJjDNCq3jkTutMB8431UMMtRTNBRpc7UfVV",
        "platform": "huggingface",
        "capabilities": ("code", "chat", "reasoning", "text"),
    },
    {
        "model": "xiaomi/mimo-v2-omni",
        "feed_hash": "720e476068264605d61a3863ee2e31baa59780c88fb27f2d08f1a10c5b826d9e",
        "quote_account": "6kun4gHAS96ZLreu4z3Cpvb7jWY71UCMweUeTMxZpwKw",
        "platform": "openrouter",
        "capabilities": ("code", "chat", "reasoning", "vision", "text"),
    },
)

_CLIENT_CACHE: dict[tuple[str, float, int, int, str, str], "WZRDClient"] = {}


class WZRDError(RuntimeError):
    """Raised when the client cannot produce a usable selection."""


@dataclass(frozen=True)
class Signal:
    """Normalized feed entry."""

    model: str
    trend: str
    score: float
    action: str
    confidence: str
    platform: str
    velocity_ema: Optional[float]
    accel: Optional[float]
    quality_index: Optional[float]
    capabilities: tuple[str, ...]
    raw: Mapping[str, Any]

    @classmethod
    def from_payload(cls, payload: Mapping[str, Any]) -> "Signal":
        model = str(
            payload.get("model")
            or payload.get("channel_id")
            or payload.get("name")
            or ""
        )
        trend = str(
            payload.get("trend")
            or payload.get("velocity_trend")
            or ("insufficient_history" if str(payload.get("confidence", "")).lower() in {"insufficient", "low"} else "stable")
        ).lower()
        score = _coerce_float(payload.get("score", payload.get("momentum_score", 0.0)))
        action = str(payload.get("action") or payload.get("routing_implication") or "observe").lower()
        confidence = str(
            payload.get("confidence")
            or payload.get("history_confidence")
            or ("insufficient" if trend == "insufficient_history" else "unknown")
        ).lower()
        platform = str(
            payload.get("platform")
            or payload.get("source")
            or payload.get("provider")
            or ""
        ).lower()
        velocity_ema = _coerce_float_or_none(payload.get("velocity_ema"))
        accel = _coerce_float_or_none(payload.get("accel"))
        quality_index = _coerce_float_or_none(payload.get("quality_index"))
        caps = payload.get("capabilities", [])
        capabilities = tuple(str(c).lower() for c in caps) if isinstance(caps, (list, tuple)) else ()
        return cls(
            model=model,
            trend=trend,
            score=score,
            action=action,
            confidence=confidence,
            platform=platform,
            velocity_ema=velocity_ema,
            accel=accel,
            quality_index=quality_index,
            capabilities=capabilities,
            raw=dict(payload),
        )


@dataclass(frozen=True)
class FeedMeta:
    """Envelope metadata from the momentum API response."""

    contract_version: str
    generated_at: str
    source: str
    root_seq: Optional[int]

    @classmethod
    def from_payload(cls, payload: Mapping[str, Any]) -> Optional["FeedMeta"]:
        version = payload.get("contract_version")
        if version is None:
            return None
        provenance = payload.get("provenance") or {}
        return cls(
            contract_version=str(version),
            generated_at=str(payload.get("generated_at", "")),
            source=str(provenance.get("source", "")),
            root_seq=provenance.get("root_seq"),
        )


@dataclass(frozen=True)
class PickResult:
    """Structured output from ranking a model or candidate."""

    model: str
    signal_model: Optional[str]
    task: str
    policy: str
    score: float
    raw_score: float
    trend: str
    confidence: str
    action: str
    platform: str
    reason: str
    velocity_ema: Optional[float] = None
    accel: Optional[float] = None
    quality_index: Optional[float] = None
    capabilities: tuple[str, ...] = ()

    def as_dict(self) -> dict[str, Any]:
        return asdict(self)


def clear_cache() -> None:
    """Clear the shared cache used by the default client."""
    get_default_client().clear_cache()


def pick(
    task: str = "general",
    candidates: Optional[Sequence[str]] = None,
    *,
    alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    fallback: Optional[str] = None,
    api_url: Optional[str] = None,
    rpc_url: Optional[str] = None,
    feed_source: Optional[str] = None,
    timeout_seconds: Optional[float] = None,
    cache_ttl_seconds: Optional[int] = None,
    feed_limit: Optional[int] = None,
) -> str:
    """Return the best model string for a task."""
    return get_client(
        **_resolve_client_options(
            api_url=api_url,
            rpc_url=rpc_url,
            feed_source=feed_source,
            timeout_seconds=timeout_seconds,
            cache_ttl_seconds=cache_ttl_seconds,
            feed_limit=feed_limit,
        ),
    ).pick(
        task=task,
        candidates=candidates,
        alias_map=alias_map,
        fallback=fallback,
    )


def pick_details(
    task: str = "general",
    candidates: Optional[Sequence[str]] = None,
    *,
    alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    fallback: Optional[str] = None,
    api_url: Optional[str] = None,
    rpc_url: Optional[str] = None,
    feed_source: Optional[str] = None,
    timeout_seconds: Optional[float] = None,
    cache_ttl_seconds: Optional[int] = None,
    feed_limit: Optional[int] = None,
) -> PickResult:
    """Return the selection plus its scoring metadata."""
    return get_client(
        **_resolve_client_options(
            api_url=api_url,
            rpc_url=rpc_url,
            feed_source=feed_source,
            timeout_seconds=timeout_seconds,
            cache_ttl_seconds=cache_ttl_seconds,
            feed_limit=feed_limit,
        ),
    ).pick_details(
        task=task,
        candidates=candidates,
        alias_map=alias_map,
        fallback=fallback,
    )


def shortlist(
    task: str = "general",
    limit: int = 5,
    *,
    candidates: Optional[Sequence[str]] = None,
    alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    api_url: Optional[str] = None,
    rpc_url: Optional[str] = None,
    feed_source: Optional[str] = None,
    timeout_seconds: Optional[float] = None,
    cache_ttl_seconds: Optional[int] = None,
    feed_limit: Optional[int] = None,
) -> list[PickResult]:
    """Return the top ranked candidates for a task."""
    return get_client(
        **_resolve_client_options(
            api_url=api_url,
            rpc_url=rpc_url,
            feed_source=feed_source,
            timeout_seconds=timeout_seconds,
            cache_ttl_seconds=cache_ttl_seconds,
            feed_limit=feed_limit,
        ),
    ).shortlist(
        task=task,
        limit=limit,
        candidates=candidates,
        alias_map=alias_map,
    )


def compare(
    model_a: str,
    model_b: str,
    *,
    task: str = "general",
    alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    api_url: Optional[str] = None,
    rpc_url: Optional[str] = None,
    feed_source: Optional[str] = None,
    timeout_seconds: Optional[float] = None,
    cache_ttl_seconds: Optional[int] = None,
    feed_limit: Optional[int] = None,
) -> dict[str, Any]:
    """Compare two model names using the live feed."""
    return get_client(
        **_resolve_client_options(
            api_url=api_url,
            rpc_url=rpc_url,
            feed_source=feed_source,
            timeout_seconds=timeout_seconds,
            cache_ttl_seconds=cache_ttl_seconds,
            feed_limit=feed_limit,
        ),
    ).compare(
        model_a=model_a,
        model_b=model_b,
        task=task,
        alias_map=alias_map,
    )


def get_client(
    *,
    api_url: str = DEFAULT_API_URL,
    rpc_url: str = DEFAULT_RPC_URL,
    feed_source: str = DEFAULT_FEED_SOURCE,
    timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
    cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS,
    feed_limit: int = DEFAULT_FEED_LIMIT,
) -> "WZRDClient":
    """Return a configured client instance."""
    key = (api_url, timeout_seconds, cache_ttl_seconds, feed_limit, rpc_url, feed_source)
    client = _CLIENT_CACHE.get(key)
    if client is None:
        client = WZRDClient(
            api_url=api_url,
            rpc_url=rpc_url,
            feed_source=feed_source,
            timeout_seconds=timeout_seconds,
            cache_ttl_seconds=cache_ttl_seconds,
            feed_limit=feed_limit,
        )
        _CLIENT_CACHE[key] = client
    return client


def get_default_client() -> "WZRDClient":
    """Return the module-level default client, creating it on first use."""
    env_client = WZRDClient.from_env()
    return get_client(
        api_url=env_client.api_url,
        rpc_url=env_client.rpc_url,
        feed_source=env_client.feed_source,
        timeout_seconds=env_client.timeout_seconds,
        cache_ttl_seconds=env_client.cache_ttl_seconds,
        feed_limit=env_client.feed_limit,
    )


def _resolve_client_options(
    *,
    api_url: Optional[str],
    rpc_url: Optional[str],
    feed_source: Optional[str],
    timeout_seconds: Optional[float],
    cache_ttl_seconds: Optional[int],
    feed_limit: Optional[int],
) -> dict[str, str | float | int]:
    return {
        "api_url": api_url if api_url is not None else os.environ.get("WZRD_API_URL", DEFAULT_API_URL),
        "rpc_url": rpc_url if rpc_url is not None else os.environ.get("WZRD_RPC_URL", DEFAULT_RPC_URL),
        "feed_source": feed_source if feed_source is not None else os.environ.get("WZRD_FEED_SOURCE", DEFAULT_FEED_SOURCE),
        "timeout_seconds": timeout_seconds if timeout_seconds is not None else _env_float("WZRD_TIMEOUT_SECONDS", DEFAULT_TIMEOUT_SECONDS),
        "cache_ttl_seconds": cache_ttl_seconds if cache_ttl_seconds is not None else _env_int("WZRD_CACHE_TTL_SECONDS", DEFAULT_CACHE_TTL_SECONDS),
        "feed_limit": feed_limit if feed_limit is not None else _env_int("WZRD_FEED_LIMIT", DEFAULT_FEED_LIMIT),
    }


class WZRDClient:
    """Small synchronous client for WZRD model priors."""

    def __init__(
        self,
        *,
        api_url: str = DEFAULT_API_URL,
        rpc_url: str = DEFAULT_RPC_URL,
        feed_source: str = DEFAULT_FEED_SOURCE,
        timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
        cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS,
        feed_limit: int = DEFAULT_FEED_LIMIT,
    ) -> None:
        self.api_url = api_url
        self.rpc_url = rpc_url
        self.feed_source = (feed_source or DEFAULT_FEED_SOURCE).strip().lower()
        self.timeout_seconds = timeout_seconds
        self.cache_ttl_seconds = cache_ttl_seconds
        self.feed_limit = feed_limit
        self._cache: dict[tuple[str, str, int], tuple[float, dict[str, Any]]] = {}

    @classmethod
    def from_env(cls) -> "WZRDClient":
        """Create a client from WZRD_* environment variables."""
        return cls(
            api_url=os.environ.get("WZRD_API_URL", DEFAULT_API_URL),
            rpc_url=os.environ.get("WZRD_RPC_URL", DEFAULT_RPC_URL),
            feed_source=os.environ.get("WZRD_FEED_SOURCE", DEFAULT_FEED_SOURCE),
            timeout_seconds=_env_float("WZRD_TIMEOUT_SECONDS", DEFAULT_TIMEOUT_SECONDS),
            cache_ttl_seconds=_env_int("WZRD_CACHE_TTL_SECONDS", DEFAULT_CACHE_TTL_SECONDS),
            feed_limit=_env_int("WZRD_FEED_LIMIT", DEFAULT_FEED_LIMIT),
        )

    def clear_cache(self) -> None:
        self._cache.clear()

    def feed_meta(self) -> Optional[FeedMeta]:
        """Return envelope metadata from the most recent API response."""
        payload = self._fetch_payload()
        return FeedMeta.from_payload(payload)

    def shortlist(
        self,
        task: str = "general",
        limit: int = 5,
        *,
        candidates: Optional[Sequence[str]] = None,
        alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    ) -> list[PickResult]:
        payload = self._fetch_payload()
        signals = self._normalize_signals(payload)
        if candidates:
            ranked = self._rank_candidates(task, candidates, signals, alias_map)
        else:
            ranked = self._rank_signals(task, signals)
        return ranked[: max(0, limit)]

    def pick(
        self,
        task: str = "general",
        candidates: Optional[Sequence[str]] = None,
        *,
        alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
        fallback: Optional[str] = None,
    ) -> str:
        result = self.pick_details(
            task=task,
            candidates=candidates,
            alias_map=alias_map,
            fallback=fallback,
        )
        return result.model

    def pick_details(
        self,
        task: str = "general",
        candidates: Optional[Sequence[str]] = None,
        *,
        alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
        fallback: Optional[str] = None,
    ) -> PickResult:
        ranked = self.shortlist(
            task=task,
            limit=1,
            candidates=candidates,
            alias_map=alias_map,
        )
        if ranked:
            return ranked[0]
        if fallback:
            return PickResult(
                model=fallback,
                signal_model=None,
                task=task,
                policy="fallback",
                score=0.0,
                raw_score=0.0,
                trend="unknown",
                confidence="unknown",
                action="observe",
                platform="",
                reason="using explicit fallback",
            )
        raise WZRDError("WZRD could not produce a model selection")

    def compare(
        self,
        model_a: str,
        model_b: str,
        *,
        task: str = "general",
        alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    ) -> dict[str, Any]:
        signals = self._normalize_signals(self._fetch_payload())
        alias_map = alias_map or {}
        matched_a = self._match_candidate(model_a, signals, alias_map, task=task)
        matched_b = self._match_candidate(model_b, signals, alias_map, task=task)

        a_entry = self._compare_entry(model_a, matched_a, task)
        b_entry = self._compare_entry(model_b, matched_b, task)

        # Fail closed: do not fabricate winners for untracked models.
        if matched_a is None and matched_b is None:
            return {"a": a_entry, "b": b_entry, "winner": None, "verdict": "neither model is tracked"}
        if matched_a is None:
            return {"a": a_entry, "b": b_entry, "winner": model_b, "verdict": f"{model_a} is not tracked; {model_b} has signal"}
        if matched_b is None:
            return {"a": a_entry, "b": b_entry, "winner": model_a, "verdict": f"{model_b} is not tracked; {model_a} has signal"}

        a = self._signal_result(task, matched_a, policy="compare", model=model_a)
        b = self._signal_result(task, matched_b, policy="compare", model=model_b)

        if a.score > b.score * 1.05:
            verdict = f"{a.model} has stronger momentum"
            winner = a.model
        elif b.score > a.score * 1.05:
            verdict = f"{b.model} has stronger momentum"
            winner = b.model
        else:
            verdict = "Similar momentum"
            winner = None

        return {
            "a": a.as_dict(),
            "b": b.as_dict(),
            "winner": winner,
            "verdict": verdict,
        }

    def _fetch_payload(self) -> dict[str, Any]:
        if self.feed_source == "switchboard":
            return self._fetch_switchboard_payload()
        if self.feed_source == "api":
            return self._fetch_api_payload()

        payload = self._fetch_api_payload()
        if self._normalize_signals(payload):
            return payload
        return self._fetch_switchboard_payload()

    def _fetch_api_payload(self) -> dict[str, Any]:
        key = ("api", self.api_url, self.feed_limit)
        now = time.monotonic()
        cached = self._cache.get(key)
        if cached and (now - cached[0]) < self.cache_ttl_seconds:
            return cached[1]

        url = f"{self.api_url}?limit={self.feed_limit}"
        req = Request(
            url,
            headers={
                "Accept": "application/json",
                "User-Agent": "wzrd-client/0.5.5",
            },
        )

        try:
            with urlopen(req, timeout=self.timeout_seconds) as resp:
                data = json.loads(resp.read())
        except (HTTPError, URLError, TimeoutError, ValueError, OSError):
            if cached:
                return cached[1]
            return {"count": 0, "models": []}

        if not isinstance(data, dict):
            return {"count": 0, "models": []}
        models = data.get("models")
        if not isinstance(models, list):
            data["models"] = []
        self._cache[key] = (now, data)
        return data

    def _fetch_switchboard_payload(self) -> dict[str, Any]:
        key = ("switchboard", self.rpc_url, self.feed_limit)
        now = time.monotonic()
        cached = self._cache.get(key)
        if cached and (now - cached[0]) < self.cache_ttl_seconds:
            return cached[1]

        try:
            result = self._rpc_request(
                "getMultipleAccounts",
                [
                    [entry["quote_account"] for entry in _SWITCHBOARD_FEEDS],
                    {"encoding": "base64", "commitment": "confirmed"},
                ],
            )
        except (HTTPError, URLError, TimeoutError, ValueError, OSError, WZRDError):
            if cached:
                return cached[1]
            return {"count": 0, "models": []}

        data = _switchboard_payload_from_rpc_result(result, limit=self.feed_limit)
        self._cache[key] = (now, data)
        return data

    def _rpc_request(self, method: str, params: Sequence[Any]) -> Any:
        req = Request(
            self.rpc_url,
            data=json.dumps(
                {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": method,
                    "params": list(params),
                }
            ).encode("utf-8"),
            headers={
                "Accept": "application/json",
                "Content-Type": "application/json",
                "User-Agent": "wzrd-client/0.5.6",
            },
            method="POST",
        )
        with urlopen(req, timeout=self.timeout_seconds) as resp:
            data = json.loads(resp.read())
        if not isinstance(data, Mapping):
            raise WZRDError("Invalid RPC response")
        if data.get("error"):
            raise WZRDError(f"RPC error: {data['error']}")
        return data.get("result")

    def _normalize_signals(self, payload: Mapping[str, Any]) -> list[Signal]:
        models = payload.get("models", [])
        if not isinstance(models, list):
            return []
        signals: list[Signal] = []
        for item in models:
            if isinstance(item, Mapping):
                signals.append(Signal.from_payload(item))
        return signals

    def _rank_signals(self, task: str, signals: Sequence[Signal]) -> list[PickResult]:
        # Step 1: Filter by capability if the task maps to one.
        # This ensures pick("code") only considers code-capable models.
        required_cap = _TASK_CAPABILITY_MAP.get(task.lower().strip())
        if required_cap:
            cap_filtered = [s for s in signals if required_cap in s.capabilities]
            if cap_filtered:
                signals = cap_filtered
            # else: fall through to all signals (graceful degradation)

        # Step 2: Filter by confidence + action (trusted → relaxed → all)
        filtered = self._filter_signals(signals)
        if not filtered:
            filtered = self._relax_signals(signals)
        if not filtered:
            filtered = list(signals)

        ranked: list[PickResult] = []
        for signal in filtered:
            ranked.append(self._signal_result(task, signal, policy=self._signal_policy(signal)))
        ranked.sort(key=lambda item: item.score, reverse=True)
        return ranked

    def _rank_candidates(
        self,
        task: str,
        candidates: Sequence[str],
        signals: Sequence[Signal],
        alias_map: Optional[Mapping[str, Sequence[str] | str]] = None,
    ) -> list[PickResult]:
        alias_map = alias_map or {}
        ranked: list[PickResult] = []
        seen = {}
        for index, candidate in enumerate(candidates):
            matched = self._match_candidate(candidate, signals, alias_map, task=task)
            if matched is None:
                ranked.append(
                    PickResult(
                        model=candidate,
                        signal_model=None,
                        task=task,
                        policy="candidate",
                        score=-0.001 * index,
                        raw_score=0.0,
                        trend="unknown",
                        confidence="unknown",
                        action="observe",
                        platform="",
                        reason="no matching signal; preserving candidate order",
                    )
                )
                continue

            signal = matched
            key = (candidate, signal.model)
            if key in seen:
                continue
            seen[key] = True
            ranked.append(self._signal_result(task, signal, policy="candidate", model=candidate))

        ranked.sort(key=lambda item: item.score, reverse=True)
        return ranked

    def _signal_result(
        self,
        task: str,
        signal: Signal,
        *,
        policy: str,
        model: Optional[str] = None,
    ) -> PickResult:
        score = signal.score
        score *= _TREND_WEIGHTS.get(signal.trend, 1.0)
        score *= _CONFIDENCE_WEIGHTS.get(signal.confidence, _CONFIDENCE_WEIGHTS["unknown"])
        score *= _ACTION_WEIGHTS.get(signal.action, 1.0)
        score *= self._task_platform_weight(task, signal.platform)
        if signal.quality_index is not None and signal.quality_index > 0:
            score *= 1.0 + (signal.quality_index / 100.0) * 0.15
        return PickResult(
            model=model or signal.model,
            signal_model=signal.model,
            task=task,
            policy=policy,
            score=round(score, 6),
            raw_score=round(signal.score, 6),
            trend=signal.trend,
            confidence=signal.confidence,
            action=signal.action,
            platform=signal.platform,
            reason=self._reason_for(signal, policy),
            velocity_ema=signal.velocity_ema,
            accel=signal.accel,
            quality_index=signal.quality_index,
            capabilities=signal.capabilities,
        )

    def _signal_policy(self, signal: Signal) -> str:
        if signal.confidence in _TRUSTED_CONFIDENCE and signal.action != "observe":
            return "strict"
        if signal.confidence in _RELAXED_CONFIDENCE and signal.action != "observe":
            return "relaxed"
        return "fallback"

    def _compare_entry(self, name: str, signal: Optional[Signal], task: str) -> dict[str, Any]:
        if signal is None:
            return {"model": name, "status": "not_tracked"}
        return self._signal_result(task, signal, policy="compare", model=name).as_dict()

    def _filter_signals(self, signals: Sequence[Signal]) -> list[Signal]:
        return [signal for signal in signals if signal.confidence in _TRUSTED_CONFIDENCE and signal.action != "observe"]

    def _relax_signals(self, signals: Sequence[Signal]) -> list[Signal]:
        return [signal for signal in signals if signal.confidence in _RELAXED_CONFIDENCE and signal.action != "observe"]

    def _task_platform_weight(self, task: str, platform: str) -> float:
        task_key = task.lower().strip()
        platform_key = platform.lower().strip()
        return _TASK_PLATFORM_WEIGHTS.get(task_key, {}).get(platform_key, 1.0)

    def _reason_for(self, signal: Signal, policy: str) -> str:
        src = signal.platform or "unknown"
        source = str(signal.raw.get("source") or "").lower()
        if source == "switchboard_on_demand":
            parts = [f"{policy} signal from switchboard_on_demand/{src}"]
            age = signal.raw.get("quote_age_slots")
            if age is not None:
                parts.append(f"age_slots={age}")
        else:
            parts = [f"{policy} signal from {src}"]
        if signal.trend in ("surging", "accelerating"):
            parts.append(signal.trend)
        if signal.quality_index is not None:
            parts.append(f"quality={signal.quality_index:.1f}")
        return ", ".join(parts)

    def _match_candidate(
        self,
        candidate: str,
        signals: Sequence[Signal],
        alias_map: Mapping[str, Sequence[str] | str],
        *,
        task: str,
    ) -> Optional[Signal]:
        aliases = [candidate]
        alias_entry = alias_map.get(candidate)
        if alias_entry is not None:
            if isinstance(alias_entry, str):
                aliases.append(alias_entry)
            else:
                aliases.extend(alias_entry)

        normalized_aliases = [_normalize_name(alias) for alias in aliases if alias]
        if not normalized_aliases:
            return None

        best: Optional[Signal] = None
        best_score: Optional[float] = None
        for signal in signals:
            signal_name = _normalize_name(signal.model)
            if not signal_name:
                continue
            if not any(_names_match(alias, signal_name) for alias in normalized_aliases):
                continue
            score = signal.score * _TREND_WEIGHTS.get(signal.trend, 1.0)
            score *= _CONFIDENCE_WEIGHTS.get(signal.confidence, _CONFIDENCE_WEIGHTS["unknown"])
            score *= _ACTION_WEIGHTS.get(signal.action, 1.0)
            score *= self._task_platform_weight(task, signal.platform)
            if best_score is None or score > best_score:
                best_score = score
                best = signal
        return best


def _coerce_float(value: Any) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.0


def _coerce_float_or_none(value: Any) -> Optional[float]:
    if value is None:
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _env_float(name: str, default: float) -> float:
    try:
        return float(os.environ.get(name, default))
    except (TypeError, ValueError):
        return default


def _env_int(name: str, default: int) -> int:
    try:
        return int(os.environ.get(name, default))
    except (TypeError, ValueError):
        return default


def _coerce_int_or_none(value: Any) -> Optional[int]:
    if value is None:
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def _normalize_name(value: str) -> str:
    return re.sub(r"[^a-z0-9]+", "", value.lower())


def _names_match(left: str, right: str) -> bool:
    return left in right or right in left


def _switchboard_payload_from_rpc_result(result: Any, *, limit: int) -> dict[str, Any]:
    values: list[Any] = []
    context_slot: Optional[int] = None
    if isinstance(result, Mapping):
        raw_values = result.get("value")
        if isinstance(raw_values, list):
            values = raw_values
        context = result.get("context")
        if isinstance(context, Mapping):
            context_slot = _coerce_int_or_none(context.get("slot"))

    models: list[dict[str, Any]] = []
    for entry, account in zip(_SWITCHBOARD_FEEDS, values):
        if not isinstance(account, Mapping):
            continue
        encoded = account.get("data")
        if not isinstance(encoded, list) or not encoded or not isinstance(encoded[0], str):
            continue
        try:
            account_data = base64.b64decode(encoded[0])
            quote = _decode_switchboard_quote_account(account_data)
        except (TypeError, ValueError, WZRDError):
            continue

        feed = next((item for item in quote["feeds"] if item["feed_hash"] == entry["feed_hash"]), None)
        if feed is None:
            continue

        quote_age_slots = None
        if context_slot is not None:
            quote_age_slots = max(0, context_slot - int(quote["slot"]))

        score = _fixed_point_to_float(int(feed["value"]), _SWITCHBOARD_FIXED_POINT_DECIMALS)
        models.append(
            {
                "model": entry["model"],
                "score": score,
                "trend": "onchain",
                "action": "onchain",
                "confidence": _switchboard_confidence(quote_age_slots),
                "platform": entry["platform"],
                "velocity_ema": score,
                "capabilities": list(entry["capabilities"]),
                "source": "switchboard_on_demand",
                "quote_account": entry["quote_account"],
                "quote_slot": int(quote["slot"]),
                "quote_age_slots": quote_age_slots,
                "feed_hash": entry["feed_hash"],
                "version": int(quote["version"]),
            }
        )

    models.sort(key=lambda item: _coerce_float(item.get("score")), reverse=True)
    models = models[: max(0, limit)]
    return {
        "contract_version": "wzrd.switchboard.v1",
        "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        "provenance": {
            "source": "switchboard_on_demand",
            "root_seq": None,
            "slot": context_slot,
        },
        "count": len(models),
        "models": models,
    }


def _decode_switchboard_quote_account(data: bytes) -> dict[str, Any]:
    if len(data) < 42:
        raise WZRDError("Switchboard quote account too small")
    if data[:8] != _SWITCHBOARD_ACCOUNT_DISCRIMINATOR:
        raise WZRDError("Invalid Switchboard quote discriminator")

    payload_len = int.from_bytes(data[40:42], "little")
    if payload_len <= 0 or len(data) < 42 + payload_len:
        raise WZRDError("Malformed Switchboard quote payload")

    payload = data[42 : 42 + payload_len]
    if len(payload) < 16:
        raise WZRDError("Malformed Switchboard quote body")

    signature_count = payload[0]
    if signature_count <= 0:
        raise WZRDError("Switchboard quote has no signatures")

    first_offsets = payload[2:16]
    message_offset = int.from_bytes(first_offsets[8:10], "little")
    message_size = int.from_bytes(first_offsets[10:12], "little")
    if message_offset + message_size > len(payload):
        raise WZRDError("Malformed Switchboard quote message bounds")

    message = payload[message_offset : message_offset + message_size]
    if len(message) < 32:
        raise WZRDError("Malformed Switchboard quote message")

    feeds: list[dict[str, Any]] = []
    feed_info_data = message[32:]
    feed_info_size = 49
    for offset in range(0, len(feed_info_data), feed_info_size):
        chunk = feed_info_data[offset : offset + feed_info_size]
        if len(chunk) < feed_info_size:
            break
        feeds.append(
            {
                "feed_hash": chunk[:32].hex(),
                "value": int.from_bytes(chunk[32:48], "little", signed=True),
                "min_oracle_samples": chunk[48],
            }
        )

    oracle_indexes_offset = message_offset + message_size
    slot_offset = oracle_indexes_offset + signature_count
    if slot_offset + 13 > len(payload):
        raise WZRDError("Malformed Switchboard quote trailer")
    tail = payload[slot_offset + 9 : slot_offset + 13]
    if tail != _SWITCHBOARD_PAYLOAD_DISCRIMINATOR:
        raise WZRDError("Malformed Switchboard quote tail discriminator")

    return {
        "slot": int.from_bytes(payload[slot_offset : slot_offset + 8], "little"),
        "version": payload[slot_offset + 8],
        "feeds": feeds,
    }


def _fixed_point_to_float(value: int, decimals: int) -> float:
    return value / (10 ** decimals)


def _switchboard_confidence(age_slots: Optional[int]) -> str:
    if age_slots is None:
        return "unknown"
    if age_slots <= _SWITCHBOARD_FRESH_SLOT_CUTOFF:
        return "high"
    if age_slots <= _SWITCHBOARD_NORMAL_SLOT_CUTOFF:
        return "normal"
    return "low"
