package firewall

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

type rateLimitEntry struct {
	count atomic.Int64
	start atomic.Int64 // UnixNano
}

type rateLimiter struct {
	limit   int64
	window  time.Duration
	entries sync.Map // netip.Addr → *rateLimitEntry
	now     func() time.Time
	done    chan struct{}
}

func newRateLimiter(limit int64, window time.Duration) *rateLimiter {
	rl := &rateLimiter{
		limit:  limit,
		window: window,
		now:    time.Now,
		done:   make(chan struct{}),
	}

	go rl.cleanup()
	return rl
}

// Allow checks whether the given address is within its rate limit window.
// Returns false if the address has exceeded the limit.
func (rl *rateLimiter) Allow(addr netip.Addr) bool {
	now := rl.now().UnixNano()

	val, _ := rl.entries.LoadOrStore(addr, &rateLimitEntry{})
	entry := val.(*rateLimitEntry)

	windowStart := entry.start.Load()
	if now-windowStart >= rl.window.Nanoseconds() {
		// Window expired — reset. Intentionally non-atomic across the two
		// stores; worst case a few extra connections slip through at the
		// boundary, which is acceptable for a connection rate limiter.
		entry.start.Store(now)
		entry.count.Store(1)
		return true
	}

	n := entry.count.Add(1)
	return n <= rl.limit
}

// Stop terminates the cleanup goroutine.
func (rl *rateLimiter) Stop() {
	close(rl.done)
}

func (rl *rateLimiter) cleanup() {
	ticker := time.NewTicker(rl.window)
	defer ticker.Stop()

	for {
		select {
		case <-rl.done:
			return
		case <-ticker.C:
			cutoff := rl.now().Add(-2 * rl.window).UnixNano()
			rl.entries.Range(func(key, value any) bool {
				entry := value.(*rateLimitEntry)
				if entry.start.Load() < cutoff {
					rl.entries.Delete(key)
				}
				return true
			})
		}
	}
}
