"""
cortex/llm/provider_router.py — ProviderRouter: Mode-based LLM provider selection.

Routes all LLM calls to the appropriate provider based on the configured
provider mode (host | byok | bundled). This replaces the old FallbackLLMProvider
cascade with a simpler, single-provider-per-mode architecture.

Modes:
    host     — Uses credentials from the host application (OpenClaw plugin).
               Default mode for self-hosters. Zero extra config needed.
    byok     — Uses credentials from the Cortex config/dashboard.
               Same as HostProvider but reads keys from CortexConfig.
    bundled  — ElectricSheep provides API keys server-side. (Future — not yet implemented.)

Design:
    - ProviderRouter IS a LLMProvider (delegates all ABC methods to the inner provider)
    - No fallback chains — if the provider fails, the operation fails cleanly
    - Provider is selected once at init time based on config.provider_mode
    - Runtime reconfiguration via configure_from_host() for host mode

Dependencies: cortex.llm.host_provider, cortex.config.CortexConfig.
Implements: cortex.llm.provider.LLMProvider ABC.
"""

from __future__ import annotations

import logging
from typing import Any, Optional

from cortex.llm.provider import (
    CompletionResponse,
    EmbedResponse,
    ExtractionResponse,
    LLMProvider,
)

logger = logging.getLogger(__name__)


