"""
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", "gemini-2.5-flash"),
            "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-4-large"),
            "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", "gemini-2.5-flash"),
            "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-4-large"),
            "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", "gemini-2.5-flash")

        # Pick the best available LLM provider from the host's providers
        for provider_name in ["google", "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-4-large")
        embedding_dim = payload.get("embedding_dim", 1024)

        for provider_name in ["voyage", "google", "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: Optional[str] = None,
        response_format: str = "text",
        max_tokens: Optional[int] = 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: Optional[str] = None,
        max_tokens: Optional[int] = 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: Optional[str] = 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()


# ---------------------------------------------------------------------------
# Per-operation provider factory (Issue #1247)
# ---------------------------------------------------------------------------

def build_operation_provider(
    config: Any,
    operation: str,
) -> Optional[LLMProvider]:
    """Build a dedicated LLMProvider for a specific operation.

    Checks for per-operation provider overrides in CortexConfig
    (e.g. provider_extraction_base_url, provider_extraction_api_key).
    If a per-operation base_url is configured, creates a new HostProvider
    with those credentials. Otherwise returns None (caller should fall
    back to the main shared provider).

    Each operation gets its own HostProvider instance so rate limits,
    usage tracking, and circuit breaking are independent.

    Args:
        config: CortexConfig instance.
        operation: One of 'extraction', 'reconciliation', 'dialectic'.

    Returns:
        A dedicated LLMProvider for the operation, or None if no override configured.
    """
    _KNOWN_OPERATIONS = {"extraction", "reconciliation", "dialectic"}
    if operation not in _KNOWN_OPERATIONS:
        logger.warning(
            "Unknown operation %r passed to build_operation_provider "
            "(known: %s). Proceeding but this may indicate a bug.",
            operation, ", ".join(sorted(_KNOWN_OPERATIONS)),
        )

    base_url = getattr(config, f"provider_{operation}_base_url", "").strip()
    api_key = getattr(config, f"provider_{operation}_api_key", "").strip()

    if not base_url:
        return None

    # Fall back to main provider credentials if per-op key not set
    if not api_key:
        api_key = getattr(config, "provider_llm_api_key", "").strip()
        if not api_key:
            logger.warning(
                "Per-operation provider for %s has base_url=%s but no API key "
                "(neither per-op nor main provider). LLM calls will likely fail.",
                operation, base_url,
            )

    # Determine model: use the operation-specific model from config
    model_field = f"{operation}_model"
    model = getattr(config, model_field, None)
    if not model:
        model = getattr(config, "provider_llm_model", "gemini-2.5-flash")
        logger.warning(
            "Per-operation provider configured for %s (base_url=%s) "
            "but %s is not set — using default model %s",
            operation, base_url, model_field, model,
        )

    from cortex.llm.host_provider import HostProvider

    llm_config = {
        "api_key": api_key,
        "base_url": base_url,
        "model": model,
        "provider": getattr(config, "provider_llm_provider", ""),
    }
    # Per-op providers don't need embedding config
    embedding_config = {
        "api_key": "",
        "base_url": "",
        "model": "",
        "dim": 0,
    }

    provider = HostProvider(llm_config, embedding_config)
    logger.info(
        "Per-operation LLM provider created for %s: base_url=%s, model=%s",
        operation, base_url, model,
    )
    return provider
