package resil

import (
	"math"
	"sync"
	"sync/atomic"
	"time"
)

// Adaptive is a gradient-style adaptive concurrency limiter (the Netflix
// concurrency-limits idea). By Little's Law, in-flight = arrival_rate ×
// latency; below the knee, raising the limit raises throughput at flat
// latency; past it, latency climbs while throughput doesn't. So we track
// the minimum (no-load) latency and shrink the limit when current
// latency climbs above it, grow it when they're close. Excess requests
// are shed at admission (Acquire returns false) — bounded latency under
// overload, with no magic-number tuning.
type Adaptive struct {
	mu       sync.Mutex
	limit    float64
	minRTT   time.Duration
	inflight atomic.Int64

	min, max float64
}

func NewAdaptive() *Adaptive {
	return &Adaptive{limit: 20, min: 1, max: 2000, minRTT: time.Hour}
}

func (a *Adaptive) Limit() float64   { a.mu.Lock(); defer a.mu.Unlock(); return a.limit }
func (a *Adaptive) Inflight() int64  { return a.inflight.Load() }

// Acquire admits a request if in-flight is under the current limit. The
// returned release MUST be called with the observed RTT (and whether the
// request was dropped/errored) to drive the control loop.
func (a *Adaptive) Acquire() (release func(rtt time.Duration, dropped bool), ok bool) {
	if float64(a.inflight.Add(1)) > a.Limit() {
		a.inflight.Add(-1)
		return nil, false // SHED at admission
	}
	start := time.Now()
	return func(rtt time.Duration, dropped bool) {
		a.inflight.Add(-1)
		if rtt == 0 {
			rtt = time.Since(start)
		}
		a.update(rtt, dropped)
	}, true
}

func (a *Adaptive) update(rtt time.Duration, dropped bool) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if rtt > 0 && rtt < a.minRTT {
		a.minRTT = rtt // track the uncontended baseline
	}
	if dropped {
		a.limit = math.Max(a.min, a.limit*0.9) // multiplicative decrease on error
		return
	}
	// gradient = noload/current, clamped to [0.5, 1.0]; sqrt(limit) is the
	// additive growth headroom when gradient ~ 1.
	gradient := math.Max(0.5, math.Min(1.0, float64(a.minRTT)/float64(rtt)))
	newLimit := a.limit*gradient + math.Sqrt(a.limit)
	a.limit = math.Max(a.min, math.Min(a.max, newLimit))
}
