package retry

import (
	"context"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
)

// AddWithErrorCodes returns a Retryer with additional error codes considered
// for determining if the error should be retried.
func AddWithErrorCodes(r aws.Retryer, codes ...string) aws.Retryer {
	retryable := &RetryableErrorCode{
		Codes: map[string]struct{}{},
	}
	for _, c := range codes {
		retryable.Codes[c] = struct{}{}
	}

	return &withIsErrorRetryable{
		RetryerV2: wrapAsRetryerV2(r),
		Retryable: retryable,
	}
}

type withIsErrorRetryable struct {
	aws.RetryerV2
	Retryable IsErrorRetryable
}

func (r *withIsErrorRetryable) IsErrorRetryable(err error) bool {
	if v := r.Retryable.IsErrorRetryable(err); v != aws.UnknownTernary {
		return v.Bool()
	}
	return r.RetryerV2.IsErrorRetryable(err)
}

// AddWithMaxAttempts returns a Retryer with MaxAttempts set to the value
// specified.
func AddWithMaxAttempts(r aws.Retryer, max int) aws.Retryer {
	return &withMaxAttempts{
		RetryerV2: wrapAsRetryerV2(r),
		Max:       max,
	}
}

type withMaxAttempts struct {
	aws.RetryerV2
	Max int
}

func (w *withMaxAttempts) MaxAttempts() int {
	return w.Max
}

// AddWithMaxBackoffDelay returns a retryer wrapping the passed in retryer
// overriding the RetryDelay behavior for a alternate minimum initial backoff
// delay.
func AddWithMaxBackoffDelay(r aws.Retryer, delay time.Duration) aws.Retryer {
	return &withMaxBackoffDelay{
		RetryerV2: wrapAsRetryerV2(r),
		backoff:   NewExponentialJitterBackoff(delay),
	}
}

type withMaxBackoffDelay struct {
	aws.RetryerV2
	backoff *ExponentialJitterBackoff
}

func (r *withMaxBackoffDelay) RetryDelay(attempt int, err error) (time.Duration, error) {
	return r.backoff.BackoffDelay(attempt, err)
}

// AddWithLongPolling returns a retryer that is marked as long-polling.
// Long-polling operations will back off even when the retry quota is
// exhausted.
func AddWithLongPolling(r aws.Retryer) aws.Retryer {
	return &withLongPolling{RetryerV2: wrapAsRetryerV2(r)}
}

type withLongPolling struct {
	aws.RetryerV2
}

func (w *withLongPolling) IsLongPolling() bool { return true }

type wrappedAsRetryerV2 struct {
	aws.Retryer
}

func wrapAsRetryerV2(r aws.Retryer) aws.RetryerV2 {
	v, ok := r.(aws.RetryerV2)
	if !ok {
		v = wrappedAsRetryerV2{Retryer: r}
	}

	return v
}

func (w wrappedAsRetryerV2) GetAttemptToken(context.Context) (func(error) error, error) {
	return w.Retryer.GetInitialToken(), nil
}
