"""LLM model factory for multi-provider model management with caching.

Provides centralized model creation, caching, provider management, and tool binding
with support for multiple LLM providers and intelligent optimization strategies.
"""

import logging
from typing import Dict, List, Optional

from cadence_sdk.base.loggable import Loggable
from langchain_core.language_models import BaseChatModel

from ...config.settings import Settings
from .providers import (
    AnthropicProvider,
    AzureOpenAIProvider,
    BaseLLMProvider,
    GoogleGenAIProvider,
    ModelConfig,
    OpenAIProvider,
)


class ModelCacheManager(Loggable):
    """Intelligent model instance cache manager for performance optimization.

    This class manages a cache of LLM model instances to prevent the overhead of
    repeatedly creating identical model configurations. The caching strategy is
    based on provider, model name, and temperature settings to ensure cache hits
    for identical configurations while maintaining model isolation.

    Cache Benefits:
        - Eliminates model initialization overhead for repeated requests
        - Reduces API connection setup time for providers
        - Maintains provider-specific optimizations and configurations
        - Provides memory-efficient storage of model instances

    Cache Strategy:
        - Key Generation: Combines provider + model + temperature for uniqueness
        - Instance Reuse: Returns cached instances for identical configurations
        - Memory Management: Automatic cleanup of unused model instances
        - Provider Isolation: Separate caching per provider to prevent conflicts

    Example:
        ```python
        cache_manager = ModelCacheManager()

        config = ModelConfig(provider="openai", model_name="gpt-4", temperature=0.7)
        cache_key = cache_manager.get_cache_key(config)

        cached_model = cache_manager.get_cached_model(cache_key)
        if cached_model:
            return cached_model

        new_model = create_model(config)
        cache_manager.cache_model(cache_key, new_model)
        ```
    """

    def __init__(self):
        super().__init__()
        self._cache: Dict[str, BaseChatModel] = {}

    @staticmethod
    def get_cache_key(config: ModelConfig) -> str:
        """Generate a stable cache key for model configuration.

        Creates a unique cache key based on the essential model parameters that
        affect model behavior: provider, model name, and temperature. This ensures
        that identical configurations reuse cached instances while different
        configurations get separate instances.

        Args:
            config: Model configuration containing provider, model name, and settings

        Returns:
            Stable cache key string in format "provider:model_name:temperature"

        Example:
            ```python
            config = ModelConfig(provider="openai", model_name="gpt-4", temperature=0.7)
            key = ModelCacheManager.get_cache_key(config)
            ```
        """
        return f"{config.provider}:{config.model_name}:{config.temperature}"

    def get_cached_model(self, cache_key: str) -> Optional[BaseChatModel]:
        """Retrieve a cached model instance if available.

        Attempts to retrieve a previously cached model instance for the given cache key.
        Returns None if no cached instance is available, indicating that a new model
        instance needs to be created and cached.

        Args:
            cache_key: Cache key generated by get_cache_key() method

        Returns:
            Cached BaseChatModel instance if available, None otherwise

        Example:
            ```python
            cache_key = "openai:gpt-4:0.7"
            cached_model = cache_manager.get_cached_model(cache_key)
            if cached_model:
                return cached_model
            else:
                new_model = create_new_model()
                cache_manager.cache_model(cache_key, new_model)
            ```
        """
        return self._cache.get(cache_key)

    def cache_model(self, cache_key: str, model: BaseChatModel) -> None:
        """Store a model in the cache.

        Args:
            cache_key: Key under which to store the model.
            model: The instantiated ``BaseChatModel`` to cache.
        """
        self._cache[cache_key] = model
        self.logger.debug(f"Cached model: {cache_key}")

    def clear_cache(self) -> None:
        """Clear all cached models."""
        self._cache.clear()
        self.logger.debug("Cleared model cache")

    def get_cache_stats(self) -> Dict[str, int]:
        """Return cache statistics.

        Returns:
            A dictionary with basic metrics, e.g., ``{"cached_models": N}``.
        """
        return {"cached_models": len(self._cache)}


