// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package retry

import (
	"context"
	"errors"
	"sync"
	"time"
)

// RetryContext is a basic wrapper around StateChangeConf that will just retry
// a function until it no longer returns an error.
//
// Cancellation from the passed in context will propagate through to the
// underlying StateChangeConf
func RetryContext(ctx context.Context, timeout time.Duration, f RetryFunc) error {
	// These are used to pull the error out of the function; need a mutex to
	// avoid a data race.
	var resultErr error
	var resultErrMu sync.Mutex

	c := &StateChangeConf{
		Pending:    []string{"retryableerror"},
		Target:     []string{"success"},
		Timeout:    timeout,
		MinTimeout: 500 * time.Millisecond,
		Refresh: func() (interface{}, string, error) {
			rerr := f()

			resultErrMu.Lock()
			defer resultErrMu.Unlock()

			if rerr == nil {
				resultErr = nil
				return 42, "success", nil
			}

			resultErr = rerr.Err

			if rerr.Retryable {
				return 42, "retryableerror", nil
			}
			return nil, "quit", rerr.Err
		},
	}

	_, waitErr := c.WaitForStateContext(ctx)

	// Need to acquire the lock here to be able to avoid race using resultErr as
	// the return value
	resultErrMu.Lock()
	defer resultErrMu.Unlock()

	// resultErr may be nil because the wait timed out and resultErr was never
	// set; this is still an error
	if resultErr == nil {
		return waitErr
	}
	// resultErr takes precedence over waitErr if both are set because it is
	// more likely to be useful
	return resultErr
}

// Retry is a basic wrapper around StateChangeConf that will just retry
// a function until it no longer returns an error.
//
// Deprecated: Please use RetryContext to ensure proper plugin shutdown
func Retry(timeout time.Duration, f RetryFunc) error {
	return RetryContext(context.Background(), timeout, f)
}

// RetryFunc is the function retried until it succeeds.
type RetryFunc func() *RetryError

// RetryError is the required return type of RetryFunc. It forces client code
// to choose whether or not a given error is retryable.
type RetryError struct {
	Err       error
	Retryable bool
}

func (e *RetryError) Unwrap() error {
	return e.Err
}

// RetryableError is a helper to create a RetryError that's retryable from a
// given error. To prevent logic errors, will return an error when passed a
// nil error.
func RetryableError(err error) *RetryError {
	if err == nil {
		return &RetryError{
			Err: errors.New("empty retryable error received. " +
				"This is a bug with the Terraform provider and should be " +
				"reported as a GitHub issue in the provider repository."),
			Retryable: false,
		}
	}
	return &RetryError{Err: err, Retryable: true}
}

// NonRetryableError is a helper to create a RetryError that's _not_ retryable
// from a given error. To prevent logic errors, will return an error when
// passed a nil error.
func NonRetryableError(err error) *RetryError {
	if err == nil {
		return &RetryError{
			Err: errors.New("empty non-retryable error received. " +
				"This is a bug with the Terraform provider and should be " +
				"reported as a GitHub issue in the provider repository."),
			Retryable: false,
		}
	}
	return &RetryError{Err: err, Retryable: false}
}
