package middleware

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

	"github.com/gin-gonic/gin"
)

// AppRateLimiter tracks API call counts per Connected App using fixed windows.
type AppRateLimiter struct {
	mu     sync.Mutex
	hourly map[string]*windowCounter
	daily  map[string]*windowCounter
}

type windowCounter struct {
	count     int
	windowEnd time.Time
}

// NewAppRateLimiter creates a new per-app rate limiter.
func NewAppRateLimiter() *AppRateLimiter {
	return &AppRateLimiter{
		hourly: make(map[string]*windowCounter),
		daily:  make(map[string]*windowCounter),
	}
}

// Middleware returns a gin middleware that enforces per-app rate limits.
// It reads the request's rate-limit subject, which is the Connected App when there is one and the
// personal access token otherwise. Keying on the Connected App alone exempted every PAT — a PAT has
// no app by design — so the longest-lived programmatic credential was the one class with no ceiling.
// Limits are applied only when defaultHourlyLimit > 0 or defaultDailyLimit > 0.
// Phase 2 will read per-app limits from the ConnectedApp table (DailyApiLimit, HourlyApiLimit).
func (l *AppRateLimiter) Middleware(defaultHourlyLimit, defaultDailyLimit int) gin.HandlerFunc {
	return func(c *gin.Context) {
		appIDStr := RateLimitSubject(c)
		if appIDStr == "" {
			// An interactive or browser session: no programmatic subject to throttle here.
			c.Next()
			return
		}

		l.mu.Lock()

		// Check hourly limit
		if defaultHourlyLimit > 0 {
			hc := l.getOrCreateCounter(l.hourly, appIDStr, time.Hour)
			if hc.count >= defaultHourlyLimit {
				l.mu.Unlock()
				c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
					"errorCode": "REQUEST_LIMIT_EXCEEDED",
					"message":   "Hourly API request limit exceeded for this Connected App",
				})
				return
			}
			hc.count++
		}

		// Check daily limit
		if defaultDailyLimit > 0 {
			dc := l.getOrCreateCounter(l.daily, appIDStr, 24*time.Hour)
			if dc.count >= defaultDailyLimit {
				l.mu.Unlock()
				c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
					"errorCode": "REQUEST_LIMIT_EXCEEDED",
					"message":   "Daily API request limit exceeded for this Connected App",
				})
				return
			}
			dc.count++
		}

		l.mu.Unlock()
		c.Next()
	}
}

// getOrCreateCounter returns the existing counter for key if still within its
// window, or creates a fresh one if the window has expired.
// Caller must hold l.mu.
func (l *AppRateLimiter) getOrCreateCounter(m map[string]*windowCounter, key string, window time.Duration) *windowCounter {
	wc, exists := m[key]
	now := time.Now()
	if !exists || now.After(wc.windowEnd) {
		wc = &windowCounter{count: 0, windowEnd: now.Add(window)}
		m[key] = wc
	}
	return wc
}
