"""
Pydantic AI dependency injection and model configuration.

Provides the dependency container (AnalysisDeps) and model configuration (ModelConfig)
for Pydantic AI Agent integration.
"""

import asyncio
from dataclasses import dataclass
import logging
from typing import Literal, Union

from information_composer.pubmed.analyzer.cache import AnalysisCache
from information_composer.pubmed.analyzer.models import AnalysisConfig


logger = logging.getLogger(__name__)

# Type alias for Pydantic AI model (can be string or OpenAIChatModel instance)
# Import delayed to avoid circular imports
PydanticAIModel = Union[str, "OpenAIChatModel"]  # noqa: F821


@dataclass
class ModelConfig:
    """Configuration for LLM model instantiation.

    Encapsulates all parameters needed to create a Pydantic AI model instance
    for either OpenAI or Ollama providers.

    Attributes:
        provider: LLM provider type ("openai" or "ollama")
        model_name: Name of the model (e.g., "gpt-4o-mini", "qwen2.5:latest")
        temperature: Sampling temperature (0.0-2.0, default 0.1)
        api_key: API key for OpenAI (optional, uses env var if not provided)
        base_url: Custom base URL for API endpoint (optional)

    Example:
        >>> # OpenAI configuration
        >>> config = ModelConfig(
        ...     provider="openai",
        ...     model_name="gpt-4o-mini",
        ...     api_key="sk-..."
        ... )
        >>>
        >>> # Ollama configuration
        >>> config = ModelConfig(
        ...     provider="ollama",
        ...     model_name="qwen2.5:latest",
        ...     base_url="http://localhost:11434"
        ... )
    """

    provider: Literal["openai", "ollama"]
    model_name: str
    temperature: float = 0.1
    api_key: str | None = None
    base_url: str | None = None

    def to_pydantic_ai_model(self) -> str:
        """Convert configuration to Pydantic AI model specification.

        For OpenAI, returns a model name string like "openai:gpt-4o-mini".
        For Ollama, returns a model name string like "ollama:qwen2.5:latest".

        Note: Pydantic AI uses string-based model selection. For Ollama,
        the base_url and other parameters are passed separately to the Agent.

        Returns:
            Model specification string for Pydantic AI Agent

        Example:
            >>> config = ModelConfig(provider="openai", model_name="gpt-4o-mini")
            >>> config.to_pydantic_ai_model()
            'openai:gpt-4o-mini'
        """
        if self.provider == "openai":
            return f"openai:{self.model_name}"
        elif self.provider == "ollama":
            return f"ollama:{self.model_name}"
        else:
            raise ValueError(f"Unsupported provider: {self.provider}")

    def to_model_string(self) -> PydanticAIModel:
        """Generate Pydantic AI compatible model string or instance.

        Returns a string for simple configurations or an OpenAIChatModel instance
        when custom provider configuration is needed (e.g., custom API keys or URLs).

        For OpenAI without custom config: returns "openai:model-name" string
        For OpenAI with custom config: returns OpenAIChatModel with OpenAIProvider
        For Ollama: returns OpenAIChatModel with OllamaProvider (always uses provider)

        Returns:
            String model specification or OpenAIChatModel instance

        Example:
            >>> # Simple OpenAI (uses env var for API key)
            >>> config = ModelConfig(provider="openai", model_name="gpt-4o-mini")
            >>> config.to_model_string()
            'openai:gpt-4o-mini'
            >>>
            >>> # Ollama (always uses OllamaProvider)
            >>> config = ModelConfig(provider="ollama", model_name="qwen2.5:latest")
            >>> model = config.to_model_string()
            >>> # Returns OpenAIChatModel with OllamaProvider
        """
        if self.provider == "openai":
            if self.api_key or self.base_url:
                # Use explicit provider configuration
                from pydantic_ai.models.openai import OpenAIChatModel
                from pydantic_ai.providers.openai import OpenAIProvider

                provider = OpenAIProvider(api_key=self.api_key, base_url=self.base_url)
                return OpenAIChatModel(self.model_name, provider=provider)
            else:
                # Use simple string format (env var for API key)
                return f"openai:{self.model_name}"
        elif self.provider == "ollama":
            from pydantic_ai.models.openai import OpenAIChatModel
            from pydantic_ai.providers.ollama import OllamaProvider

            base_url = self.base_url or "http://localhost:11434"
            provider = OllamaProvider(base_url=base_url)
            return OpenAIChatModel(self.model_name, provider=provider)
        else:
            raise ValueError(f"Unsupported provider: {self.provider}")


