"""
Abstraction pour les fournisseurs AI (OpenAI, Claude, Gemini)

Gemini utilise le nouveau SDK google-genai avec support pour :
- Code Execution
- Structured Outputs (Pydantic)
- Function Calling
- Thinking/Reasoning
- Context Caching
- Batch API
"""

import asyncio
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING

if TYPE_CHECKING:
    from pydantic import BaseModel
    from src.core.gemini_config import GeminiFeatureConfig


class AIProvider(ABC):
    """Classe abstraite pour les fournisseurs AI"""

    def __init__(self, api_key: str, model: str, timeout_seconds: int = 60, max_retries: int = 3):
        self.api_key = api_key
        self.model = model
        self.timeout_seconds = timeout_seconds
        self.max_retries = max_retries

    @abstractmethod
    async def generate_content(self, prompt: str) -> str:
        """
        Génère du contenu à partir d'un prompt

        Args:
            prompt: Le prompt à envoyer au modèle

        Returns:
            Le texte généré

        Raises:
            Exception: Si la génération échoue
        """
        pass

    async def generate_content_with_reasoning(self, prompt: str) -> Tuple[str, Optional[str]]:
        """
        Génère du contenu avec reasoning si disponible

        Args:
            prompt: Le prompt à envoyer au modèle

        Returns:
            Tuple (contenu, reasoning) où reasoning est None si non disponible

        Raises:
            Exception: Si la génération échoue
        """
        # Par défaut, retourner le contenu sans reasoning
        content = await self.generate_content(prompt)
        return content, None

    async def generate_content_with_retry(
        self, prompt: str, timeout_multiplier: float = 1.0
    ) -> str:
        """
        Génère du contenu avec retry et gestion des timeouts

        Args:
            prompt: Le prompt à envoyer au modèle
            timeout_multiplier: Multiplicateur pour le timeout (défaut: 1.0)

        Returns:
            Le texte généré

        Raises:
            Exception: Si toutes les tentatives échouent
        """
        timeout = int(self.timeout_seconds * timeout_multiplier)

        for attempt in range(self.max_retries):
            try:
                response = await asyncio.wait_for(self.generate_content(prompt), timeout=timeout)
                return response

            except asyncio.TimeoutError:
                if attempt == self.max_retries - 1:
                    raise
                await asyncio.sleep(2**attempt)
            except Exception as e:
                if attempt == self.max_retries - 1:
                    raise
                await asyncio.sleep(2**attempt)

        raise Exception("Toutes les tentatives ont échoué")


