"""
Système de cache pour le pipeline RAG.

Ce module implémente un cache pour les embeddings et les résultats de recherche
afin d'améliorer les performances du pipeline RAG.

Exigences: 14.1, 14.2
"""

import logging
import hashlib
import pickle
from pathlib import Path
from typing import Any, Optional, List
from datetime import datetime, timedelta
import numpy as np

logger = logging.getLogger(__name__)


class RAGCache:
    """
    Cache pour les embeddings et résultats de recherche RAG.
    
    Le cache utilise un système de fichiers pour persister les données
    entre les exécutions. Les entrées ont une durée de vie configurable.
    
    Exigences: 14.1, 14.2
    """
    
    def __init__(
        self,
        cache_dir: str = "data/cache",
        ttl_hours: int = 24,
        max_cache_size_mb: int = 500
    ):
        """
        Initialise le cache RAG.
        
        Args:
            cache_dir: Répertoire pour stocker le cache
            ttl_hours: Durée de vie des entrées en heures
            max_cache_size_mb: Taille maximale du cache en MB
        """
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        
        self.embeddings_dir = self.cache_dir / "embeddings"
        self.embeddings_dir.mkdir(exist_ok=True)
        
        self.search_results_dir = self.cache_dir / "search_results"
        self.search_results_dir.mkdir(exist_ok=True)
        
        self.ttl = timedelta(hours=ttl_hours)
        self.max_cache_size = max_cache_size_mb * 1024 * 1024  # Convertir en bytes
        
        logger.info(f"RAGCache initialisé: cache_dir={cache_dir}, ttl={ttl_hours}h, max_size={max_cache_size_mb}MB")
    
    def _get_cache_key(self, data: Any) -> str:
        """
        Génère une clé de cache unique pour les données.
        
        Args:
            data: Données à hasher
            
        Returns:
            Clé de cache (hash SHA-256)
        """
        # Convertir les données en string pour le hashing
        if isinstance(data, str):
            data_str = data
        elif isinstance(data, (list, tuple)):
            data_str = str(sorted(data))
        elif isinstance(data, dict):
            data_str = str(sorted(data.items()))
        else:
            data_str = str(data)
        
        # Générer le hash
        return hashlib.sha256(data_str.encode()).hexdigest()
    
    def _is_expired(self, file_path: Path) -> bool:
        """
        Vérifie si une entrée de cache est expirée.
        
        Args:
            file_path: Chemin du fichier de cache
            
        Returns:
            True si expiré, False sinon
        """
        if not file_path.exists():
            return True
        
        # Vérifier la date de modification
        mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
        age = datetime.now() - mtime
        
        return age > self.ttl
    
    def get_embedding(self, text: str) -> Optional[np.ndarray]:
        """
        Récupère un embedding depuis le cache.
        
        Args:
            text: Texte dont on veut l'embedding
            
        Returns:
            Embedding (numpy array) ou None si non trouvé/expiré
        """
        cache_key = self._get_cache_key(text)
        cache_file = self.embeddings_dir / f"{cache_key}.pkl"
        
        if self._is_expired(cache_file):
            return None
        
        try:
            with open(cache_file, "rb") as f:
                embedding = pickle.load(f)
            logger.debug(f"Embedding trouvé dans le cache: {cache_key[:8]}...")
            return embedding
        except Exception as e:
            logger.warning(f"Erreur lors de la lecture du cache d'embedding: {e}")
            return None
    
    def set_embedding(self, text: str, embedding: np.ndarray) -> None:
        """
        Stocke un embedding dans le cache.
        
        Args:
            text: Texte source
            embedding: Embedding à stocker
        """
        cache_key = self._get_cache_key(text)
        cache_file = self.embeddings_dir / f"{cache_key}.pkl"
        
        try:
            with open(cache_file, "wb") as f:
                pickle.dump(embedding, f)
            logger.debug(f"Embedding mis en cache: {cache_key[:8]}...")
        except Exception as e:
            logger.warning(f"Erreur lors de l'écriture du cache d'embedding: {e}")
    
    def get_search_results(
        self,
        query: str,
        referentiel_type: str,
        version: str,
        top_k: int
    ) -> Optional[List[Any]]:
        """
        Récupère des résultats de recherche depuis le cache.
        
        Args:
            query: Requête de recherche
            referentiel_type: Type de référentiel
            version: Version du référentiel
            top_k: Nombre de résultats
            
        Returns:
            Résultats de recherche ou None si non trouvé/expiré
        """
        cache_data = {
            "query": query,
            "referentiel_type": referentiel_type,
            "version": version,
            "top_k": top_k
        }
        cache_key = self._get_cache_key(cache_data)
        cache_file = self.search_results_dir / f"{cache_key}.pkl"
        
        if self._is_expired(cache_file):
            return None
        
        try:
            with open(cache_file, "rb") as f:
                results = pickle.load(f)
            logger.debug(f"Résultats de recherche trouvés dans le cache: {cache_key[:8]}...")
            return results
        except Exception as e:
            logger.warning(f"Erreur lors de la lecture du cache de recherche: {e}")
            return None
    
    def set_search_results(
        self,
        query: str,
        referentiel_type: str,
        version: str,
        top_k: int,
        results: List[Any]
    ) -> None:
        """
        Stocke des résultats de recherche dans le cache.
        
        Args:
            query: Requête de recherche
            referentiel_type: Type de référentiel
            version: Version du référentiel
            top_k: Nombre de résultats
            results: Résultats à stocker
        """
        cache_data = {
            "query": query,
            "referentiel_type": referentiel_type,
            "version": version,
            "top_k": top_k
        }
        cache_key = self._get_cache_key(cache_data)
        cache_file = self.search_results_dir / f"{cache_key}.pkl"
        
        try:
            with open(cache_file, "wb") as f:
                pickle.dump(results, f)
            logger.debug(f"Résultats de recherche mis en cache: {cache_key[:8]}...")
        except Exception as e:
            logger.warning(f"Erreur lors de l'écriture du cache de recherche: {e}")
    
    def clear_expired(self) -> int:
        """
        Supprime les entrées de cache expirées.
        
        Returns:
            Nombre d'entrées supprimées
        """
        logger.info("Nettoyage des entrées de cache expirées...")
        
        deleted_count = 0
        
        # Nettoyer les embeddings
        for cache_file in self.embeddings_dir.glob("*.pkl"):
            if self._is_expired(cache_file):
                cache_file.unlink()
                deleted_count += 1
        
        # Nettoyer les résultats de recherche
        for cache_file in self.search_results_dir.glob("*.pkl"):
            if self._is_expired(cache_file):
                cache_file.unlink()
                deleted_count += 1
        
        logger.info(f"{deleted_count} entrées de cache expirées supprimées")
        return deleted_count
    
    def get_cache_size(self) -> int:
        """
        Calcule la taille totale du cache en bytes.
        
        Returns:
            Taille du cache en bytes
        """
        total_size = 0
        
        for cache_file in self.embeddings_dir.glob("*.pkl"):
            total_size += cache_file.stat().st_size
        
        for cache_file in self.search_results_dir.glob("*.pkl"):
            total_size += cache_file.stat().st_size
        
        return total_size
    
    def enforce_size_limit(self) -> int:
        """
        Applique la limite de taille du cache en supprimant les entrées les plus anciennes.
        
        Returns:
            Nombre d'entrées supprimées
        """
        current_size = self.get_cache_size()
        
        if current_size <= self.max_cache_size:
            return 0
        
        logger.warning(f"Cache trop volumineux ({current_size / 1024 / 1024:.2f} MB), nettoyage...")
        
        # Collecter tous les fichiers avec leur date de modification
        all_files = []
        
        for cache_file in self.embeddings_dir.glob("*.pkl"):
            all_files.append((cache_file, cache_file.stat().st_mtime))
        
        for cache_file in self.search_results_dir.glob("*.pkl"):
            all_files.append((cache_file, cache_file.stat().st_mtime))
        
        # Trier par date (plus ancien en premier)
        all_files.sort(key=lambda x: x[1])
        
        # Supprimer les fichiers jusqu'à atteindre la limite
        deleted_count = 0
        for cache_file, _ in all_files:
            if current_size <= self.max_cache_size:
                break
            
            file_size = cache_file.stat().st_size
            cache_file.unlink()
            current_size -= file_size
            deleted_count += 1
        
        logger.info(f"{deleted_count} entrées supprimées pour respecter la limite de taille")
        return deleted_count
    
    def clear_all(self) -> None:
        """
        Supprime toutes les entrées du cache.
        """
        logger.warning("Suppression de toutes les entrées du cache...")
        
        for cache_file in self.embeddings_dir.glob("*.pkl"):
            cache_file.unlink()
        
        for cache_file in self.search_results_dir.glob("*.pkl"):
            cache_file.unlink()
        
        logger.info("Cache vidé")
    
    def get_statistics(self) -> dict:
        """
        Récupère des statistiques sur le cache.
        
        Returns:
            Dictionnaire avec les statistiques
        """
        embeddings_count = len(list(self.embeddings_dir.glob("*.pkl")))
        search_results_count = len(list(self.search_results_dir.glob("*.pkl")))
        total_size = self.get_cache_size()
        
        return {
            "embeddings_count": embeddings_count,
            "search_results_count": search_results_count,
            "total_entries": embeddings_count + search_results_count,
            "total_size_mb": round(total_size / 1024 / 1024, 2),
            "max_size_mb": round(self.max_cache_size / 1024 / 1024, 2),
            "usage_percent": round((total_size / self.max_cache_size) * 100, 2) if self.max_cache_size > 0 else 0
        }
