"""Platform classifier key client — fetches RR-owned LLM key for classification.

Robot Resources absorbs the cost of the classifier LLM call. This client
fetches the key from the Platform API, caches it in memory (1hr TTL), and
gracefully degrades to None on any failure.

Never raises — all methods return a value or None.
"""

from __future__ import annotations

import asyncio
import json
import time
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse

import httpx
import structlog

logger = structlog.get_logger()

_DEFAULT_CONFIG_PATH = Path.home() / ".robot-resources" / "config.json"
_DEFAULT_PLATFORM_URL = "https://api.robotresources.ai"

_ALLOWED_PLATFORM_HOSTS = frozenset({
    "api.robotresources.ai",
    "platform.robotresources.ai",
    "localhost",
})

# Cache TTL: 1 hour
_CACHE_TTL_SECONDS: float = 3600.0

# Retry backoff after a failed fetch
_RETRY_BACKOFF_SECONDS: float = 60.0

# HTTP timeout for fetching the key
_FETCH_TIMEOUT: float = 5.0


@dataclass(frozen=True)
class ClassifierKeyInfo:
    """Immutable classifier key data from the platform."""

    provider: str
    model: str
    api_key: str


class ClassifierKeyClient:
    """Async client that fetches and caches the RR-owned classifier key.

    Reads the user's ``api_key`` from ``~/.robot-resources/config.json``
    to authenticate with the Platform API.

    Thread-safe via asyncio.Lock (prevents thundering-herd on concurrent
    requests during cache miss).

    Never raises — ``get_key()`` returns ``ClassifierKeyInfo`` or ``None``.
    """

    def __init__(
        self,
        config_path: Path | None = None,
        platform_url: str | None = None,
    ) -> None:
        self._config_path = config_path or _DEFAULT_CONFIG_PATH
        self._platform_url: str | None = None
        self._user_api_key: str | None = None
        self._enabled: bool = False

        # Validate constructor-supplied platform_url against allowlist
        if platform_url is not None:
            parsed = urlparse(platform_url)
            if parsed.scheme in ("https", "http") and parsed.hostname in _ALLOWED_PLATFORM_HOSTS:
                self._platform_url = platform_url
            else:
                logger.warning(
                    "classifier_key_invalid_platform_url",
                    url=platform_url,
                    reason="host not in allowlist",
                )

        # Cache state
        self._cached: ClassifierKeyInfo | None = None
        self._cached_at: float = 0.0

        # Retry backoff state
        self._last_failure: float = 0.0

        # Concurrency guard
        self._lock = asyncio.Lock()

        self._load_config()

    def _load_config(self) -> None:
        """Read user API key and platform URL from config file."""
        try:
            raw = self._config_path.read_text(encoding="utf-8")
            config = json.loads(raw)
        except (FileNotFoundError, OSError):
            return
        except json.JSONDecodeError:
            logger.warning("classifier_key_invalid_config", path=str(self._config_path))
            return

        key = config.get("api_key", "")
        if not key:
            return

        self._user_api_key = key
        self._enabled = True

        if self._platform_url is None:
            candidate = config.get("platform_url") or _DEFAULT_PLATFORM_URL
            parsed = urlparse(candidate)
            valid_scheme = parsed.scheme in ("https", "http")
            valid_host = parsed.hostname in _ALLOWED_PLATFORM_HOSTS
            if not valid_scheme or not valid_host:
                logger.warning(
                    "classifier_key_invalid_platform_url",
                    url=candidate,
                    reason="host not in allowlist",
                )
                return
            self._platform_url = candidate

    @property
    def enabled(self) -> bool:
        """Whether the client has a user API key to authenticate with."""
        return self._enabled

    async def get_key(self) -> ClassifierKeyInfo | None:
        """Get the classifier key, using cache when valid.

        Returns cached key if within TTL. On cache miss or expiry,
        fetches from Platform. On fetch failure, returns stale cache
        if available, else None.

        Never raises.
        """
        if not self._enabled:
            return None

        now = time.monotonic()

        # Fast path: fresh cache (no lock needed for read)
        if self._cached is not None and (now - self._cached_at) < _CACHE_TTL_SECONDS:
            return self._cached

        async with self._lock:
            # Re-check after acquiring lock (another coroutine may have refreshed)
            now = time.monotonic()
            if self._cached is not None and (now - self._cached_at) < _CACHE_TTL_SECONDS:
                return self._cached

            # Retry backoff: don't hammer Platform after a failure
            if self._last_failure > 0 and (now - self._last_failure) < _RETRY_BACKOFF_SECONDS:
                # Return stale cache if available
                if self._cached is not None:
                    logger.debug("classifier_key_stale_cache", reason="retry_backoff")
                    return self._cached
                return None

            # Fetch from Platform
            result = await self._fetch()
            if result is not None:
                self._cached = result
                self._cached_at = time.monotonic()
                self._last_failure = 0.0
                logger.info(
                    "classifier_key_fetched",
                    provider=result.provider,
                    model=result.model,
                )
                return result

            # Fetch failed — stale-while-revalidate
            self._last_failure = time.monotonic()
            if self._cached is not None:
                logger.warning("classifier_key_stale_cache", reason="fetch_failed")
                return self._cached

            return None

    async def _fetch(self) -> ClassifierKeyInfo | None:
        """Fetch classifier key from Platform API. Returns None on any failure."""
        url = f"{self._platform_url}/v1/classifier-key"
        try:
            async with httpx.AsyncClient(timeout=_FETCH_TIMEOUT) as client:
                response = await client.get(
                    url,
                    headers={"Authorization": f"Bearer {self._user_api_key}"},
                )

            if response.status_code != 200:
                logger.warning(
                    "classifier_key_http_error",
                    status=response.status_code,
                    url=url,
                )
                return None

            body = response.json()
            data = body.get("data", {})

            provider = data.get("provider")
            model = data.get("model")
            api_key = data.get("api_key")

            if not all([provider, model, api_key]):
                logger.warning("classifier_key_invalid_response", data=data)
                return None

            return ClassifierKeyInfo(
                provider=str(provider),
                model=str(model),
                api_key=str(api_key),
            )

        except httpx.TimeoutException:
            logger.warning("classifier_key_timeout", url=url)
            return None
        except Exception:
            logger.warning("classifier_key_fetch_error", exc_info=True)
            return None


# ---------------------------------------------------------------------------
# Singleton
# ---------------------------------------------------------------------------

_client: ClassifierKeyClient | None = None


def get_classifier_key_client() -> ClassifierKeyClient:
    """Get the classifier key client singleton, initializing on first call.

    Always returns a ClassifierKeyClient instance (never None).
    If config is missing, the client is disabled (get_key returns None).
    """
    global _client  # noqa: PLW0603
    if _client is not None:
        return _client
    _client = ClassifierKeyClient()
    return _client
