// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// DO NOT MODIFY. Generated by gotmpl.
// source: internal/shared/otlp/retry/retry.go.tmpl

// Package retry provides request retry functionality that can perform
// configurable exponential backoff for transient errors and honor any
// explicit throttle responses received.
package retry

import (
	"context"
	"fmt"
	"time"

	"github.com/cenkalti/backoff/v5"
)

// DefaultConfig is the recommended default configuration.
var DefaultConfig = Config{
	Enabled:         true,
	InitialInterval: 5 * time.Second,
	MaxInterval:     30 * time.Second,
	MaxElapsedTime:  time.Minute,
}

// Config defines configuration for retrying batches after an export failure
// using exponential backoff.
type Config struct {
	// Enabled indicates whether to retry sending batches after an export
	// failure.
	Enabled bool
	// InitialInterval is the time to wait after the first failure before
	// retrying.
	InitialInterval time.Duration
	// MaxInterval is the upper bound on the backoff interval before
	// randomization. Once this value is reached, the base interval remains at
	// MaxInterval.
	MaxInterval time.Duration
	// MaxElapsedTime is the maximum amount of time (including retries) spent
	// trying to send a request/batch. Once this value is reached, retrying stops.
	MaxElapsedTime time.Duration
}

// RequestFunc wraps a request with retry logic.
type RequestFunc func(context.Context, func(context.Context) error) error

// EvaluateFunc reports whether an error is retryable and returns any explicit
// throttle duration to honor.
//
// The function must return true as its first return value if the error
// argument is retryable; otherwise, it must return false.
//
// The function must return a nonzero time.Duration if the error contains an
// explicit throttle duration that should be honored; otherwise, it must return
// the zero value for time.Duration.
type EvaluateFunc func(error) (bool, time.Duration)

// RequestFunc returns a RequestFunc that uses evaluate to determine whether
// requests can be retried and uses c to configure exponential backoff.
func (c Config) RequestFunc(evaluate EvaluateFunc) RequestFunc {
	if !c.Enabled {
		return func(ctx context.Context, fn func(context.Context) error) error {
			return fn(ctx)
		}
	}

	return func(ctx context.Context, fn func(context.Context) error) error {
		// Do not use NewExponentialBackOff since it calls Reset and the code here
		// must call Reset after changing the InitialInterval (this saves an
		// unnecessary call to Now).
		b := &backoff.ExponentialBackOff{
			InitialInterval:     c.InitialInterval,
			RandomizationFactor: backoff.DefaultRandomizationFactor,
			Multiplier:          backoff.DefaultMultiplier,
			MaxInterval:         c.MaxInterval,
		}
		b.Reset()

		maxElapsedTime := c.MaxElapsedTime
		startTime := time.Now()

		for {
			err := fn(ctx)
			if err == nil {
				return nil
			}

			retryable, throttle := evaluate(err)
			if !retryable {
				return err
			}

			// Check if context is canceled before attempting to wait and retry.
			if ctx.Err() != nil {
				return fmt.Errorf("%w: %w", context.Cause(ctx), err)
			}

			if maxElapsedTime != 0 && time.Since(startTime) > maxElapsedTime {
				return fmt.Errorf("max retry time elapsed: %w", err)
			}

			// Wait for the greater of the backoff or throttle delay.
			bOff := b.NextBackOff()
			delay := max(throttle, bOff)

			elapsed := time.Since(startTime)
			if maxElapsedTime != 0 && elapsed+throttle > maxElapsedTime {
				return fmt.Errorf("max retry time would elapse: %w", err)
			}

			if ctxErr := waitFunc(ctx, delay); ctxErr != nil {
				return fmt.Errorf("%w: %w", ctxErr, err)
			}
		}
	}
}

// waitFunc can be overridden for testing.
var waitFunc = wait

// wait blocks until delay elapses or ctx is done. If both happen
// simultaneously, wait favors the elapsed delay and returns nil to indicate
// that the call can be retried.
func wait(ctx context.Context, delay time.Duration) error {
	timer := time.NewTimer(delay)
	defer timer.Stop()

	select {
	case <-ctx.Done():
		// Handle the case where the timer and context deadline end
		// simultaneously by prioritizing the timer expiration nil value
		// response.
		select {
		case <-timer.C:
		default:
			return context.Cause(ctx)
		}
	case <-timer.C:
	}

	return nil
}