class GeminiProvider(AIProvider):
    """
    Provider pour Google Gemini avec fonctionnalités avancées

    Utilise le nouveau SDK google-genai pour supporter :
    - Code Execution : exécution de Python en sandbox
    - Structured Outputs : réponses JSON validées via Pydantic
    - Function Calling : appel automatique de fonctions Python
    - Thinking/Reasoning : affichage du processus de raisonnement
    - Context Caching : cache API pour réduire les coûts (~50%)
    - Batch API : traitement en lot à coût réduit (-50%)
    """

    def __init__(self, api_key: str, model: str, timeout_seconds: int = 60, max_retries: int = 3):
        super().__init__(api_key, model, timeout_seconds, max_retries)
        from google import genai

        self._client = genai.Client(api_key=api_key)

    async def generate_content(self, prompt: str) -> str:
        """Génère du contenu avec Gemini (méthode de base)"""
        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
        )
        return response.text

    async def generate_with_config(
        self, prompt: str, feature_config: "GeminiFeatureConfig"
    ) -> str:
        """
        Génère du contenu avec une configuration de fonctionnalités

        Args:
            prompt: Le prompt à envoyer
            feature_config: Configuration des fonctionnalités Gemini

        Returns:
            Le texte généré
        """
        config = self._build_config(feature_config)
        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=config,
        )
        return response.text

    async def generate_structured(
        self, prompt: str, schema: Type["BaseModel"]
    ) -> "BaseModel":
        """
        Génère du contenu avec sortie structurée Pydantic

        Args:
            prompt: Le prompt à envoyer
            schema: Classe Pydantic pour valider la réponse

        Returns:
            Instance du modèle Pydantic avec les données
        """
        from google.genai import types

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(
                response_mime_type="application/json",
                response_schema=schema,
            ),
        )
        return response.parsed

    async def generate_with_code_execution(self, prompt: str) -> Dict[str, Any]:
        """
        Génère du contenu avec exécution de code Python

        Le modèle peut générer et exécuter du code Python en sandbox.

        Args:
            prompt: Le prompt à envoyer

        Returns:
            Dict avec 'text', 'executable_code', 'code_output', 'error'
        """
        from google.genai import types
        from src.core.gemini_config import CodeExecutionResult

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(
                tools=[types.Tool(code_execution=types.ToolCodeExecution())]
            ),
        )

        result = CodeExecutionResult(text="")

        for part in response.candidates[0].content.parts:
            if hasattr(part, "text") and part.text:
                result.text += part.text
            if hasattr(part, "executable_code") and part.executable_code:
                result.executable_code = part.executable_code.code
            if hasattr(part, "code_execution_result") and part.code_execution_result:
                result.code_output = part.code_execution_result.output

        return {
            "text": result.text,
            "executable_code": result.executable_code,
            "code_output": result.code_output,
            "error": result.error,
        }

    async def generate_with_tools(
        self, prompt: str, tools: List[Callable], auto_execute: bool = True
    ) -> str:
        """
        Génère du contenu avec appel de fonctions

        Args:
            prompt: Le prompt à envoyer
            tools: Liste de fonctions Python à mettre à disposition
            auto_execute: Si True, exécute automatiquement les fonctions

        Returns:
            Le texte généré (avec résultats des fonctions si auto_execute)
        """
        from google.genai import types

        config_kwargs = {"tools": tools}
        if not auto_execute:
            config_kwargs["automatic_function_calling"] = types.AutomaticFunctionCallingConfig(
                disable=True
            )

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(**config_kwargs),
        )
        return response.text

    async def generate_with_thinking(
        self, prompt: str, budget: int = 1024
    ) -> Tuple[str, Optional[str]]:
        """
        Génère du contenu avec raisonnement visible (thinking)

        Args:
            prompt: Le prompt à envoyer
            budget: Budget de tokens pour le raisonnement (min 128)

        Returns:
            Tuple (réponse, pensées) où pensées peut être None
        """
        from google.genai import types

        # Minimum budget is 128 for gemini-2.5+
        actual_budget = max(128, budget)

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(
                thinking_config=types.ThinkingConfig(
                    thinking_budget=actual_budget,
                    include_thoughts=True,
                )
            ),
        )

        thoughts = None
        if hasattr(response, "thoughts"):
            thoughts = response.thoughts
        elif hasattr(response, "candidates") and response.candidates:
            # Try to extract from candidates
            for part in response.candidates[0].content.parts:
                if hasattr(part, "thought") and part.thought:
                    thoughts = part.thought
                    break

        return response.text, thoughts

    async def generate_content_with_reasoning(
        self, prompt: str
    ) -> Tuple[str, Optional[str]]:
        """
        Implémentation de la méthode abstraite avec thinking

        Args:
            prompt: Le prompt à envoyer

        Returns:
            Tuple (contenu, reasoning)
        """
        return await self.generate_with_thinking(prompt, budget=1024)

    # === Context Caching ===

    async def create_cache(
        self,
        contents: List[str],
        system_instruction: str,
        display_name: str = "aetheris_cache",
        ttl_seconds: int = 3600,
    ) -> str:
        """
        Crée un cache de contexte pour réduire les coûts API

        Args:
            contents: Contenu à mettre en cache
            system_instruction: Instruction système
            display_name: Nom du cache
            ttl_seconds: Durée de vie en secondes

        Returns:
            Nom du cache créé
        """
        from google.genai import types

        cached_content = await asyncio.to_thread(
            self._client.caches.create,
            model=self.model,
            config=types.CreateCachedContentConfig(
                contents=[types.Content(role="user", parts=[types.Part.from_text(c) for c in contents])],
                system_instruction=system_instruction,
                display_name=display_name,
                ttl=f"{ttl_seconds}s",
            ),
        )
        return cached_content.name

    async def generate_with_cache(self, prompt: str, cache_name: str) -> str:
        """
        Génère du contenu en utilisant un cache existant

        Args:
            prompt: Le prompt à envoyer
            cache_name: Nom du cache à utiliser

        Returns:
            Le texte généré
        """
        from google.genai import types

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(cached_content=cache_name),
        )
        return response.text

    async def get_cache(self, cache_name: str) -> Optional[Dict[str, Any]]:
        """Récupère les informations d'un cache"""
        try:
            cache = await asyncio.to_thread(self._client.caches.get, name=cache_name)
            return {
                "name": cache.name,
                "display_name": cache.display_name,
                "model": cache.model,
                "expire_time": str(cache.expire_time) if cache.expire_time else None,
            }
        except Exception:
            return None

    async def delete_cache(self, cache_name: str) -> bool:
        """Supprime un cache"""
        try:
            await asyncio.to_thread(self._client.caches.delete, name=cache_name)
            return True
        except Exception:
            return False

    # === Batch API ===

    async def create_batch_job(self, requests: List[Dict[str, Any]]) -> str:
        """
        Crée un job batch pour traitement à coût réduit (-50%)

        Args:
            requests: Liste de requêtes au format:
                [{"key": "req1", "contents": "prompt1"}, ...]

        Returns:
            Nom/ID du job créé
        """
        batch_requests = []
        for req in requests:
            batch_requests.append({
                "key": req.get("key", f"request_{len(batch_requests)}"),
                "request": {
                    "contents": [{"parts": [{"text": req["contents"]}], "role": "user"}],
                    "generation_config": req.get("config", {"response_modalities": ["TEXT"]}),
                },
            })

        job = await asyncio.to_thread(
            self._client.batches.create,
            model=self.model,
            src=batch_requests,
        )
        return job.name

    async def get_batch_job(self, job_name: str) -> Dict[str, Any]:
        """Récupère l'état d'un job batch"""
        job = await asyncio.to_thread(self._client.batches.get, name=job_name)
        return {
            "name": job.name,
            "state": str(job.state),
            "create_time": str(job.create_time) if hasattr(job, "create_time") else None,
        }

    # === Google Search (Grounding) ===

    async def generate_with_search(self, prompt: str) -> str:
        """
        Génère du contenu avec recherche Google intégrée

        Args:
            prompt: Le prompt à envoyer

        Returns:
            Le texte généré avec informations du web
        """
        from google.genai import types

        response = await asyncio.to_thread(
            self._client.models.generate_content,
            model=self.model,
            contents=prompt,
            config=types.GenerateContentConfig(
                tools=[types.Tool(google_search=types.GoogleSearch())]
            ),
        )
        return response.text

    # === Helper Methods ===

    def _build_config(self, fc: "GeminiFeatureConfig"):
        """
        Construit la configuration GenerateContentConfig

        Args:
            fc: Configuration des fonctionnalités

        Returns:
            Instance de GenerateContentConfig
        """
        from google.genai import types

        if fc is None:
            return None

        tools = list(fc.tools) if fc.tools else []

        # Add code execution tool if enabled
        if fc.enable_code_execution:
            tools.append(types.Tool(code_execution=types.ToolCodeExecution()))

        # Add Google Search if enabled
        if fc.enable_google_search:
            tools.append(types.Tool(google_search=types.GoogleSearch()))

        config_dict = {
            "temperature": fc.temperature,
            "max_output_tokens": fc.max_output_tokens,
            "top_p": fc.top_p,
            "top_k": fc.top_k,
        }

        if tools:
            config_dict["tools"] = tools

        if fc.response_schema:
            config_dict["response_mime_type"] = "application/json"
            config_dict["response_schema"] = fc.response_schema

        if fc.enable_thinking and fc.thinking_budget > 0:
            config_dict["thinking_config"] = types.ThinkingConfig(
                thinking_budget=max(128, fc.thinking_budget),
                include_thoughts=fc.include_thoughts,
            )

        if fc.use_cached_content:
            config_dict["cached_content"] = fc.use_cached_content

        if fc.system_instruction:
            config_dict["system_instruction"] = fc.system_instruction

        if fc.stop_sequences:
            config_dict["stop_sequences"] = fc.stop_sequences

        return types.GenerateContentConfig(**config_dict)


