// Package ratelimit implements a sliding window rate limiter using
// Redis-like semantics but backed by a sync.Map for in-process use.
//
// Unlike token bucket (bursty) or fixed window (boundary spikes),
// sliding window gives smooth, predictable rate limiting. It counts
// requests in the current window proportionally blended with the
// previous window.
//
// Example: 100 req/min limit. At t=0:45 (45s into current window),
// previous window had 80 requests, current window has 30.
// Weighted count = 80 * (15/60) + 30 = 50. Under limit, allow.

package ratelimit

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

type Limiter struct {
	rate   int
	window time.Duration
	mu     sync.Map
}

type entry struct {
	mu       sync.Mutex
	prevCount int64
	currCount int64
	windowStart time.Time
}

// New creates a limiter allowing `rate` requests per `window`.
func New(rate int, window time.Duration) *Limiter {
	return &Limiter{rate: rate, window: window}
}

// Allow checks whether a request from the given key should be permitted.
// Returns (allowed, remaining, resetAt).
func (l *Limiter) Allow(key string) (bool, int, time.Time) {
	now := time.Now()

	val, _ := l.mu.LoadOrStore(key, &entry{
		windowStart: now.Truncate(l.window),
	})
	e := val.(*entry)

	e.mu.Lock()
	defer e.mu.Unlock()

	currentWindow := now.Truncate(l.window)

	// Advance windows if needed
	switch {
	case currentWindow.After(e.windowStart.Add(l.window)):
		// Two or more windows have passed — reset everything
		e.prevCount = 0
		e.currCount = 0
		e.windowStart = currentWindow
	case currentWindow.After(e.windowStart):
		// One window has passed — rotate
		e.prevCount = e.currCount
		e.currCount = 0
		e.windowStart = currentWindow
	}

	// Sliding window weighted count
	elapsed := now.Sub(e.windowStart)
	prevWeight := 1.0 - (float64(elapsed) / float64(l.window))
	weighted := float64(e.prevCount)*prevWeight + float64(e.currCount)

	resetAt := e.windowStart.Add(l.window)

	if int(weighted) >= l.rate {
		remaining := 0
		return false, remaining, resetAt
	}

	e.currCount++
	remaining := l.rate - int(weighted) - 1
	return true, remaining, resetAt
}

// Stats returns current counters for a key (for monitoring).
type Stats struct {
	Key       string
	Current   int64
	Previous  int64
	Weighted  float64
	WindowAge time.Duration
}

var activeKeys atomic.Int64

func (l *Limiter) Stats(key string) *Stats {
	val, ok := l.mu.Load(key)
	if !ok {
		return nil
	}
	e := val.(*entry)
	e.mu.Lock()
	defer e.mu.Unlock()

	elapsed := time.Since(e.windowStart)
	prevWeight := 1.0 - (float64(elapsed) / float64(l.window))
	if prevWeight < 0 {
		prevWeight = 0
	}

	return &Stats{
		Key:       key,
		Current:   e.currCount,
		Previous:  e.prevCount,
		Weighted:  float64(e.prevCount)*prevWeight + float64(e.currCount),
		WindowAge: elapsed,
	}
}
