package auth

import (
	"sync"
	"time"
)

// RateLimiter is an in-memory token bucket per key (§7: 10 attempts per
// 15 min per IP+email). Buckets refill continuously and idle entries are
// swept opportunistically.
type RateLimiter struct {
	mu       sync.Mutex
	buckets  map[string]*bucket
	capacity float64
	window   time.Duration
	now      func() time.Time // injectable for tests
	lastGC   time.Time
}

type bucket struct {
	tokens float64
	last   time.Time
}

func NewRateLimiter(attempts int, window time.Duration) *RateLimiter {
	return &RateLimiter{
		buckets:  make(map[string]*bucket),
		capacity: float64(attempts),
		window:   window,
		now:      time.Now,
	}
}

// Allow consumes one attempt for key, reporting whether it was within the
// limit.
func (rl *RateLimiter) Allow(key string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := rl.now()
	rl.gcLocked(now)

	b, ok := rl.buckets[key]
	if !ok {
		b = &bucket{tokens: rl.capacity, last: now}
		rl.buckets[key] = b
	}
	refill := now.Sub(b.last).Seconds() * (rl.capacity / rl.window.Seconds())
	b.tokens = min(rl.capacity, b.tokens+refill)
	b.last = now
	if b.tokens < 1 {
		return false
	}
	b.tokens--
	return true
}

// gcLocked drops buckets that have fully refilled (idle for >= one window);
// runs at most once per window to keep Allow O(1) amortized.
func (rl *RateLimiter) gcLocked(now time.Time) {
	if now.Sub(rl.lastGC) < rl.window {
		return
	}
	rl.lastGC = now
	for k, b := range rl.buckets {
		if now.Sub(b.last) >= rl.window {
			delete(rl.buckets, k)
		}
	}
}