class OpenAIProvider(AIProvider):
    """Provider pour OpenAI"""

    def __init__(self, api_key: str, model: str, timeout_seconds: int = 60, max_retries: int = 3):
        super().__init__(api_key, model, timeout_seconds, max_retries)
        try:
            from openai import OpenAI

            self._client = OpenAI(api_key=api_key)
        except ImportError:
            raise ImportError(
                "Le package 'openai' est requis pour utiliser OpenAI. "
                "Installez-le avec: pip install openai"
            )

    async def generate_content(self, prompt: str) -> str:
        """Génère du contenu avec OpenAI"""
        response = await asyncio.to_thread(
            self._client.chat.completions.create,
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            timeout=self.timeout_seconds,
        )
        return response.choices[0].message.content

    async def generate_content_with_reasoning(self, prompt: str) -> Tuple[str, Optional[str]]:
        """Génère du contenu avec reasoning pour les modèles o1"""
        # Les modèles o1 (o1-preview, o1-mini) ont un reasoning
        if self.model.startswith("o1"):
            response = await asyncio.to_thread(
                self._client.chat.completions.create,
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                timeout=self.timeout_seconds,
            )
            content = response.choices[0].message.content
            # Le reasoning est dans response.choices[0].message.reasoning si disponible
            reasoning = getattr(response.choices[0].message, "reasoning", None)
            return content, reasoning
        else:
            # Pour les autres modèles, pas de reasoning
            content = await self.generate_content(prompt)
            return content, None


