// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.

package utils

import (
	"context"
	"errors"
	"fmt"
	"github.com/LukeHagar/plexgo/retry"
	"io"
	"math"
	"math/rand"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"time"
)

// Deprecated: Use retry.BackoffStrategy instead.
type BackoffStrategy = retry.BackoffStrategy

// Deprecated: Use retry.Config instead.
type RetryConfig = retry.Config

type Retries struct {
	Config      *retry.Config
	StatusCodes []string
}

var (
	// IETF RFC 7231 4.2 safe and idempotent HTTP methods for connection retries
	idempotentHTTPMethods = []string{
		http.MethodDelete,
		http.MethodGet,
		http.MethodHead,
		http.MethodOptions,
		http.MethodPut,
	}
)

func Retry(ctx context.Context, r Retries, operation func() (*http.Response, error)) (*http.Response, error) {
	switch r.Config.Strategy {
	case "backoff":
		if r.Config.Backoff == nil {
			return operation()
		}

		var resp *http.Response

		err := retryWithBackoff(ctx, r.Config.Backoff, func() error {
			if resp != nil {
				resp.Body.Close()
			}

			select {
			case <-ctx.Done():
				return retry.Permanent(ctx.Err())
			default:
			}

			res, err := operation()
			if err != nil {
				if !r.Config.RetryConnectionErrors {
					return retry.Permanent(err)
				}

				var httpMethod string

				// Use http.Request method if available
				if res != nil && res.Request != nil {
					httpMethod = res.Request.Method
				}

				isIdempotentHTTPMethod := slices.Contains(idempotentHTTPMethods, httpMethod)
				urlError := new(url.Error)

				if errors.As(err, &urlError) {
					if urlError.Temporary() || urlError.Timeout() {
						return err
					}

					// In certain error cases, the http.Request may not have
					// been populated, so use url.Error.Op which only has its
					// first character capitalized from the original request
					// HTTP method.
					if httpMethod == "" {
						httpMethod = strings.ToUpper(urlError.Op)
					}

					isIdempotentHTTPMethod = slices.Contains(idempotentHTTPMethods, httpMethod)

					// Connection closed
					if errors.Is(urlError.Err, io.EOF) && isIdempotentHTTPMethod {
						return err
					}
				}

				// syscall detection is not available on every platform, so
				// fallback to best effort string detection.
				isBrokenPipeError := strings.Contains(err.Error(), "broken pipe")
				isConnectionResetError := strings.Contains(err.Error(), "connection reset")

				if (isBrokenPipeError || isConnectionResetError) && isIdempotentHTTPMethod {
					return err
				}

				return retry.Permanent(err)
			}
			resp = res
			if res == nil {
				return fmt.Errorf("no response")
			}

			for _, code := range r.StatusCodes {
				if strings.Contains(strings.ToUpper(code), "X") {
					codeRange, err := strconv.Atoi(code[:1])
					if err != nil {
						continue
					}

					s := res.StatusCode / 100

					if s >= codeRange && s < codeRange+1 {
						return retry.TemporaryFromResponse("request failed", res)
					}
				} else {
					parsedCode, err := strconv.Atoi(code)
					if err != nil {
						continue
					}

					if res.StatusCode == parsedCode {
						return retry.TemporaryFromResponse("request failed", res)
					}
				}
			}

			resp = res

			return nil
		})

		var tempErr *retry.TemporaryError
		if err != nil && !errors.As(err, &tempErr) {
			return nil, err
		}

		return resp, nil
	default:
		return operation()
	}
}

func retryWithBackoff(ctx context.Context, s *retry.BackoffStrategy, operation func() error) error {
	var (
		err            error
		next           time.Duration
		attempt        int
		start          = time.Now()
		maxElapsedTime = time.Duration(s.MaxElapsedTime) * time.Millisecond
	)

	timer := &defaultTimer{}
	defer func() {
		timer.Stop()
	}()

	for {
		err = operation()
		if err == nil {
			return nil
		}

		var permanent *retry.PermanentError
		if errors.As(err, &permanent) {
			return permanent.Unwrap()
		}

		if time.Since(start) >= maxElapsedTime {
			return err
		}

		var temporary *retry.TemporaryError
		if errors.As(err, &temporary) {
			next = temporary.RetryAfter()
		}

		if next <= 0 {
			next = nextInterval(s, attempt)
		}

		timer.Start(next)

		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-timer.C():
		}

		attempt += 1
	}
}

type Timer interface {
	Start(duration time.Duration)
	Stop()
	C() <-chan time.Time
}

// defaultTimer implements Timer interface using time.Timer
type defaultTimer struct {
	timer *time.Timer
}

// C returns the timers channel which receives the current time when the timer fires.
func (t *defaultTimer) C() <-chan time.Time {
	return t.timer.C
}

// Start starts the timer to fire after the given duration
func (t *defaultTimer) Start(duration time.Duration) {
	if t.timer == nil {
		t.timer = time.NewTimer(duration)
		return
	}

	if !t.timer.Stop() {
		select {
		case <-t.timer.C:
		default:
		}
	}

	t.timer.Reset(duration)
}

// Stop is called when the timer is not used anymore and resources may be freed.
func (t *defaultTimer) Stop() {
	if t.timer != nil {
		t.timer.Stop()
	}
}

func nextInterval(s *retry.BackoffStrategy, attempt int) time.Duration {
	initialInterval := float64(time.Duration(s.InitialInterval) * time.Millisecond)
	maxInterval := float64(time.Duration(s.MaxInterval) * time.Millisecond)
	exponent := s.Exponent
	jitterFactor := float64(0.25)

	interval := initialInterval * math.Pow(float64(attempt+1), exponent)

	jitter := rand.Float64() * jitterFactor * interval
	if rand.Float64() < 0.5 {
		jitter = -1 * jitter
	}

	interval = interval + jitter

	if interval <= 0 {
		interval = initialInterval
	}

	if interval > maxInterval {
		interval = maxInterval
	}

	return time.Duration(interval)
}
