package dev.lions.unionflow.client.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
 * Service de cache pour optimiser les performances de l'application.
 * 
 * <p>Ce service fournit un cache en mémoire pour les données fréquemment accédées,
 * avec support de l'expiration automatique et de l'invalidation manuelle.
 * 
 * <p><strong>Production-ready:</strong> Cache thread-safe, gestion de l'expiration,
 * invalidation sélective, et métriques de performance.
 * 
 * <p><strong>Usage:</strong>
 * <pre>{@code
 * @Inject
 * CacheService cacheService;
 * 
 * // Obtenir une valeur du cache ou la charger si absente
 * List<TypeOrganisationDTO> types = cacheService.getOrLoad(
 *     "types-organisation",
 *     () -> typeOrganisationService.list(true),
 *     300 // Expire après 5 minutes
 * );
 * 
 * // Invalider le cache
 * cacheService.invalidate("types-organisation");
 * }</pre>
 * 
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-12-24
 */
@ApplicationScoped
public class CacheService {

    private static final Logger LOG = Logger.getLogger(CacheService.class);
    
    @Inject
    MetricsService metricsService;
    
    /**
     * Cache thread-safe pour stocker les données.
     */
    private final Map<String, CacheEntry<?>> cache = new ConcurrentHashMap<>();
    
    /**
     * Nombre maximum d'entrées dans le cache (pour éviter les fuites mémoire).
     */
    private static final int MAX_CACHE_SIZE = 1000;
    
    /**
     * Obtient une valeur du cache ou la charge si absente.
     * 
     * @param key Clé du cache
     * @param loader Fonction pour charger la valeur si absente
     * @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
     * @return La valeur du cache ou chargée
     */
    @SuppressWarnings("unchecked")
    public <T> T getOrLoad(String key, Supplier<T> loader, int ttlSeconds) {
        if (key == null || key.trim().isEmpty()) {
            LOG.warn("Tentative d'accès au cache avec une clé null ou vide");
            return loader.get();
        }
        
        // Vérifier si la valeur est en cache et valide
        CacheEntry<?> entry = cache.get(key);
        if (entry != null && entry.isValid(ttlSeconds)) {
            LOG.debugf("Cache hit pour la clé: %s", key);
            if (metricsService != null) {
                metricsService.recordCacheHit();
            }
            return (T) entry.getValue();
        }
        
        // Charger la valeur
        LOG.debugf("Cache miss pour la clé: %s - chargement depuis le backend", key);
        if (metricsService != null) {
            metricsService.recordCacheMiss();
        }
        try {
            T value = loader.get();
            
            // Vérifier la taille du cache avant d'ajouter
            if (cache.size() >= MAX_CACHE_SIZE) {
                LOG.warnf("Cache plein (%d entrées) - nettoyage des entrées expirées", cache.size());
                cleanupExpiredEntries();
                
                // Si toujours plein, supprimer les entrées les plus anciennes
                if (cache.size() >= MAX_CACHE_SIZE) {
                    evictOldestEntries(MAX_CACHE_SIZE / 10); // Supprimer 10% des entrées
                }
            }
            
            // Mettre en cache
            cache.put(key, new CacheEntry<>(value, LocalDateTime.now()));
            
            return value;
            
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du chargement de la valeur pour la clé: %s", key);
            throw new RuntimeException("Erreur lors du chargement de la valeur du cache", e);
        }
    }
    
    /**
     * Obtient une valeur du cache sans la charger si absente.
     * 
     * @param key Clé du cache
     * @return La valeur ou null si absente ou expirée
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        if (key == null || key.trim().isEmpty()) {
            return null;
        }
        
        CacheEntry<?> entry = cache.get(key);
        if (entry != null && entry.isValid(0)) { // 0 = pas d'expiration
            return (T) entry.getValue();
        }
        
        return null;
    }
    
    /**
     * Met une valeur dans le cache.
     * 
     * @param key Clé du cache
     * @param value Valeur à mettre en cache
     * @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
     */
    public <T> void put(String key, T value, int ttlSeconds) {
        if (key == null || key.trim().isEmpty()) {
            LOG.warn("Tentative de mise en cache avec une clé null ou vide");
            return;
        }
        
        if (cache.size() >= MAX_CACHE_SIZE) {
            cleanupExpiredEntries();
        }
        
        cache.put(key, new CacheEntry<>(value, LocalDateTime.now()));
        LOG.debugf("Valeur mise en cache pour la clé: %s (TTL: %d secondes)", key, ttlSeconds);
    }
    
    /**
     * Invalide une entrée du cache.
     * 
     * @param key Clé à invalider
     */
    public void invalidate(String key) {
        if (key != null && cache.remove(key) != null) {
            LOG.debugf("Cache invalidé pour la clé: %s", key);
        }
    }
    
    /**
     * Invalide toutes les entrées du cache correspondant à un préfixe.
     * 
     * @param prefix Préfixe des clés à invalider
     */
    public void invalidateByPrefix(String prefix) {
        if (prefix == null || prefix.trim().isEmpty()) {
            return;
        }
        
        int count = 0;
        for (String key : cache.keySet()) {
            if (key.startsWith(prefix)) {
                cache.remove(key);
                count++;
            }
        }
        
        if (count > 0) {
            LOG.debugf("Cache invalidé pour %d entrées avec le préfixe: %s", count, prefix);
        }
    }
    
    /**
     * Vide complètement le cache.
     */
    public void clear() {
        int size = cache.size();
        cache.clear();
        LOG.infof("Cache vidé (%d entrées supprimées)", size);
    }
    
    /**
     * Nettoie les entrées expirées du cache.
     */
    public void cleanupExpiredEntries() {
        int initialSize = cache.size();
        cache.entrySet().removeIf(entry -> !entry.getValue().isValid(0));
        int removed = initialSize - cache.size();
        
        if (removed > 0) {
            LOG.debugf("Nettoyage du cache: %d entrées expirées supprimées", removed);
        }
    }
    
    /**
     * Supprime les entrées les plus anciennes du cache.
     */
    private void evictOldestEntries(int count) {
        cache.entrySet().stream()
            .sorted((e1, e2) -> e1.getValue().getTimestamp().compareTo(e2.getValue().getTimestamp()))
            .limit(count)
            .forEach(entry -> cache.remove(entry.getKey()));
        
        LOG.debugf("Éviction de %d entrées les plus anciennes du cache", count);
    }
    
    /**
     * Obtient la taille actuelle du cache.
     */
    public int size() {
        return cache.size();
    }
    
    /**
     * Entrée du cache avec timestamp.
     */
    private static class CacheEntry<T> {
        private final T value;
        private final LocalDateTime timestamp;
        
        public CacheEntry(T value, LocalDateTime timestamp) {
            this.value = value;
            this.timestamp = timestamp;
        }
        
        public T getValue() {
            return value;
        }
        
        public LocalDateTime getTimestamp() {
            return timestamp;
        }
        
        /**
         * Vérifie si l'entrée est valide (non expirée).
         * 
         * @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
         */
        public boolean isValid(int ttlSeconds) {
            if (ttlSeconds <= 0) {
                return true; // Pas d'expiration
            }
            
            LocalDateTime expiration = timestamp.plusSeconds(ttlSeconds);
            return LocalDateTime.now().isBefore(expiration);
        }
    }
}