class ClaudeProvider(AIProvider):
    """Provider pour Anthropic Claude"""

    def __init__(self, api_key: str, model: str, timeout_seconds: int = 60, max_retries: int = 3):
        super().__init__(api_key, model, timeout_seconds, max_retries)
        try:
            from anthropic import Anthropic

            self._client = Anthropic(api_key=api_key)
        except ImportError:
            raise ImportError(
                "Le package 'anthropic' est requis pour utiliser Claude. "
                "Installez-le avec: pip install anthropic"
            )

    async def generate_content(self, prompt: str) -> str:
        """Génère du contenu avec Claude"""
        response = await asyncio.to_thread(
            self._client.messages.create,
            model=self.model,
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}],
            timeout=self.timeout_seconds,
        )
        return response.content[0].text

    async def generate_content_with_reasoning(self, prompt: str) -> Tuple[str, Optional[str]]:
        """Génère du contenu avec reasoning pour Claude (si disponible)"""
        # Claude peut avoir un reasoning dans certains modèles, mais l'API actuelle ne l'expose pas directement
        # On retourne None pour le reasoning pour l'instant
        content = await self.generate_content(prompt)
        return content, None


def create_ai_provider(
    provider: str, api_key: str, model: str, timeout_seconds: int = 60, max_retries: int = 3
) -> AIProvider:
    """
    Crée une instance de provider AI selon le type spécifié

    Args:
        provider: Type de provider ("gemini", "openai", "claude")
        api_key: Clé API du provider
        model: Nom du modèle à utiliser
        timeout_seconds: Timeout en secondes
        max_retries: Nombre maximum de tentatives

    Returns:
        Instance de AIProvider

    Raises:
        ValueError: Si le provider n'est pas reconnu
        ImportError: Si les dépendances nécessaires ne sont pas installées
    """
    provider = provider.lower()

    if provider == "gemini":
        return GeminiProvider(api_key, model, timeout_seconds, max_retries)
    elif provider == "openai":
        return OpenAIProvider(api_key, model, timeout_seconds, max_retries)
    elif provider == "claude":
        return ClaudeProvider(api_key, model, timeout_seconds, max_retries)
    else:
        raise ValueError(
            f"Provider '{provider}' non reconnu. "
            "Valeurs supportées: 'gemini', 'openai', 'claude'"
        )
