"""Client for the LLMHosts cloud router intelligence API.

Calls ``/api/route`` on the LLMHosts cloud with request *metadata* only —
never user content.  The cloud endpoint returns a backend selection decision
that the local dispatcher can use instead of (or in addition to) local Tier 0
rule-based routing.

Circuit-breaker pattern prevents cascading failures when the cloud API is
unreachable.  An LRU response cache avoids redundant calls for similar
requests within a session.

Architecture notes
------------------
- The cloud API currently uses rule-based (Tier 0) logic — same as the local
  engine — but is designed to transparently upgrade to kNN (Tier 1) and
  ModernBERT (Tier 2) when issue #285 lands.  Clients need no changes.
- Caller must handle ``None`` return and fall back to local routing.
- All I/O is async; never blocks the FastAPI event loop.
"""

from __future__ import annotations

import hashlib
import logging
import time
from collections import OrderedDict
from typing import Any

logger = logging.getLogger(__name__)


class _CacheEntry:
    """Single LRU cache entry for a cloud routing response."""

    __slots__ = ("backend", "confidence", "expires_at", "reasoning", "tier_used")

    def __init__(self, data: dict[str, Any], ttl_seconds: int = 3600) -> None:
        self.backend: str = data["backend"]
        self.confidence: float = data["confidence"]
        self.tier_used: int = data["tier_used"]
        self.reasoning: str = data["reasoning"]
        self.expires_at: float = time.monotonic() + ttl_seconds


