"""Abstract LLM interface supporting multiple providers.

Provides a unified API for generating text completions across OpenAI,
Anthropic, Google (Gemini), and a deterministic mock provider for testing.
"""

from __future__ import annotations

import logging
import os
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)


class LLMProvider(ABC):
    """Abstract base for LLM text-generation providers."""

    @abstractmethod
    def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.8,
        max_tokens: int = 4096,
    ) -> str:
        """Generate a text completion.

        Parameters
        ----------
        system_prompt : str
            System-level instructions (role, rules, operator library, etc.).
        user_prompt : str
            Per-iteration user prompt (memory signal, library state, etc.).
        temperature : float
            Sampling temperature; higher = more creative.
        max_tokens : int
            Maximum tokens in the response.

        Returns
        -------
        str
            Raw text response from the model.
        """

    @property
    @abstractmethod
    def provider_name(self) -> str:
        """Human-readable provider name."""


class OpenAIProvider(LLMProvider):
    """OpenAI API provider (GPT-4, GPT-4o, etc.)."""

    def __init__(
        self,
        model: str = "gpt-4o",
        api_key: Optional[str] = None,
    ) -> None:
        self.model = model
        self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
        self._client: Any = None

    def _get_client(self) -> Any:
        if self._client is None:
            try:
                from openai import OpenAI
            except ImportError:
                raise ImportError(
                    "openai package is required for OpenAIProvider. "
                    "Install with: pip install openai"
                )
            self._client = OpenAI(api_key=self.api_key)
        return self._client

    def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.8,
        max_tokens: int = 4096,
    ) -> str:
        client = self._get_client()
        logger.debug("OpenAI request: model=%s temp=%.2f", self.model, temperature)
        response = client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            temperature=temperature,
            max_tokens=max_tokens,
        )
        text = response.choices[0].message.content or ""
        logger.debug("OpenAI response: %d chars", len(text))
        return text

    @property
    def provider_name(self) -> str:
        return f"openai/{self.model}"


class AnthropicProvider(LLMProvider):
    """Anthropic Claude API provider with adaptive thinking support."""

    def __init__(
        self,
        model: str = "claude-sonnet-4-6",
        api_key: Optional[str] = None,
        use_thinking: bool = True,
        effort: str = "max",
    ) -> None:
        self.model = model
        self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
        self.use_thinking = use_thinking
        self.effort = effort
        self._client: Any = None

    def _get_client(self) -> Any:
        if self._client is None:
            try:
                import anthropic
            except ImportError:
                raise ImportError(
                    "anthropic package is required for AnthropicProvider. "
                    "Install with: pip install anthropic"
                )
            self._client = anthropic.Anthropic(api_key=self.api_key, base_url='https://api.minimaxi.com/anthropic')
        return self._client

    def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 1,
        max_tokens: int = 32000,
    ) -> str:
        client = self._get_client()
        logger.debug(
            "Anthropic request: model=%s thinking=%s effort=%s",
            self.model,
            self.use_thinking,
            self.effort,
        )

        kwargs: dict = {
            "model": self.model,
            "system": system_prompt,
            "messages": [{"role": "user", "content": user_prompt}],
            "max_tokens": max_tokens,
        }

        if self.use_thinking:
            kwargs["thinking"] = {"type": "adaptive"}
            kwargs["temperature"] = 1  # Required for thinking mode
            kwargs["output_config"] = {"effort": self.effort}
        else:
            kwargs["temperature"] = temperature

        text_parts: list[str] = []
        with client.messages.stream(**kwargs) as stream:
            for text in stream.text_stream:
                text_parts.append(text)
        full_text = "".join(text_parts)
        logger.debug("Anthropic response: %d chars", len(full_text))
        return full_text

    @property
    def provider_name(self) -> str:
        return f"anthropic/{self.model}"


class GoogleProvider(LLMProvider):
    """Google Gemini API provider (paper uses Gemini 3.0 Flash)."""

    def __init__(
        self,
        model: str = "gemini-2.0-flash",
        api_key: Optional[str] = None,
    ) -> None:
        self.model = model
        self.api_key = api_key or os.environ.get("GOOGLE_API_KEY", "")
        self._client: Any = None

    def _get_client(self) -> Any:
        if self._client is None:
            try:
                import google.generativeai as genai
            except ImportError:
                raise ImportError(
                    "google-generativeai package is required for GoogleProvider. "
                    "Install with: pip install google-generativeai"
                )
            genai.configure(api_key=self.api_key)
            self._client = genai.GenerativeModel(
                self.model,
                generation_config={"max_output_tokens": 8192},
            )
        return self._client

    def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.8,
        max_tokens: int = 4096,
    ) -> str:
        client = self._get_client()
        logger.debug("Google request: model=%s temp=%.2f", self.model, temperature)
        combined = f"{system_prompt}\n\n---\n\n{user_prompt}"
        response = client.generate_content(
            combined,
            generation_config={
                "temperature": temperature,
                "max_output_tokens": max_tokens,
            },
        )
        text = response.text if response.text else ""
        logger.debug("Google response: %d chars", len(text))
        return text

    @property
    def provider_name(self) -> str:
        return f"google/{self.model}"


# ---------------------------------------------------------------------------
# Factory
# ---------------------------------------------------------------------------

_PROVIDER_MAP: Dict[str, type] = {
    "openai": OpenAIProvider,
    "anthropic": AnthropicProvider,
    "google": GoogleProvider,
}


def create_provider(config: Dict[str, Any]) -> LLMProvider:
    """Factory function to instantiate an LLM provider from config.

    Parameters
    ----------
    config : dict
        Must contain ``"provider"`` key (one of "openai", "anthropic",
        "google", "mock").  Additional keys are passed as kwargs to the
        provider constructor:
        - ``"model"`` : model identifier
        - ``"api_key"`` : API key (overrides env var)

    Returns
    -------
    LLMProvider
    """
    provider_name = config.get("provider", "mock")
    cls = _PROVIDER_MAP.get(provider_name)
    if cls is None:
        raise ValueError(
            f"Unknown LLM provider '{provider_name}'. "
            f"Available: {sorted(_PROVIDER_MAP.keys())}"
        )

    kwargs: Dict[str, Any] = {}
    if "model" in config:
        kwargs["model"] = config["model"]
    if "api_key" in config:
        kwargs["api_key"] = config["api_key"]

    logger.info(
        "Creating LLM provider: %s (kwargs=%s)", provider_name, list(kwargs.keys())
    )
    return cls(**kwargs)
