package client

import (
	"math"
	"strconv"
	"time"

	"github.com/IBM/ibm-cos-sdk-go/aws/request"
	"github.com/IBM/ibm-cos-sdk-go/internal/sdkrand"
)

// DefaultRetryer implements basic retry logic using exponential backoff for
// most services. If you want to implement custom retry logic, you can implement the
// request.Retryer interface.
type DefaultRetryer struct {
	// Num max Retries is the number of max retries that will be performed.
	// By default, this is zero.
	NumMaxRetries int

	// MinRetryDelay is the minimum retry delay after which retry will be performed.
	// If not set, the value is 0ns.
	MinRetryDelay time.Duration

	// MinThrottleRetryDelay is the minimum retry delay when throttled.
	// If not set, the value is 0ns.
	MinThrottleDelay time.Duration

	// MaxRetryDelay is the maximum retry delay before which retry must be performed.
	// If not set, the value is 0ns.
	MaxRetryDelay time.Duration

	// MaxThrottleDelay is the maximum retry delay when throttled.
	// If not set, the value is 0ns.
	MaxThrottleDelay time.Duration
}

const (
	// DefaultRetryerMaxNumRetries sets maximum number of retries
	DefaultRetryerMaxNumRetries = 3

	// DefaultRetryerMinRetryDelay sets minimum retry delay
	DefaultRetryerMinRetryDelay = 30 * time.Millisecond

	// DefaultRetryerMinThrottleDelay sets minimum delay when throttled
	DefaultRetryerMinThrottleDelay = 500 * time.Millisecond

	// DefaultRetryerMaxRetryDelay sets maximum retry delay
	DefaultRetryerMaxRetryDelay = 300 * time.Second

	// DefaultRetryerMaxThrottleDelay sets maximum delay when throttled
	DefaultRetryerMaxThrottleDelay = 300 * time.Second
)

// MaxRetries returns the number of maximum returns the service will use to make
// an individual API request.
func (d DefaultRetryer) MaxRetries() int {
	return d.NumMaxRetries
}

// setRetryerDefaults sets the default values of the retryer if not set
func (d *DefaultRetryer) setRetryerDefaults() {
	if d.MinRetryDelay == 0 {
		d.MinRetryDelay = DefaultRetryerMinRetryDelay
	}
	if d.MaxRetryDelay == 0 {
		d.MaxRetryDelay = DefaultRetryerMaxRetryDelay
	}
	if d.MinThrottleDelay == 0 {
		d.MinThrottleDelay = DefaultRetryerMinThrottleDelay
	}
	if d.MaxThrottleDelay == 0 {
		d.MaxThrottleDelay = DefaultRetryerMaxThrottleDelay
	}
}

// RetryRules returns the delay duration before retrying this request again
func (d DefaultRetryer) RetryRules(r *request.Request) time.Duration {

	// if number of max retries is zero, no retries will be performed.
	if d.NumMaxRetries == 0 {
		return 0
	}

	// Sets default value for retryer members
	d.setRetryerDefaults()

	// minDelay is the minimum retryer delay
	minDelay := d.MinRetryDelay

	var initialDelay time.Duration

	isThrottle := r.IsErrorThrottle()
	if isThrottle {
		if delay, ok := getRetryAfterDelay(r); ok {
			initialDelay = delay
		}
		minDelay = d.MinThrottleDelay
	}

	retryCount := r.RetryCount

	// maxDelay the maximum retryer delay
	maxDelay := d.MaxRetryDelay

	if isThrottle {
		maxDelay = d.MaxThrottleDelay
	}

	var delay time.Duration

	// Logic to cap the retry count based on the minDelay provided
	actualRetryCount := int(math.Log2(float64(minDelay))) + 1
	if actualRetryCount < 63-retryCount {
		delay = time.Duration(1<<uint64(retryCount)) * getJitterDelay(minDelay)
		if delay > maxDelay {
			delay = getJitterDelay(maxDelay / 2)
		}
	} else {
		delay = getJitterDelay(maxDelay / 2)
	}
	return delay + initialDelay
}

// getJitterDelay returns a jittered delay for retry
func getJitterDelay(duration time.Duration) time.Duration {
	return time.Duration(sdkrand.SeededRand.Int63n(int64(duration)) + int64(duration))
}

// ShouldRetry returns true if the request should be retried.
func (d DefaultRetryer) ShouldRetry(r *request.Request) bool {

	// ShouldRetry returns false if number of max retries is 0.
	if d.NumMaxRetries == 0 {
		return false
	}

	// If one of the other handlers already set the retry state
	// we don't want to override it based on the service's state
	if r.Retryable != nil {
		return *r.Retryable
	}
	return r.IsErrorRetryable() || r.IsErrorThrottle()
}

// This will look in the Retry-After header, RFC 7231, for how long
// it will wait before attempting another request
func getRetryAfterDelay(r *request.Request) (time.Duration, bool) {
	if !canUseRetryAfterHeader(r) {
		return 0, false
	}

	delayStr := r.HTTPResponse.Header.Get("Retry-After")
	if len(delayStr) == 0 {
		return 0, false
	}

	delay, err := strconv.Atoi(delayStr)
	if err != nil {
		return 0, false
	}

	return time.Duration(delay) * time.Second, true
}

// Will look at the status code to see if the retry header pertains to
// the status code.
func canUseRetryAfterHeader(r *request.Request) bool {
	switch r.HTTPResponse.StatusCode {
	case 429:
	case 503:
	default:
		return false
	}

	return true
}
