package dev.lions.unionflow.client.service;

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

import java.util.concurrent.Callable;
import java.util.function.Supplier;

/**
 * Service de retry automatique pour les appels backend en cas d'échec temporaire.
 * 
 * <p>Ce service implémente une stratégie de retry intelligente pour gérer:
 * - Erreurs temporaires (503 Service Unavailable, 504 Gateway Timeout)
 * - Timeouts réseau
 * - Erreurs de connexion
 * 
 * <p><strong>Production-ready:</strong> Gestion complète des retries avec backoff exponentiel,
 * limite de tentatives, et logging approprié.
 * 
 * <p><strong>Usage:</strong>
 * <pre>{@code
 * @Inject
 * RetryService retryService;
 * 
 * OrganisationResponse org = retryService.executeWithRetry(
 *     () -> organisationService.creer(nouvelleOrganisation),
 *     "création d'une organisation"
 * );
 * }</pre>
 * 
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-12-24
 */
@ApplicationScoped
public class RetryService {
    
    private static final Logger LOG = Logger.getLogger(RetryService.class);
    
    @Inject
    MetricsService metricsService;
    
    /**
     * Nombre maximum de tentatives (incluant la première).
     */
    private static final int MAX_ATTEMPTS = 3;
    
    /**
     * Délai initial entre les tentatives en millisecondes.
     */
    private static final long INITIAL_DELAY_MS = 1000;
    
    /**
     * Multiplicateur pour le backoff exponentiel.
     */
    private static final double BACKOFF_MULTIPLIER = 2.0;
    
    /**
     * Exécute une opération avec retry automatique en cas d'échec temporaire.
     * 
     * @param operation L'opération à exécuter (Supplier)
     * @param contextMessage Message contextuel pour le logging (ex: "création d'une organisation")
     * @return Le résultat de l'opération
     * @throws Exception Si toutes les tentatives échouent ou si l'erreur n'est pas retryable
     */
    public <T> T executeWithRetrySupplier(Supplier<T> operation, String contextMessage) throws Exception {
        return executeWithRetry(() -> operation.get(), contextMessage);
    }
    
    /**
     * Exécute une opération avec retry automatique en cas d'échec temporaire.
     * 
     * @param operation L'opération à exécuter (Callable)
     * @param contextMessage Message contextuel pour le logging
     * @return Le résultat de l'opération
     * @throws Exception Si toutes les tentatives échouent ou si l'erreur n'est pas retryable
     */
    public <T> T executeWithRetry(Callable<T> operation, String contextMessage) throws Exception {
        Exception lastException = null;
        
        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            try {
                LOG.debugf("Tentative %d/%d pour: %s", attempt, MAX_ATTEMPTS, contextMessage);
                
                T result = operation.call();
                
                if (attempt > 1) {
                    LOG.infof("Succès après %d tentative(s) pour: %s", attempt, contextMessage);
                }
                
                return result;
                
            } catch (Exception e) {
                lastException = e;
                
                // Vérifier si l'erreur est retryable
                if (!isRetryable(e)) {
                    LOG.debugf("Erreur non retryable pour: %s - %s", contextMessage, e.getClass().getSimpleName());
                    throw e; // Ne pas retryer
                }
                
                // Si c'est la dernière tentative, re-lancer l'exception
                if (attempt == MAX_ATTEMPTS) {
                    LOG.errorf(e, "Échec après %d tentative(s) pour: %s", MAX_ATTEMPTS, contextMessage);
                    throw e;
                }
                
                // Calculer le délai avant la prochaine tentative (backoff exponentiel)
                long delay = calculateDelay(attempt);
                LOG.warnf("Tentative %d/%d échouée pour: %s - Retry dans %d ms - Erreur: %s", 
                    attempt, MAX_ATTEMPTS, contextMessage, delay, e.getMessage());
                
                // Attendre avant la prochaine tentative
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrompu", ie);
                }
            }
        }
        
        // Ne devrait jamais arriver ici, mais au cas où
        throw lastException != null ? lastException : new RuntimeException("Échec inattendu");
    }
    
    /**
     * Vérifie si une exception est retryable (erreur temporaire).
     */
    private boolean isRetryable(Exception e) {
        // Erreurs de connexion (retryable)
        if (e instanceof java.net.ConnectException ||
            e instanceof java.net.SocketTimeoutException ||
            e instanceof java.util.concurrent.TimeoutException ||
            e.getCause() instanceof java.net.ConnectException ||
            e.getCause() instanceof java.net.SocketTimeoutException ||
            e.getCause() instanceof java.util.concurrent.TimeoutException) {
            return true;
        }
        
        // Erreurs REST Client spécifiques
        if (e instanceof RestClientExceptionMapper.ServiceUnavailableException ||
            e instanceof RestClientExceptionMapper.GatewayTimeoutException ||
            e instanceof RestClientExceptionMapper.BadGatewayException) {
            return true;
        }
        
        // Erreurs ProcessingException (connexion, timeout, etc.)
        if (e instanceof jakarta.ws.rs.ProcessingException) {
            String message = e.getMessage();
            if (message != null && (
                message.contains("Connection") ||
                message.contains("timeout") ||
                message.contains("refused") ||
                message.contains("unreachable"))) {
                return true;
            }
        }
        
        // Erreurs 5xx (sauf 500 qui peut être une erreur permanente)
        if (e instanceof RestClientExceptionMapper.InternalServerErrorException) {
            // 500 peut être retryable si c'est une erreur temporaire du serveur
            return true;
        }
        
        // Par défaut, ne pas retryer (erreurs 4xx, validation, etc.)
        return false;
    }
    
    /**
     * Calcule le délai avant la prochaine tentative (backoff exponentiel).
     */
    private long calculateDelay(int attempt) {
        // Backoff exponentiel: INITIAL_DELAY * (BACKOFF_MULTIPLIER ^ (attempt - 1))
        double delay = INITIAL_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempt - 1);
        
        // Limiter à 10 secondes maximum
        return Math.min((long) delay, 10000);
    }
    
    /**
     * Exécute une opération avec retry, mais sans propagation d'exception (retourne null en cas d'échec).
     * 
     * <p>Utile pour les opérations non critiques où on peut continuer même en cas d'échec.
     */
    public <T> T executeWithRetryOrNull(Callable<T> operation, String contextMessage) {
        try {
            return executeWithRetry(operation, contextMessage);
        } catch (Exception e) {
            LOG.warnf(e, "Échec définitif pour: %s - Retour de null", contextMessage);
            return null;
        }
    }
}

