// Package retry provides a retry mechanism with exponential backoff.
package retry

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

	"AutoSSO/internal/pkg/httpclient"
)

// Config defines the retry configuration.
type Config struct {
	// MaxRetries is the maximum number of retry attempts.
	// A value of 0 means no retries (only the initial attempt).
	MaxRetries int

	// InitialBackoff is the initial backoff duration before the first retry.
	InitialBackoff time.Duration

	// MaxBackoff is the maximum backoff duration between retries.
	MaxBackoff time.Duration

	// Multiplier is the factor by which the backoff increases after each retry.
	Multiplier float64

	// Jitter enables randomization of backoff duration to prevent thundering herd.
	// When enabled, the actual backoff will be between 0.5x and 1.5x of the calculated backoff.
	Jitter bool
}

// DefaultConfig provides sensible default retry configuration.
var DefaultConfig = Config{
	MaxRetries:     3,
	InitialBackoff: 100 * time.Millisecond,
	MaxBackoff:     10 * time.Second,
	Multiplier:     2.0,
	Jitter:         true,
}

// RetryableError wraps an error to indicate it is retryable.
type RetryableError struct {
	Err        error
	RetryAfter time.Duration
}

// Error returns the error message.
func (e *RetryableError) Error() string {
	if e.RetryAfter > 0 {
		return fmt.Sprintf("retryable error (retry after %v): %v", e.RetryAfter, e.Err)
	}
	return fmt.Sprintf("retryable error: %v", e.Err)
}

// Unwrap returns the underlying error.
func (e *RetryableError) Unwrap() error {
	return e.Err
}

// NewRetryableError creates a new RetryableError.
func NewRetryableError(err error) *RetryableError {
	return &RetryableError{Err: err}
}

// NewRetryableErrorWithDelay creates a new RetryableError with a suggested retry delay.
func NewRetryableErrorWithDelay(err error, retryAfter time.Duration) *RetryableError {
	return &RetryableError{Err: err, RetryAfter: retryAfter}
}

// MaxRetriesExceededError indicates that the maximum number of retries was exceeded.
type MaxRetriesExceededError struct {
	Attempts int
	LastErr  error
}

// Error returns the error message.
func (e *MaxRetriesExceededError) Error() string {
	return fmt.Sprintf("max retries exceeded after %d attempts: %v", e.Attempts, e.LastErr)
}

// Unwrap returns the underlying error.
func (e *MaxRetriesExceededError) Unwrap() error {
	return e.LastErr
}

// IsRetryable checks if an error is retryable.
// It checks for:
// 1. RetryableError type from this package
// 2. httpclient.IsRetryable for HTTP-related errors
func IsRetryable(err error) bool {
	if err == nil {
		return false
	}

	// Check for our RetryableError type
	var retryableErr *RetryableError
	if errors.As(err, &retryableErr) {
		return true
	}

	// Check using httpclient's IsRetryable function
	return httpclient.IsRetryable(err)
}

// IsMaxRetriesExceeded checks if the error indicates max retries were exceeded.
func IsMaxRetriesExceeded(err error) bool {
	var maxRetriesErr *MaxRetriesExceededError
	return errors.As(err, &maxRetriesErr)
}

// Do executes the given function with retry logic.
// It will retry the function if it returns a retryable error, up to MaxRetries times.
// The backoff between retries follows an exponential pattern with optional jitter.
// The function respects context cancellation and will return immediately if the context is canceled.
func Do(ctx context.Context, cfg Config, fn func() error) error {
	var lastErr error
	backoff := cfg.InitialBackoff

	// Total attempts = initial attempt + retries
	totalAttempts := cfg.MaxRetries + 1

	for attempt := 0; attempt < totalAttempts; attempt++ {
		// Check context before each attempt
		if err := ctx.Err(); err != nil {
			if lastErr != nil {
				return fmt.Errorf("context error after %d attempts: %w (last error: %v)", attempt, err, lastErr)
			}
			return err
		}

		// Execute the function
		err := fn()
		if err == nil {
			return nil
		}

		lastErr = err

		// Check if the error is retryable
		if !IsRetryable(err) {
			return err
		}

		// Don't wait after the last attempt
		if attempt >= cfg.MaxRetries {
			break
		}

		// Calculate the actual backoff with optional jitter
		actualBackoff := calculateBackoff(backoff, cfg.Jitter)

		// Check if the error suggests a specific retry delay
		var retryableErr *RetryableError
		if errors.As(err, &retryableErr) && retryableErr.RetryAfter > 0 {
			actualBackoff = retryableErr.RetryAfter
		}

		// Wait for the backoff duration or context cancellation
		select {
		case <-ctx.Done():
			return fmt.Errorf("context canceled during backoff after %d attempts: %w (last error: %v)", attempt+1, ctx.Err(), lastErr)
		case <-time.After(actualBackoff):
		}

		// Calculate next backoff with exponential increase
		backoff = time.Duration(math.Min(
			float64(backoff)*cfg.Multiplier,
			float64(cfg.MaxBackoff),
		))
	}

	return &MaxRetriesExceededError{
		Attempts: totalAttempts,
		LastErr:  lastErr,
	}
}

// DoWithResult executes a function that returns a result and an error with retry logic.
// This is useful when the function returns a value that needs to be captured.
func DoWithResult[T any](ctx context.Context, cfg Config, fn func() (T, error)) (T, error) {
	var result T
	var lastErr error
	backoff := cfg.InitialBackoff

	totalAttempts := cfg.MaxRetries + 1

	for attempt := 0; attempt < totalAttempts; attempt++ {
		if err := ctx.Err(); err != nil {
			if lastErr != nil {
				return result, fmt.Errorf("context error after %d attempts: %w (last error: %v)", attempt, err, lastErr)
			}
			return result, err
		}

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

		lastErr = err

		if !IsRetryable(err) {
			return result, err
		}

		if attempt >= cfg.MaxRetries {
			break
		}

		actualBackoff := calculateBackoff(backoff, cfg.Jitter)

		var retryableErr *RetryableError
		if errors.As(err, &retryableErr) && retryableErr.RetryAfter > 0 {
			actualBackoff = retryableErr.RetryAfter
		}

		select {
		case <-ctx.Done():
			return result, fmt.Errorf("context canceled during backoff after %d attempts: %w (last error: %v)", attempt+1, ctx.Err(), lastErr)
		case <-time.After(actualBackoff):
		}

		backoff = time.Duration(math.Min(
			float64(backoff)*cfg.Multiplier,
			float64(cfg.MaxBackoff),
		))
	}

	return result, &MaxRetriesExceededError{
		Attempts: totalAttempts,
		LastErr:  lastErr,
	}
}

// calculateBackoff calculates the actual backoff duration with optional jitter.
// When jitter is enabled, the backoff is randomized between 0.5x and 1.5x of the base backoff.
func calculateBackoff(base time.Duration, jitter bool) time.Duration {
	if !jitter {
		return base
	}

	// Add jitter: multiply by a random factor between 0.5 and 1.5
	// This helps prevent thundering herd problem
	jitterFactor := 0.5 + rand.Float64() // Range: [0.5, 1.5)
	return time.Duration(float64(base) * jitterFactor)
}
