package util

import (
	"context"
	"fmt"
	"time"

	"errors"

	"github.com/gruntwork-io/terragrunt/pkg/log"
)

// DoWithRetry runs the specified action. If it returns a value, return that value. If it returns an error, sleep for
// sleepBetweenRetries and try again, up to a maximum of maxRetries retries. If maxRetries is exceeded, return a
// MaxRetriesExceeded error.
func DoWithRetry(
	ctx context.Context,
	actionDescription string,
	maxRetries int,
	sleepBetweenRetries time.Duration,
	logger log.Logger,
	logLevel log.Level,
	action func(ctx context.Context) error,
) error {
	for i := 0; i <= maxRetries; i++ {
		logger.Logf(logLevel, "%s", actionDescription)

		err := action(ctx)
		if err == nil {
			return nil
		}

		var fatalErr FatalError
		if ok := errors.As(err, &fatalErr); ok {
			return err
		}

		if ctx.Err() != nil {
			logger.Debugf("%s returned an error: %s.", actionDescription, err.Error())

			return ctx.Err()
		}

		logger.Errorf(
			"%s returned an error: %s. Retry %d of %d. Sleeping for %s and will try again.",
			actionDescription,
			err.Error(),
			i,
			maxRetries,
			sleepBetweenRetries,
		)

		select {
		case <-time.After(sleepBetweenRetries): // Try again
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	return MaxRetriesExceeded{Description: actionDescription, MaxRetries: maxRetries}
}

// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.
type MaxRetriesExceeded struct {
	Description string
	MaxRetries  int
}

func (err MaxRetriesExceeded) Error() string {
	return fmt.Sprintf("'%s' unsuccessful after %d retries", err.Description, err.MaxRetries)
}

// FatalError is error interface for cases that should not be retried.
type FatalError struct {
	Underlying error
}

func (err FatalError) Error() string {
	return err.Underlying.Error()
}

func (err FatalError) Unwrap() error {
	return err.Underlying
}
