// Package backoff implements exponential backoff with jitter for retry logic.
package backoff

import (
	"fmt"
	"math"
	"math/rand/v2"
	"time"
)

// Config holds the configuration for a Backoff instance.
type Config struct {
	// Initial is the starting upper bound for the backoff duration.
	// Must be > 0.
	Initial time.Duration `yaml:"initial"`
	// Max is the absolute maximum upper bound for the backoff duration.
	// Must be >= Initial.
	Max time.Duration `yaml:"max"`
	// RNG is an optional source of randomness.
	// If nil, the global math/rand/v2 source is used.
	RNG *rand.Rand `yaml:"-"`
}

// Validate checks that the configuration values are sensible.
func (cfg Config) Validate() error {
	if cfg.Initial <= 0 {
		return fmt.Errorf("invalid initial backoff: %v", cfg.Initial)
	}
	if cfg.Max < cfg.Initial {
		return fmt.Errorf("invalid max duration: %v", cfg.Max)
	}
	// MaxInt64 will overflow when calculating jitter.
	if cfg.Max == math.MaxInt64 {
		return fmt.Errorf("max duration cannot be MaxInt64")
	}
	return nil
}

// Backoff implements exponential backoff with full jitter.
// It is NOT safe for concurrent use.
type Backoff struct {
	initial time.Duration
	max     time.Duration
	current time.Duration
	rng     *rand.Rand
}

// New creates a new Backoff instance with the provided configuration.
func New(cfg Config) (*Backoff, error) {
	if err := cfg.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %v", err)
	}

	return &Backoff{
		initial: cfg.Initial,
		max:     cfg.Max,
		current: cfg.Initial,
		rng:     cfg.RNG,
	}, nil
}

// Next returns the next backoff delay.
func (b *Backoff) Next() time.Duration {
	limit := b.current

	// Update state for the next call.
	if b.current >= b.max {
		b.current = b.max
	} else if b.current > b.max/2 {
		// If doubling would exceed max, just clamp to max.
		b.current = b.max
	} else {
		b.current *= 2
	}

	// Calculate jitter; return random in [0, limit].
	// Int64N(n) returns values in [0, n).
	// For [0, limit], we use limit + 1.
	var jitter int64
	if b.rng != nil {
		jitter = b.rng.Int64N(int64(limit) + 1)
	} else {
		jitter = rand.Int64N(int64(limit) + 1)
	}

	return time.Duration(jitter)
}

// Reset resets the current backoff cap to the initial value.
// This should be called after a successful operation to restart
// the backoff sequence for future retries.
func (b *Backoff) Reset() {
	b.current = b.initial
}
