package resil

import (
	"sync"
	"time"
)

// Budget caps retries to a fraction of total request volume. Each request
// adds `ratio` tokens; each retry costs 1; an empty bucket means no
// retry. This bounds amplification GLOBALLY — unlike a per-request retry
// count, which still multiplies load when EVERY request fails at once
// (the retry-storm / metastable-failure trap). Starts empty
// (conservative): you earn retry budget by serving real traffic.
type Budget struct {
	mu     sync.Mutex
	tokens float64
	ratio  float64
	max    float64
}

func NewBudget(ratio float64) *Budget {
	return &Budget{ratio: ratio, max: 1000}
}

// OnRequest accrues budget for one served request.
func (b *Budget) OnRequest() {
	b.mu.Lock()
	b.tokens += b.ratio
	if b.tokens > b.max {
		b.tokens = b.max
	}
	b.mu.Unlock()
}

// TryRetry spends one token if available.
func (b *Budget) TryRetry() bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.tokens >= 1 {
		b.tokens--
		return true
	}
	return false
}

// Tokens exposes the current budget (for tests/observability).
func (b *Budget) Tokens() float64 {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.tokens
}

// Circuit-breaker states.
type CBState int

const (
	Closed CBState = iota
	Open
	HalfOpen
)

func (s CBState) String() string { return [...]string{"closed", "open", "half-open"}[s] }

// CircuitBreaker fails fast on a sustainedly-failing dependency: it opens
// after `Threshold` consecutive failures, rejects calls for `Cooldown`,
// then lets a single probe through (half-open); a probe success closes
// it, a probe failure reopens it.
type CircuitBreaker struct {
	mu        sync.Mutex
	state     CBState
	failures  int
	openedAt  time.Time
	Threshold int
	Cooldown  time.Duration
	now       func() time.Time
}

func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker {
	return &CircuitBreaker{Threshold: threshold, Cooldown: cooldown, now: time.Now}
}

// Allow reports whether a call may proceed.
func (c *CircuitBreaker) Allow() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.state == Open {
		if c.now().Sub(c.openedAt) >= c.Cooldown {
			c.state = HalfOpen // let one probe through
			return true
		}
		return false // fail fast — don't call a known-sick dependency
	}
	return true
}

// Record feeds a call result back into the breaker.
func (c *CircuitBreaker) Record(ok bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if ok {
		c.failures = 0
		c.state = Closed
		return
	}
	c.failures++
	if c.state == HalfOpen || c.failures >= c.Threshold {
		c.state = Open
		c.openedAt = c.now()
	}
}

// State exposes the current state (for tests/observability).
func (c *CircuitBreaker) State() CBState {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.state
}
