package recovery

import (
	"context"
	"errors"
	"fmt"
	"time"

	"go.uber.org/zap"
)

// ErrorRecoveryStrategy définit une stratégie de récupération d'erreur (BE-SVC-021)
type ErrorRecoveryStrategy interface {
	// Recover tente de récupérer d'une erreur
	Recover(ctx context.Context, err error) error
	// CanRecover détermine si cette stratégie peut récupérer de cette erreur
	CanRecover(err error) bool
}

// RetryRecoveryStrategy récupère d'une erreur en réessayant l'opération
type RetryRecoveryStrategy struct {
	config *RetryConfig
	logger *zap.Logger
	fn     func() error
}

// NewRetryRecoveryStrategy crée une nouvelle stratégie de récupération par retry
func NewRetryRecoveryStrategy(fn func() error, config *RetryConfig, logger *zap.Logger) *RetryRecoveryStrategy {
	if config == nil {
		config = DefaultRetryConfig()
	}
	return &RetryRecoveryStrategy{
		config: config,
		logger: logger,
		fn:     fn,
	}
}

// CanRecover vérifie si cette stratégie peut récupérer de l'erreur
func (r *RetryRecoveryStrategy) CanRecover(err error) bool {
	return IsRetryableError(err)
}

// Recover tente de récupérer en réessayant l'opération
func (r *RetryRecoveryStrategy) Recover(ctx context.Context, err error) error {
	if r.fn == nil {
		return fmt.Errorf("no recovery function provided")
	}
	// La fonction fn sera appelée par Retry, donc on peut l'utiliser directement
	// Retry va appeler fn() plusieurs fois jusqu'à succès ou max attempts
	return RetryWithLogger(ctx, r.logger, r.fn, r.config)
}

// FallbackRecoveryStrategy récupère d'une erreur en utilisant une fonction de fallback
type FallbackRecoveryStrategy struct {
	fallbackFn func() error
	logger     *zap.Logger
}

// NewFallbackRecoveryStrategy crée une nouvelle stratégie de récupération par fallback
func NewFallbackRecoveryStrategy(fallbackFn func() error, logger *zap.Logger) *FallbackRecoveryStrategy {
	return &FallbackRecoveryStrategy{
		fallbackFn: fallbackFn,
		logger:     logger,
	}
}

// CanRecover retourne toujours true pour le fallback
func (f *FallbackRecoveryStrategy) CanRecover(err error) bool {
	return true
}

// Recover exécute la fonction de fallback
func (f *FallbackRecoveryStrategy) Recover(ctx context.Context, err error) error {
	if f.fallbackFn == nil {
		return fmt.Errorf("no fallback function provided")
	}
	if f.logger != nil {
		f.logger.Info("Using fallback recovery strategy", zap.Error(err))
	}
	return f.fallbackFn()
}

// CircuitBreakerRecoveryStrategy récupère d'une erreur en vérifiant l'état du circuit breaker
type CircuitBreakerRecoveryStrategy struct {
	checkFn func() bool
	logger  *zap.Logger
}

// NewCircuitBreakerRecoveryStrategy crée une nouvelle stratégie basée sur le circuit breaker
func NewCircuitBreakerRecoveryStrategy(checkFn func() bool, logger *zap.Logger) *CircuitBreakerRecoveryStrategy {
	return &CircuitBreakerRecoveryStrategy{
		checkFn: checkFn,
		logger:  logger,
	}
}

// CanRecover vérifie si le circuit breaker est ouvert (on peut récupérer si ouvert)
func (c *CircuitBreakerRecoveryStrategy) CanRecover(err error) bool {
	if c.checkFn == nil {
		return false
	}
	// Si le circuit breaker est ouvert (checkFn retourne true), on peut récupérer en attendant
	// Si fermé (checkFn retourne false), pas besoin de récupération
	return c.checkFn()
}

// Recover attend que le circuit breaker se ferme
func (c *CircuitBreakerRecoveryStrategy) Recover(ctx context.Context, err error) error {
	if c.logger != nil {
		c.logger.Warn("Circuit breaker is open, waiting for recovery", zap.Error(err))
	}

	// Utiliser le contexte existant (qui peut déjà avoir un timeout)
	// Ne pas créer un nouveau timeout si le contexte en a déjà un
	if _, hasTimeout := ctx.Deadline(); !hasTimeout {
		timeout := 30 * time.Second
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, timeout)
		defer cancel()
	}

	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return fmt.Errorf("timeout waiting for circuit breaker to close: %w", ctx.Err())
		case <-ticker.C:
			if c.checkFn() {
				// Circuit breaker fermé, on peut réessayer
				return nil
			}
		}
	}
}

// CompositeRecoveryStrategy combine plusieurs stratégies de récupération
type CompositeRecoveryStrategy struct {
	strategies []ErrorRecoveryStrategy
	logger     *zap.Logger
}

// NewCompositeRecoveryStrategy crée une stratégie composite
func NewCompositeRecoveryStrategy(strategies []ErrorRecoveryStrategy, logger *zap.Logger) *CompositeRecoveryStrategy {
	return &CompositeRecoveryStrategy{
		strategies: strategies,
		logger:     logger,
	}
}

// CanRecover vérifie si au moins une stratégie peut récupérer
func (c *CompositeRecoveryStrategy) CanRecover(err error) bool {
	for _, strategy := range c.strategies {
		if strategy.CanRecover(err) {
			return true
		}
	}
	return false
}

// Recover tente de récupérer en utilisant la première stratégie applicable
func (c *CompositeRecoveryStrategy) Recover(ctx context.Context, err error) error {
	for _, strategy := range c.strategies {
		if strategy.CanRecover(err) {
			if c.logger != nil {
				c.logger.Debug("Attempting recovery with strategy", zap.String("type", fmt.Sprintf("%T", strategy)))
			}
			recoveryErr := strategy.Recover(ctx, err)
			if recoveryErr == nil {
				return nil
			}
			// Si cette stratégie échoue, essayer la suivante
			if c.logger != nil {
				c.logger.Warn("Recovery strategy failed, trying next", zap.Error(recoveryErr))
			}
		}
	}
	return fmt.Errorf("all recovery strategies failed: %w", err)
}

// RecoverWithStrategies tente de récupérer d'une erreur en utilisant plusieurs stratégies
func RecoverWithStrategies(ctx context.Context, err error, strategies []ErrorRecoveryStrategy, logger *zap.Logger) error {
	if len(strategies) == 0 {
		return fmt.Errorf("no recovery strategies provided: %w", err)
	}

	composite := NewCompositeRecoveryStrategy(strategies, logger)
	return composite.Recover(ctx, err)
}

// IsTemporaryError vérifie si une erreur est temporaire et peut être retryée
func IsTemporaryError(err error) bool {
	if err == nil {
		return false
	}

	// Vérifier les erreurs temporaires connues
	var tempErr interface {
		Temporary() bool
	}
	if errors.As(err, &tempErr) {
		return tempErr.Temporary()
	}

	// Vérifier les patterns d'erreurs temporaires
	return IsRetryableError(err)
}

// IsPermanentError vérifie si une erreur est permanente et ne doit pas être retryée
func IsPermanentError(err error) bool {
	return !IsTemporaryError(err)
}
