package recovery

import (
	"context"
	"errors"
	"fmt"
	"math"
	"math/rand"
	"time"

	"go.uber.org/zap"
)

// RetryConfig configure le comportement du retry (BE-SVC-021)
type RetryConfig struct {
	MaxAttempts     int                          // Nombre maximum de tentatives
	InitialDelay    time.Duration                // Délai initial avant le premier retry
	MaxDelay        time.Duration                // Délai maximum entre les tentatives
	Multiplier      float64                      // Multiplicateur pour le backoff exponentiel
	Jitter          bool                         // Ajouter du jitter pour éviter le thundering herd
	RetryableErrors []error                      // Liste des erreurs qui doivent être retryées
	RetryableFunc   func(error) bool             // Fonction pour déterminer si une erreur est retryable
	OnRetry         func(attempt int, err error) // Callback appelé à chaque retry
}

// DefaultRetryConfig retourne une configuration de retry par défaut
func DefaultRetryConfig() *RetryConfig {
	return &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 100 * time.Millisecond,
		MaxDelay:     5 * time.Second,
		Multiplier:   2.0,
		Jitter:       true,
	}
}

// RetryStrategy définit la stratégie de retry
type RetryStrategy int

const (
	// ExponentialBackoff utilise un backoff exponentiel
	ExponentialBackoff RetryStrategy = iota
	// LinearBackoff utilise un backoff linéaire
	LinearBackoff
	// FixedBackoff utilise un délai fixe
	FixedBackoff
)

// Retry exécute une fonction avec retry automatique (BE-SVC-021)
func Retry(ctx context.Context, fn func() error, config *RetryConfig) error {
	if config == nil {
		config = DefaultRetryConfig()
	}

	var lastErr error
	for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
		// Vérifier si le contexte est annulé avant d'exécuter
		select {
		case <-ctx.Done():
			return fmt.Errorf("context cancelled: %w", ctx.Err())
		default:
		}

		// Exécuter la fonction
		err := fn()
		if err == nil {
			return nil // Succès
		}

		lastErr = err

		// Vérifier si le contexte est annulé après l'exécution
		select {
		case <-ctx.Done():
			return fmt.Errorf("context cancelled: %w", ctx.Err())
		default:
		}

		// Vérifier si l'erreur est retryable
		if !isRetryable(err, config) {
			return err // Erreur non retryable, arrêter
		}

		// Si c'est la dernière tentative, ne pas attendre
		if attempt >= config.MaxAttempts {
			break
		}

		// Calculer le délai avant le prochain retry
		delay := calculateDelay(attempt, config)

		// Appeler le callback OnRetry si défini
		if config.OnRetry != nil {
			config.OnRetry(attempt, err)
		}

		// Attendre avant le prochain retry
		select {
		case <-ctx.Done():
			return fmt.Errorf("context cancelled during retry: %w", ctx.Err())
		case <-time.After(delay):
			// Continuer avec le prochain retry
		}
	}

	return fmt.Errorf("max attempts (%d) reached: %w", config.MaxAttempts, lastErr)
}

// RetryWithResult exécute une fonction qui retourne un résultat avec retry
func RetryWithResult[T any](ctx context.Context, fn func() (T, error), config *RetryConfig) (T, error) {
	var zero T
	if config == nil {
		config = DefaultRetryConfig()
	}

	var lastErr error
	for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
		select {
		case <-ctx.Done():
			return zero, fmt.Errorf("context cancelled: %w", ctx.Err())
		default:
		}

		result, err := fn()
		if err == nil {
			return result, nil
		}

		lastErr = err

		if !isRetryable(err, config) {
			return zero, err
		}

		if attempt >= config.MaxAttempts {
			break
		}

		delay := calculateDelay(attempt, config)

		if config.OnRetry != nil {
			config.OnRetry(attempt, err)
		}

		select {
		case <-ctx.Done():
			return zero, fmt.Errorf("context cancelled during retry: %w", ctx.Err())
		case <-time.After(delay):
		}
	}

	return zero, fmt.Errorf("max attempts (%d) reached: %w", config.MaxAttempts, lastErr)
}

// isRetryable détermine si une erreur doit être retryée
func isRetryable(err error, config *RetryConfig) bool {
	if err == nil {
		return false
	}

	// Si une fonction de vérification est fournie, l'utiliser
	if config.RetryableFunc != nil {
		return config.RetryableFunc(err)
	}

	// Vérifier dans la liste des erreurs retryables
	if len(config.RetryableErrors) > 0 {
		for _, retryableErr := range config.RetryableErrors {
			if errors.Is(err, retryableErr) {
				return true
			}
		}
	}

	// Par défaut, retryer toutes les erreurs
	return true
}

// calculateDelay calcule le délai avant le prochain retry
func calculateDelay(attempt int, config *RetryConfig) time.Duration {
	var delay time.Duration

	// Calculer le délai de base selon la stratégie
	baseDelay := config.InitialDelay
	if config.Multiplier > 0 {
		// Backoff exponentiel
		baseDelay = time.Duration(float64(config.InitialDelay) * math.Pow(config.Multiplier, float64(attempt-1)))
	} else {
		// Backoff linéaire
		baseDelay = config.InitialDelay * time.Duration(attempt)
	}

	delay = baseDelay

	// Appliquer le jitter si activé
	if config.Jitter {
		// Jitter aléatoire entre 0 et 25% du délai
		jitter := time.Duration(rand.Float64() * 0.25 * float64(delay))
		delay = delay + jitter
	}

	// Limiter au délai maximum
	if delay > config.MaxDelay {
		delay = config.MaxDelay
	}

	return delay
}

// RetryWithLogger exécute une fonction avec retry et logging
func RetryWithLogger(ctx context.Context, logger *zap.Logger, fn func() error, config *RetryConfig) error {
	if config == nil {
		config = DefaultRetryConfig()
	}

	// Configurer le callback OnRetry pour logger
	originalOnRetry := config.OnRetry
	config.OnRetry = func(attempt int, err error) {
		if logger != nil {
			logger.Warn("Retry attempt",
				zap.Int("attempt", attempt),
				zap.Int("max_attempts", config.MaxAttempts),
				zap.Error(err),
			)
		}
		if originalOnRetry != nil {
			originalOnRetry(attempt, err)
		}
	}

	return Retry(ctx, fn, config)
}

// IsRetryableError vérifie si une erreur est généralement retryable
// Cette fonction peut être utilisée comme RetryableFunc dans RetryConfig
func IsRetryableError(err error) bool {
	if err == nil {
		return false
	}

	errStr := err.Error()

	// Erreurs réseau généralement retryables
	retryablePatterns := []string{
		"timeout",
		"connection refused",
		"connection reset",
		"no such host",
		"network is unreachable",
		"temporary failure",
		"server error",
		"service unavailable",
		"bad gateway",
		"gateway timeout",
	}

	for _, pattern := range retryablePatterns {
		if contains(errStr, pattern) {
			return true
		}
	}

	return false
}

// contains vérifie si une chaîne contient une sous-chaîne (case-insensitive)
func contains(s, substr string) bool {
	if len(substr) > len(s) {
		return false
	}
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