@dataclass
class AnalysisDeps:
    """Dependency container for Pydantic AI analysis agents.

    Provides access to analysis configuration, caching, and concurrency control
    for PubMed paper analysis. This container is injected into Pydantic AI Agents
    to provide runtime dependencies.

    Attributes:
        config: Analysis configuration (provider, model, temperature, etc.)
        cache: Analysis result cache for avoiding redundant LLM calls
        semaphore: Async semaphore for limiting concurrent API requests

    Example:
        >>> from information_composer.pubmed.analyzer.models import AnalysisConfig
        >>> from information_composer.pubmed.analyzer.cache import AnalysisCache
        >>>
        >>> config = AnalysisConfig(
        ...     analysis_types=["summary"],
        ...     llm_provider="openai",
        ...     llm_model="gpt-4o-mini"
        ... )
        >>> cache = AnalysisCache(enabled=True)
        >>> semaphore = asyncio.Semaphore(3)
        >>>
        >>> deps = AnalysisDeps(config=config, cache=cache, semaphore=semaphore)
    """

    config: AnalysisConfig
    cache: AnalysisCache
    semaphore: asyncio.Semaphore

    def get_model_config(self) -> ModelConfig:
        """Extract ModelConfig from AnalysisConfig.

        Converts the AnalysisConfig into a ModelConfig suitable for
        creating Pydantic AI model instances.

        Returns:
            ModelConfig with provider-specific settings

        Example:
            >>> deps = AnalysisDeps(config=config, cache=cache, semaphore=sem)
            >>> model_config = deps.get_model_config()
            >>> model_config.provider
            'openai'
        """
        return ModelConfig(
            provider=self.config.llm_provider,  # type: ignore[arg-type]
            model_name=self.config.llm_model,
            temperature=self.config.temperature,
            api_key=self.config.llm_api_key,
            base_url=self.config.llm_base_url,
        )

    def create_model(self) -> str:
        """Create a Pydantic AI model from configuration.

        Convenience method that extracts ModelConfig and converts it
        to a Pydantic AI model specification string.

        Returns:
            Pydantic AI model specification string

        Example:
            >>> deps = AnalysisDeps(config=config, cache=cache, semaphore=sem)
            >>> model = deps.create_model()
            >>> # Use with Pydantic AI Agent
            >>> # agent = Agent(model, deps_type=AnalysisDeps)
        """
        model_config = self.get_model_config()
        return model_config.to_pydantic_ai_model()


def create_model_from_config(config: AnalysisConfig) -> str:
    """Factory function to create a Pydantic AI model from AnalysisConfig.

    Creates a model specification string suitable for use with Pydantic AI Agent.
    This is a convenience function for direct model creation without needing
    to instantiate AnalysisDeps.

    Args:
        config: Analysis configuration containing provider, model, API settings

    Returns:
        Pydantic AI model specification string

    Example:
        >>> from information_composer.pubmed.analyzer.models import AnalysisConfig
        >>>
        >>> config = AnalysisConfig(
        ...     analysis_types=["summary"],
        ...     llm_provider="openai",
        ...     llm_model="gpt-4o-mini",
        ...     llm_api_key="sk-..."
        ... )
        >>> model = create_model_from_config(config)
        >>> model
        'openai:gpt-4o-mini'
        >>>
        >>> # Ollama example
        >>> config = AnalysisConfig(
        ...     analysis_types=["summary"],
        ...     llm_provider="ollama",
        ...     llm_model="qwen2.5:latest",
        ...     llm_base_url="http://localhost:11434"
        ... )
        >>> model = create_model_from_config(config)
        >>> model
        'ollama:qwen2.5:latest'
    """
    model_config = ModelConfig(
        provider=config.llm_provider,  # type: ignore[arg-type]
        model_name=config.llm_model,
        temperature=config.temperature,
        api_key=config.llm_api_key,
        base_url=config.llm_base_url,
    )
    return model_config.to_pydantic_ai_model()


def create_model_config_from_analysis_config(config: AnalysisConfig) -> ModelConfig:
    """Create ModelConfig from AnalysisConfig.

    Factory function that converts an existing AnalysisConfig into a
    ModelConfig suitable for use with Pydantic AI.

    Maintains backward compatibility with existing analyzer initialization.
    Maps AnalysisConfig fields to ModelConfig fields:
    - llm_provider -> provider
    - llm_model -> model_name
    - llm_api_key -> api_key
    - llm_base_url -> base_url
    - temperature -> temperature

    Args:
        config: Analysis configuration from existing analyzer setup

    Returns:
        ModelConfig configured for Pydantic AI

    Example:
        >>> from information_composer.pubmed.analyzer.models import AnalysisConfig
        >>>
        >>> analysis_config = AnalysisConfig(
        ...     analysis_types=["summary"],
        ...     llm_provider="openai",
        ...     llm_model="gpt-4o-mini",
        ... )
        >>> model_config = create_model_config_from_analysis_config(analysis_config)
        >>> model_config.provider
        'openai'
        >>> model_config.model_name
        'gpt-4o-mini'
    """
    return ModelConfig(
        provider=config.llm_provider,  # type: ignore[arg-type]
        model_name=config.llm_model,
        temperature=config.temperature,
        api_key=config.llm_api_key,
        base_url=config.llm_base_url,
    )


def create_pydantic_ai_model(config: AnalysisConfig) -> PydanticAIModel:
    """Direct factory to create Pydantic AI model from AnalysisConfig.

    Creates a Pydantic AI model specification (string or instance) directly
    from an AnalysisConfig. This is the simplest way to get a model that
    can be passed to a Pydantic AI Agent.

    For OpenAI without custom config: returns string like "openai:gpt-4o-mini"
    For OpenAI with custom config: returns OpenAIChatModel instance
    For Ollama: returns OpenAIChatModel with OllamaProvider

    Args:
        config: Analysis configuration containing provider, model, API settings

    Returns:
        Pydantic AI model specification (str or OpenAIChatModel)

    Example:
        >>> from information_composer.pubmed.analyzer.models import AnalysisConfig
        >>>
        >>> config = AnalysisConfig(
        ...     analysis_types=["summary"],
        ...     llm_provider="openai",
        ...     llm_model="gpt-4o-mini",
        ... )
        >>> model = create_pydantic_ai_model(config)
        >>> model
        'openai:gpt-4o-mini'
        >>>
        >>> # Use with Pydantic AI Agent
        >>> # from pydantic_ai import Agent
        >>> # agent = Agent(model, deps_type=AnalysisDeps)
    """
    model_config = create_model_config_from_analysis_config(config)
    return model_config.to_model_string()
