"""
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
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type

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
        """

    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:
                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'"
        )