class ProviderRouter(LLMProvider):
    """Routes LLM calls to the appropriate provider based on config mode.

    The router acts as a transparent proxy — all LLMProvider methods are
    delegated to ``self.provider``. The mode determines which concrete
    provider implementation is used.

    In host mode, the provider starts unconfigured and is populated when
    the OpenClaw plugin calls POST /api/v1/config/providers. Until then,
    all calls will raise RuntimeError.

    In byok mode, the provider reads credentials from CortexConfig fields
    at init time and is immediately usable.

    Args:
        config: CortexConfig instance with provider_mode and related fields.
    """

    PROVIDER_NAME = "router"

    def __init__(self, config: Any) -> None:
        super().__init__()
        self._config = config
        self._mode = getattr(config, "provider_mode", "host")
        self.provider: Optional[LLMProvider] = None

        if self._mode == "host":
            self._init_host_provider(config)
        elif self._mode == "byok":
            self._init_byok_provider(config)
        elif self._mode == "bundled":
            raise NotImplementedError(
                "Bundled provider mode is not yet implemented. "
                "Use 'host' or 'byok' mode instead."
            )
        else:
            raise ValueError(
                f"Unknown provider_mode: {self._mode!r}. "
                "Supported modes: host, byok, bundled."
            )

        logger.info(
            "ProviderRouter initialised: mode=%s, provider=%s",
            self._mode,
            type(self.provider).__name__ if self.provider else "None (awaiting config)",
        )

    def _init_host_provider(self, config: Any) -> None:
        """Initialize HostProvider with whatever config is available.

        In host mode, the provider may start with empty credentials if the
        host hasn't called POST /api/v1/config/providers yet. The provider
        will be reconfigured when credentials arrive.
        """
        from cortex.llm.host_provider import HostProvider

        llm_config = {
            "api_key": getattr(config, "provider_llm_api_key", ""),
            "base_url": getattr(config, "provider_llm_base_url", ""),
            "model": getattr(config, "provider_llm_model", "gpt-4.1-nano"),
            "provider": getattr(config, "provider_llm_provider", ""),
        }
        embedding_config = {
            "api_key": getattr(config, "provider_embedding_api_key", ""),
            "base_url": getattr(config, "provider_embedding_base_url", ""),
            "model": getattr(config, "provider_embedding_model", "voyage-code-3"),
            "dim": getattr(config, "provider_embedding_dim", 1024),
            "provider": getattr(config, "provider_embedding_provider", ""),
        }
        self.provider = HostProvider(llm_config, embedding_config)

    def _init_byok_provider(self, config: Any) -> None:
        """Initialize HostProvider with BYOK credentials from config.

        BYOK mode uses the same HostProvider implementation but reads
        credentials from the CortexConfig fields (set via TOML/env/dashboard).
        """
        from cortex.llm.host_provider import HostProvider

        llm_config = {
            "api_key": getattr(config, "provider_llm_api_key", ""),
            "base_url": getattr(config, "provider_llm_base_url", ""),
            "model": getattr(config, "provider_llm_model", "gpt-4.1-nano"),
            "provider": getattr(config, "provider_llm_provider", ""),
        }
        embedding_config = {
            "api_key": getattr(config, "provider_embedding_api_key", ""),
            "base_url": getattr(config, "provider_embedding_base_url", ""),
            "model": getattr(config, "provider_embedding_model", "voyage-code-3"),
            "dim": getattr(config, "provider_embedding_dim", 1024),
            "provider": getattr(config, "provider_embedding_provider", ""),
        }
        self.provider = HostProvider(llm_config, embedding_config)

    # --- Host-mode runtime configuration ------------------------------------

    def configure_from_host(self, payload: dict[str, Any]) -> list[str]:
        """Reconfigure the provider from host-supplied credentials.

        Called by POST /api/v1/config/providers when the OpenClaw plugin
        sends its provider map at startup.

        Args:
            payload: Dict with keys: mode, providers, extraction_model,
                     embedding_model, embedding_dim.

        Returns:
            List of provider names that were configured (e.g. ["openai", "voyage"]).
        """
        providers = payload.get("providers", {})
        configured: list[str] = []

        # Determine LLM provider credentials
        llm_config: dict[str, Any] = {}
        extraction_model = payload.get("extraction_model", "gpt-4.1-nano")

        # Pick the best available LLM provider from the host's providers
        for provider_name in ["openai", "anthropic", "venice"]:
            if provider_name in providers:
                p = providers[provider_name]
                llm_config = {
                    "api_key": p.get("api_key", ""),
                    "base_url": p.get("base_url", ""),
                    "model": extraction_model,
                    "provider": provider_name,
                }
                configured.append(provider_name)
                break

        # Determine embedding provider credentials
        embedding_config: dict[str, Any] = {}
        embedding_model = payload.get("embedding_model", "voyage-code-3")
        embedding_dim = payload.get("embedding_dim", 1024)

        for provider_name in ["voyage", "openai"]:
            if provider_name in providers:
                p = providers[provider_name]
                embedding_config = {
                    "api_key": p.get("api_key", ""),
                    "base_url": p.get("base_url", ""),
                    "model": embedding_model,
                    "dim": embedding_dim,
                    "provider": provider_name,
                }
                if provider_name not in configured:
                    configured.append(provider_name)
                break

        # Reconfigure the inner HostProvider
        if self.provider is not None and hasattr(self.provider, "reconfigure"):
            self.provider.reconfigure(
                llm_config=llm_config if llm_config else None,
                embedding_config=embedding_config if embedding_config else None,
            )
        else:
            from cortex.llm.host_provider import HostProvider
            self.provider = HostProvider(
                llm_config=llm_config or {"model": extraction_model},
                embedding_config=embedding_config or {"model": embedding_model, "dim": embedding_dim},
            )

        logger.info(
            "ProviderRouter: configured from host — providers=%s, "
            "llm_model=%s, embed_model=%s",
            configured, extraction_model, embedding_model,
        )
        return configured

    # --- LLMProvider ABC delegation -----------------------------------------

    def _ensure_provider(self) -> LLMProvider:
        """Return the inner provider, raising if not configured."""
        if self.provider is None:
            raise RuntimeError(
                f"ProviderRouter ({self._mode} mode): no provider configured. "
                "In host mode, wait for POST /api/v1/config/providers from the host plugin."
            )
        return self.provider

    async def complete(
        self,
        system: str,
        user: str,
        model: str | None = None,
        response_format: str = "text",
        max_tokens: int | None = None,
        temperature: float = 0.0,
    ) -> CompletionResponse:
        """Delegate to the inner provider's complete()."""
        return await self._ensure_provider().complete(
            system=system, user=user, model=model,
            response_format=response_format,
            max_tokens=max_tokens, temperature=temperature,
        )

    async def extract(
        self,
        system: str,
        user: str,
        schema: dict[str, Any],
        model: str | None = None,
        max_tokens: int | None = None,
        temperature: float = 0.0,
    ) -> ExtractionResponse:
        """Delegate to the inner provider's extract()."""
        return await self._ensure_provider().extract(
            system=system, user=user, schema=schema,
            model=model, max_tokens=max_tokens,
            temperature=temperature,
        )

    async def embed(
        self,
        texts: list[str],
        model: str | None = None,
        input_type: str = "query",
    ) -> EmbedResponse:
        """Delegate to the inner provider's embed()."""
        return await self._ensure_provider().embed(
            texts=texts, model=model, input_type=input_type,
        )

    # --- Compatibility methods for existing codebase ------------------------

    def provider_names(self) -> list[str]:
        """Return list of active provider names (for logging/health checks)."""
        if self.provider is not None:
            name = getattr(self.provider, "PROVIDER_NAME", "unknown")
            return [name]
        return []

    def usage_summary(self) -> dict[str, Any]:
        """Aggregate usage from the inner provider."""
        if self.provider is not None:
            return self.provider.usage_summary()
        return super().usage_summary()