class ProviderRegistry(Loggable):
    """Registry for managing LLM providers.

    Pre-registers OpenAI, Anthropic (aliased as ``"anthropic"`` and
    ``"claude"``), and Google GenAI (aliased as ``"google"`` and
    ``"gemini"``).
    """

    def __init__(self):
        super().__init__()
        self._providers: Dict[str, BaseLLMProvider] = {
            "openai": OpenAIProvider(),
            "azure-openai": AzureOpenAIProvider(),
            "azure": AzureOpenAIProvider(),
            "anthropic": AnthropicProvider(),
            "claude": AnthropicProvider(),
            "google": GoogleGenAIProvider(),
            "gemini": GoogleGenAIProvider(),
        }
        self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")

    def get_provider(self, provider_name: str) -> Optional[BaseLLMProvider]:
        """Return provider by name.

        Args:
            provider_name: Provider key or alias.

        Returns:
            The provider instance if registered; otherwise, ``None``.
        """
        return self._providers.get(provider_name)

    def register_provider(self, name: str, provider: BaseLLMProvider) -> None:
        """Register a new provider.

        Args:
            name: Provider name under which it will be registered.
            provider: Concrete ``BaseLLMProvider`` implementation.
        """
        self._providers[name] = provider
        self.logger.debug(f"Registered provider: {name}")

    def get_available_providers(self) -> List[str]:
        """Return the list of available provider names."""
        return list(self._providers.keys())

    def is_provider_available(self, provider_name: str) -> bool:
        """Return whether a provider is registered and available."""
        return provider_name in self._providers


class LLMModelFactory(Loggable):
    """Create and manage chat models with provider and cache handling.

    Responsibilities:
    - Resolve provider credentials from ``Settings`` when missing in ``ModelConfig``.
    - Cache constructed models to avoid redundant instantiation.
    - Bind tools for agent-specific execution.
    """

    def __init__(self, settings: Settings):
        super().__init__()
        self.settings = settings
        self.cache_manager = ModelCacheManager()
        self.provider_registry = ProviderRegistry()

    def create_base_model(self, config: ModelConfig) -> BaseChatModel:
        """Create a base model without tools.

        Args:
            config: Model configuration (provider, model, temperature, etc.).

        Returns:
            A provider-specific ``BaseChatModel`` instance.

        Raises:
            ValueError: If the provider is unknown or credentials are missing.
        """
        cache_key = self.cache_manager.get_cache_key(config)

        cached_model = self.cache_manager.get_cached_model(cache_key)
        if cached_model:
            return cached_model

        provider = self.provider_registry.get_provider(config.provider)
        if not provider:
            raise ValueError(f"Unknown provider: {config.provider}")

        self._ensure_api_key(config)

        model = provider.create_model(config)
        self.cache_manager.cache_model(cache_key, model)
        self.logger.debug(f"Created model: {cache_key}")
        return model

    def _ensure_api_key(self, config: ModelConfig) -> None:
        """Ensure an API key is present in the configuration.

        If ``config.api_key`` is empty, resolve it via ``Settings``. Raises
        ``ValueError`` if a key cannot be found for the given provider.
        """
        if not config.api_key:
            config.api_key = self.settings.get_api_key_for_provider(config.provider)
            if not config.api_key:
                raise ValueError(f"No API key found for provider: {config.provider}")

    def get_default_model_config(self) -> ModelConfig:
        """Create a default model configuration.

        Returns:
            A ``ModelConfig`` using the default provider from settings and a
            reasonable default model name for that provider.
        """

        return ModelConfig(
            provider=self.settings.default_llm_provider,
            model_name=self.settings.get_default_provider_llm_model(),
            api_key=self.settings.get_api_key_for_provider(self.settings.default_llm_provider),
            additional_params=self.settings.get_provider_extra_params(self.settings.default_llm_provider),
        )

    def get_available_providers(self) -> List[str]:
        """Return the list of available provider names."""
        return self.provider_registry.get_available_providers()

    def clear_cache(self) -> None:
        """Clear the model cache."""
        self.cache_manager.clear_cache()

    def get_cache_stats(self) -> Dict[str, int]:
        """Return cache statistics from the cache manager."""
        return self.cache_manager.get_cache_stats()

    def register_provider(self, name: str, provider: BaseLLMProvider) -> None:
        """Register a new LLM provider.

        Args:
            name: Provider name.
            provider: Concrete implementation to register.
        """
        self.provider_registry.register_provider(name, provider)
