"""Marketplace node selection engine: fetch, filter, rank, register.

Catalog URL is typically the frontend GET /api/marketplace/nodes endpoint
(e.g. from env LLMHOSTS_MARKETPLACE_CATALOG_URL). Response shape:
  { "nodes": [ { "id", "name", "tunnel_url", "models", "price_cents_per_hour",
      "latency_ms", "reputation_score", "status", "last_seen", ... } ] }
  reputation_score is 0-100 (computed from status, lastSeen, latencyMs).
"""

from __future__ import annotations

import json
import logging
from typing import TYPE_CHECKING, Any

import httpx

if TYPE_CHECKING:
    from llmhosts.proxy.dispatcher import BackendDispatcher

logger = logging.getLogger(__name__)

_CATALOG_TIMEOUT = 15.0


async def fetch_nodes(catalog_url: str) -> list[dict[str, Any]]:
    """Fetch marketplace nodes from *catalog_url* (GET JSON).

    Expects response body to be either ``{"nodes": [...]}`` or a direct list.
    Returns the list of node dicts; empty list on error or non-2xx.
    """
    if not (catalog_url or "").strip():
        logger.warning("fetch_nodes: empty catalog_url")
        return []
    try:
        async with httpx.AsyncClient(timeout=_CATALOG_TIMEOUT) as client:
            resp = await client.get(catalog_url)
            resp.raise_for_status()
            data = resp.json()
    except Exception as exc:
        logger.warning("fetch_nodes failed for %s: %s", catalog_url, exc)
        return []
    if isinstance(data, list):
        return data
    if isinstance(data, dict) and "nodes" in data:
        nodes = data["nodes"]
        return list(nodes) if isinstance(nodes, list) else []
    logger.warning("fetch_nodes: unexpected response shape from %s", catalog_url)
    return []


def filter_by_model(nodes: list[dict[str, Any]], model: str) -> list[dict[str, Any]]:
    """Return nodes that serve *model*.

    A node is included if any of its ``models`` equals *model* or starts with
    ``model + ":"`` (Ollama-style tags). Match is case-sensitive.
    """
    if not model or not nodes:
        return list(nodes) if not model else []
    result = []
    for node in nodes:
        raw = node.get("models")
        if raw is None:
            continue
        models = list(raw) if isinstance(raw, (list, tuple)) else []
        for m in models:
            if m == model or (isinstance(m, str) and m.startswith(f"{model}:")):
                result.append(node)
                break
    return result


def rank_nodes(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Sort *nodes* by price (asc), then latency (asc); nulls last.

    Keys: ``price_cents_per_hour``, ``latency_ms``. Missing or null values
    sort after any numeric value (nodes with both set come first).
    """

    def sort_key(n: dict[str, Any]) -> tuple[bool, bool, float, float]:
        price = n.get("price_cents_per_hour")
        latency = n.get("latency_ms")
        # Nulls last: (price_is_none, latency_is_none, price, latency)
        return (
            price is None,
            latency is None,
            float(price) if price is not None else 0.0,
            float(latency) if latency is not None else 0.0,
        )

    return sorted(nodes, key=sort_key)


async def load_and_register(
    dispatcher: BackendDispatcher,
    catalog_url: str,
) -> int:
    """Fetch nodes from *catalog_url*, rank, and register as remote backends.

    No model filter is applied (all nodes). Nodes are ranked by price then
    latency; rank index becomes backend priority (0, 1, 2, ...). Existing
    remote backends are unregistered before registering the new set.
    Returns the number of backends registered.
    """
    nodes = await fetch_nodes(catalog_url)
    if not nodes:
        dispatcher.unregister_remote_backends()
        return 0
    ranked = rank_nodes(nodes)
    devices: list[dict[str, Any]] = []
    for priority, node in enumerate(ranked):
        name = node.get("name")
        tunnel_url = node.get("tunnel_url")
        if not name or not tunnel_url:
            continue
        raw_models = node.get("models")
        if isinstance(raw_models, str):
            try:
                models = json.loads(raw_models) if raw_models else []
            except json.JSONDecodeError:
                models = []
        else:
            models = list(raw_models) if raw_models else []
        devices.append(
            {
                "id": node.get("id"),
                "name": name,
                "tunnel_url": tunnel_url.rstrip("/"),
                "models": models,
                "priority": priority,
            }
        )
    dispatcher.unregister_remote_backends()
    count = dispatcher.register_remote_backends(devices, priority_key="priority")
    logger.info("Marketplace router: registered %d remote backend(s) from %s", count, catalog_url)
    return count