class CloudRouterClient:
    """Calls the LLMHosts cloud router API with circuit breaker + LRU cache fallback.

    Parameters
    ----------
    api_base_url:
        Base URL of the LLMHosts cloud (default ``https://llmhosts.com``).
    api_key:
        Optional bearer token.  If omitted, relies on session cookies (not
        suitable for server-to-server calls without a session).

    Examples
    --------
    ::

        client = CloudRouterClient(api_key="lh_sk_...")
        decision = await client.route(
            available_backends=["llama3.1:8b", "qwen3:14b"],
            context_length_tokens=1200,
            privacy_tier="personal",
        )
        if decision is None:
            decision = local_fallback(...)
    """

    #: Maximum number of entries in the response cache.
    CACHE_SIZE: int = 1000
    #: Consecutive failures before the circuit breaker opens.
    CIRCUIT_BREAKER_THRESHOLD: int = 10
    #: Seconds to wait after opening before attempting a retry.
    CIRCUIT_BREAKER_COOLDOWN: int = 300
    #: HTTP request timeout in seconds.
    API_TIMEOUT: float = 2.0

    def __init__(
        self,
        api_base_url: str = "https://llmhosts.com",
        api_key: str | None = None,
    ) -> None:
        self._api_base = api_base_url.rstrip("/")
        self._api_key = api_key
        self._cache: OrderedDict[str, _CacheEntry] = OrderedDict()
        self._consecutive_failures: int = 0
        self._circuit_open_until: float = 0.0

    # ------------------------------------------------------------------
    # Cache helpers
    # ------------------------------------------------------------------

    def _cache_key(
        self,
        backends: list[str],
        hardware_tier: str,
        context_tokens: int,
        privacy_tier: str,
    ) -> str:
        """Produce a stable, privacy-safe cache key.

        Context lengths are bucketed to 512-token intervals so that requests
        of 1000 tokens and 1100 tokens reuse the same cached decision.
        """
        context_bucket = (context_tokens // 512) * 512
        raw = f"{sorted(backends)}|{hardware_tier}|{context_bucket}|{privacy_tier}"
        return hashlib.sha256(raw.encode()).hexdigest()[:16]

    def _get_cached(self, key: str) -> dict[str, Any] | None:
        """Return a cached entry (updating LRU order) or ``None`` if absent/expired."""
        entry = self._cache.get(key)
        if entry is None:
            return None
        if time.monotonic() > entry.expires_at:
            del self._cache[key]
            return None
        self._cache.move_to_end(key)
        return {
            "backend": entry.backend,
            "confidence": entry.confidence,
            "tier_used": entry.tier_used,
            "reasoning": f"(cached) {entry.reasoning}",
        }

    def _put_cached(self, key: str, data: dict[str, Any]) -> None:
        """Insert a response into the LRU cache, evicting the oldest entry if full."""
        if len(self._cache) >= self.CACHE_SIZE:
            self._cache.popitem(last=False)
        self._cache[key] = _CacheEntry(data)

    # ------------------------------------------------------------------
    # Circuit breaker helpers
    # ------------------------------------------------------------------

    def _is_circuit_open(self) -> bool:
        """Return True when the circuit breaker is open (cloud API suppressed)."""
        if self._circuit_open_until > 0:
            if time.monotonic() < self._circuit_open_until:
                return True
            # Cooldown elapsed — reset the breaker for a retry
            logger.info("Cloud router circuit breaker: resetting after cooldown")
            self._consecutive_failures = 0
            self._circuit_open_until = 0.0
        return False

    def _record_failure(self) -> None:
        """Increment failure counter and open the circuit breaker when threshold hit."""
        self._consecutive_failures += 1
        if self._consecutive_failures >= self.CIRCUIT_BREAKER_THRESHOLD:
            self._circuit_open_until = time.monotonic() + self.CIRCUIT_BREAKER_COOLDOWN
            logger.warning(
                "Cloud router circuit breaker OPEN (%d failures) — will retry in %ds",
                self._consecutive_failures,
                self.CIRCUIT_BREAKER_COOLDOWN,
            )

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def route(
        self,
        available_backends: list[str],
        hardware_tier: str = "unknown",
        privacy_tier: str = "external",
        context_length_tokens: int = 0,
        cache_hit: bool = False,
        request_hash: str = "",
    ) -> dict[str, Any] | None:
        """Request a routing decision from the cloud API.

        Parameters
        ----------
        available_backends:
            Local backends (model identifiers) that can handle the request.
        hardware_tier:
            GPU tier string, e.g. ``"rtx4060_8gb"``.
        privacy_tier:
            One of ``"personal"`` | ``"private"`` | ``"external"``.
        context_length_tokens:
            Estimated token count of the request (used for routing heuristics).
        cache_hit:
            Whether a cache hit is already known for this request.
        request_hash:
            sha256 of the request content.  Sent to cloud for logging only —
            never send actual user content.

        Returns
        -------
        dict or None
            Routing decision ``{"backend", "confidence", "tier_used", "reasoning"}``,
            or ``None`` if the cloud API is unavailable.  Callers must handle
            ``None`` and fall back to local routing.

        Notes
        -----
        This method never raises.  All failures are logged and swallowed so
        that the caller's hot path is never interrupted by a cloud outage.
        """
        if not available_backends:
            return None

        cache_key = self._cache_key(available_backends, hardware_tier, context_length_tokens, privacy_tier)

        # Fast path: local cache
        cached = self._get_cached(cache_key)
        if cached:
            return cached

        # Circuit breaker: cloud is known-unavailable
        if self._is_circuit_open():
            logger.debug("Cloud router circuit open — using local fallback")
            return None

        try:
            import httpx  # optional dependency — guard import

            payload: dict[str, Any] = {
                "request_hash": request_hash,
                "available_backends": available_backends,
                "hardware_tier": hardware_tier,
                "privacy_tier": privacy_tier,
                "context_length_tokens": context_length_tokens,
                "cache_hit": cache_hit,
            }
            headers: dict[str, str] = {}
            if self._api_key:
                headers["Authorization"] = f"Bearer {self._api_key}"

            async with httpx.AsyncClient(timeout=self.API_TIMEOUT) as client:
                t0 = time.monotonic()
                response = await client.post(
                    f"{self._api_base}/api/route",
                    json=payload,
                    headers=headers,
                )
                latency_ms = (time.monotonic() - t0) * 1000

            if response.status_code == 200:
                data: dict[str, Any] = response.json()
                self._put_cached(cache_key, data)
                self._consecutive_failures = 0
                logger.debug(
                    "Cloud router: backend=%s confidence=%.2f tier=%d latency=%.0fms",
                    data.get("backend"),
                    data.get("confidence"),
                    data.get("tier_used", 0),
                    latency_ms,
                )
                return data

            logger.warning("Cloud router: HTTP %d", response.status_code)
            self._record_failure()
            return None

        except Exception as exc:
            logger.debug("Cloud router unavailable: %s", exc)
            self._record_failure()
            return None

    # ------------------------------------------------------------------
    # Introspection helpers (testing / metrics)
    # ------------------------------------------------------------------

    def cache_size(self) -> int:
        """Return the number of entries currently in the response cache."""
        return len(self._cache)

    def is_circuit_open(self) -> bool:
        """Return True when the circuit breaker is suppressing cloud calls."""
        return self._is_circuit_open()
