package retry

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

// Sentinel errors.
//
// Implementations wrap or assert these via an Is method:
//
//	type MyError struct{ ... }
//	func (e *MyError) Is(target error) bool {
//	    return target == retry.ErrTransient  // always retry
//	}
var (
	ErrTransient = errors.New("transient error")
	ErrPermanent = errors.New("permanent error")
)

// Policy configures Do.
//
// MaxAttempts is the total attempt count (1 = no retry). Backoff returns the
// delay before the next attempt, given the 1-indexed attempt number that just
// failed. A nil Backoff means no delay.
type Policy struct {
	MaxAttempts int
	Backoff     func(attempt int) time.Duration
}

// Do calls fn up to policy.MaxAttempts times.
//
// Between attempts it sleeps for policy.Backoff(attempt), interruptible by
// ctx. Returns nil on success; otherwise the last error from fn.
//
// Retry decision: fn's error is retried iff errors.Is(err, ErrTransient) is
// true. ErrPermanent and unclassified errors return immediately.
//
// context.Cancellation during sleep returns ctx.Err() promptly. The context
// passed to fn is the caller's ctx; fn is responsible for its own timeouts.
func Do(ctx context.Context, policy Policy, fn func() error) error {
	attempts := policy.MaxAttempts
	if attempts < 1 {
		attempts = 1
	}
	var lastErr error
	for attempt := 1; attempt <= attempts; attempt++ {
		if err := ctx.Err(); err != nil {
			return err
		}
		lastErr = fn()
		if lastErr == nil {
			return nil
		}
		if !errors.Is(lastErr, ErrTransient) {
			return lastErr
		}
		if attempt == attempts {
			break
		}
		if policy.Backoff != nil {
			select {
			case <-time.After(policy.Backoff(attempt)):
			case <-ctx.Done():
				return ctx.Err()
			}
		}
	}
	return lastErr
}

// ExponentialBackoff returns a backoff function.
//
// delay = base * 2^(attempt-1), capped at maxDelay. attempt is 1-indexed
// (first failure → base, second → 2*base, third → 4*base, ...).
func ExponentialBackoff(base, maxDelay time.Duration) func(int) time.Duration {
	return func(attempt int) time.Duration {
		if attempt < 1 {
			attempt = 1
		}
		d := base * (1 << (attempt - 1))
		if d > maxDelay || d < base {
			d = maxDelay
		}
		return d
	}
}
