package portal

import (
	"context"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"
)

// RateLimitConfig configures the rate limiter.
type RateLimitConfig struct {
	RequestsPerMinute int `yaml:"requests_per_minute"`
	BurstSize         int `yaml:"burst_size"`
}

// RateLimiter provides per-IP token bucket rate limiting.
type RateLimiter struct {
	mu      sync.Mutex
	buckets map[string]*bucket
	rate    float64 // tokens per second
	burst   int
	stop    context.CancelFunc
}

type bucket struct {
	tokens   float64
	lastSeen time.Time
}

// NewRateLimiter creates a rate limiter from config.
func NewRateLimiter(cfg RateLimitConfig) *RateLimiter {
	rpm := cfg.RequestsPerMinute
	if rpm <= 0 {
		rpm = defaultRPM
	}
	burst := cfg.BurstSize
	if burst <= 0 {
		burst = defaultBurst
	}
	ctx, cancel := context.WithCancel(context.Background())
	rl := &RateLimiter{
		buckets: make(map[string]*bucket),
		rate:    float64(rpm) / secondsPerMinute,
		burst:   burst,
		stop:    cancel,
	}
	go rl.cleanupLoop(ctx)
	return rl
}

const (
	// defaultRPM is the default requests per minute.
	defaultRPM = 60
	// defaultBurst is the default burst size.
	defaultBurst = 10
	// secondsPerMinute converts RPM to per-second rate.
	secondsPerMinute = 60.0
	// cleanupInterval is how often the background goroutine runs.
	cleanupInterval = 10 * time.Minute
	// cleanupMaxAge is the max idle time before a bucket is evicted.
	cleanupMaxAge = 30 * time.Minute
)

// Allow checks whether a request from the given IP should be allowed.
func (rl *RateLimiter) Allow(ip string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	b, ok := rl.buckets[ip]
	if !ok {
		b = &bucket{tokens: float64(rl.burst), lastSeen: now}
		rl.buckets[ip] = b
	}

	// Refill tokens based on elapsed time.
	elapsed := now.Sub(b.lastSeen).Seconds()
	b.tokens += elapsed * rl.rate
	if b.tokens > float64(rl.burst) {
		b.tokens = float64(rl.burst)
	}
	b.lastSeen = now

	if b.tokens < 1 {
		return false
	}
	b.tokens--
	return true
}

// Cleanup removes stale entries older than the given duration.
func (rl *RateLimiter) Cleanup(maxAge time.Duration) {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	cutoff := time.Now().Add(-maxAge)
	for ip, b := range rl.buckets {
		if b.lastSeen.Before(cutoff) {
			delete(rl.buckets, ip)
		}
	}
}

// cleanupLoop periodically evicts stale rate-limit buckets.
func (rl *RateLimiter) cleanupLoop(ctx context.Context) {
	rl.runCleanupLoop(ctx, cleanupInterval, cleanupMaxAge)
}

// runCleanupLoop is the testable core of cleanupLoop.
func (rl *RateLimiter) runCleanupLoop(ctx context.Context, interval, maxAge time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			rl.Cleanup(maxAge)
			slog.Debug("rate limiter cleanup completed")
		}
	}
}

// Close stops the background cleanup goroutine.
func (rl *RateLimiter) Close() {
	if rl.stop != nil {
		rl.stop()
	}
}

// Middleware wraps an http.Handler with rate limiting.
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ip := clientIP(r)
		if !rl.Allow(ip) {
			http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// clientIP extracts the client IP, respecting X-Forwarded-For.
func clientIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		// First entry is the original client.
		if ip, _, ok := strings.Cut(xff, ","); ok {
			return strings.TrimSpace(ip)
		}
		return strings.TrimSpace(xff)
	}
	// Strip port from RemoteAddr.
	if host, _, ok := strings.Cut(r.RemoteAddr, ":"); ok {
		return host
	}
	return r.RemoteAddr
}
