package middleware

import (
	"net/http"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

// limiterEntry holds a rate limiter and the last time it was accessed as
// unix nanoseconds. lastAccess is accessed atomically to avoid data races
// between GetLimiter (writer) and cleanup (reader) under concurrent requests.
type limiterEntry struct {
	limiter    *rate.Limiter
	lastAccess atomic.Int64
}

// NewIPRateLimiterMiddleware creates a middleware that limits the number of requests per IP
// limit is the number of requests per second
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
func NewIPRateLimiterMiddleware(limit float64, burst int) gin.HandlerFunc {
	ipLimiter := NewKeyRateLimiter(rate.Limit(limit), burst, 10*time.Minute)
	return func(c *gin.Context) {
		limiter := ipLimiter.GetLimiter(c.ClientIP())
		if !limiter.Allow() {
			c.AbortWithStatus(http.StatusTooManyRequests)
			return
		}
		c.Next()
	}
}

// NewScimRateLimiterMiddleware limits SCIM requests per company rather than per
// IP. cloud identity providers (Microsoft Entra, Okta) send every tenant's SCIM
// traffic from a shared pool of source IPs, so an IP based limit would throttle
// all companies together. keying on the companyID path param gives each company
// its own bucket. requests without a companyID fall back to the client IP.
// limit is requests per second, burst is the maximum burst size.
func NewScimRateLimiterMiddleware(limit float64, burst int) gin.HandlerFunc {
	companyLimiter := NewKeyRateLimiter(rate.Limit(limit), burst, 10*time.Minute)
	return func(c *gin.Context) {
		key := c.Param("companyID")
		if key == "" {
			key = c.ClientIP()
		}
		limiter := companyLimiter.GetLimiter(key)
		if !limiter.Allow() {
			c.AbortWithStatus(http.StatusTooManyRequests)
			return
		}
		c.Next()
	}
}

// KeyRateLimiter is a rate limiter for key such as username, email or IP
type KeyRateLimiter struct {
	// key is a map of key to limiterEntry
	key sync.Map
	// limiter is the rate limit, e.g. 1 request per second
	limiter rate.Limit
	// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
	burst int
	// cleanupInterval is the interval at which idle entries are evicted
	cleanupInterval time.Duration
}

// NewKeyRateLimiter creates a new key rate limiter
// limiter is the rate limit, e.g. 1 request per second
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
func NewKeyRateLimiter(
	limiter rate.Limit,
	burst int,
	cleanupInterval time.Duration,
) *KeyRateLimiter {
	rl := &KeyRateLimiter{
		limiter:         limiter,
		burst:           burst,
		cleanupInterval: cleanupInterval,
	}
	go rl.cleanup()
	return rl
}

// cleanup evicts entries that have not been accessed within the cleanup interval,
// preventing unbounded memory growth from the sync.Map
func (r *KeyRateLimiter) cleanup() {
	for range time.Tick(r.cleanupInterval) {
		threshold := time.Now().Add(-r.cleanupInterval).UnixNano()
		r.key.Range(func(key, value interface{}) bool {
			entry, ok := value.(*limiterEntry)
			if !ok {
				// remove any entry with an unexpected type
				r.key.Delete(key)
				return true
			}
			if entry.lastAccess.Load() < threshold {
				r.key.Delete(key)
			}
			return true
		})
	}
}

// GetLimiter gets the limiter for a key or creates one if it does not exist
func (r *KeyRateLimiter) GetLimiter(key string) *rate.Limiter {
	entry := &limiterEntry{
		limiter: rate.NewLimiter(r.limiter, r.burst),
	}
	entry.lastAccess.Store(time.Now().UnixNano())

	// LoadOrStore atomically either stores our new entry or returns the
	// existing one — correctly handles both the common case and concurrent
	// goroutines racing to create an entry for the same key
	actual, loaded := r.key.LoadOrStore(key, entry)
	if loaded {
		existing := actual.(*limiterEntry)
		existing.lastAccess.Store(time.Now().UnixNano())
		return existing.limiter
	}

	return entry.limiter
}
