package middleware

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"golang.org/x/time/rate"
)

// RateLimiterConfig configuration pour le rate limiter
type RateLimiterConfig struct {
	// Limites par IP (non authentifié)
	IPRequestsPerMinute int
	IPBurst             int

	// Limites par utilisateur authentifié
	UserRequestsPerMinute int
	UserBurst             int

	// Configuration Redis
	RedisClient *redis.Client
	KeyPrefix   string
}

// RateLimiter middleware pour limiter le taux de requêtes
type RateLimiter struct {
	config      *RateLimiterConfig
	ipLimiter   *rate.Limiter
	userLimiter *rate.Limiter
}

// NewRateLimiter crée un nouveau rate limiter
func NewRateLimiter(config *RateLimiterConfig) *RateLimiter {
	return &RateLimiter{
		config: config,
		ipLimiter: rate.NewLimiter(
			rate.Every(time.Minute/time.Duration(config.IPRequestsPerMinute)),
			config.IPBurst,
		),
		userLimiter: rate.NewLimiter(
			rate.Every(time.Minute/time.Duration(config.UserRequestsPerMinute)),
			config.UserBurst,
		),
	}
}

// Routes exclues du rate limiting (routes critiques)
var excludedRateLimitPathsRedis = []string{
	"/health",
	"/healthz",
	"/readyz",
	"/api/v1/health",
	"/api/v1/healthz",
	"/api/v1/readyz",
	"/api/v1/csrf-token",
	"/api/v1/auth/register",
	"/api/v1/auth/login",
	"/api/v1/auth/refresh",
	"/api/v1/auth/verify-email",
	"/api/v1/auth/resend-verification",
	"/api/v1/auth/check-username",
	"/swagger",
	"/docs",
}

// isExcludedPathRedis vérifie si un chemin est exclu du rate limiting (version Redis)
func isExcludedPathRedis(path string) bool {
	for _, excluded := range excludedRateLimitPathsRedis {
		if path == excluded || (len(path) > len(excluded) && path[:len(excluded)] == excluded) {
			return true
		}
	}
	return false
}

// RateLimitMiddleware middleware principal de rate limiting
func (rl *RateLimiter) RateLimitMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Exclure les routes critiques du rate limiting
		if isExcludedPathRedis(c.Request.URL.Path) {
			c.Next()
			return
		}

		// DÉSACTIVER le rate limiting en mode test/e2e/development pour les tests E2E et développement
		// Vérifier via header ou variable d'environnement
		if c.GetHeader("X-Test-Mode") == "true" ||
			c.GetHeader("X-E2E-Test") == "true" ||
			os.Getenv("NODE_ENV") == "test" ||
			os.Getenv("NODE_ENV") == "e2e" ||
			os.Getenv("NODE_ENV") == "development" ||
			os.Getenv("APP_ENV") == "test" ||
			os.Getenv("APP_ENV") == "e2e" ||
			os.Getenv("APP_ENV") == "development" {
			c.Next()
			return
		}

		// Déterminer si l'utilisateur est authentifié
		userIDInterface, isAuthenticated := c.Get("user_id")

		var limiter *rate.Limiter
		var key string
		var limit int

		if isAuthenticated {
			// Utilisateur authentifié - limite plus élevée
			// BE-SVC-002: Support UUID for user rate limiting
			limiter = rl.userLimiter
			// Convertir userID en string pour la clé Redis
			var userIDStr string
			switch v := userIDInterface.(type) {
			case uuid.UUID:
				userIDStr = v.String()
			case string:
				userIDStr = v
			default:
				userIDStr = fmt.Sprintf("%v", v)
			}
			key = fmt.Sprintf("%s:user:%s", rl.config.KeyPrefix, userIDStr)
			limit = rl.config.UserRequestsPerMinute
		} else {
			// IP non authentifiée - limite plus stricte
			limiter = rl.ipLimiter
			key = fmt.Sprintf("%s:ip:%s", rl.config.KeyPrefix, c.ClientIP())
			limit = rl.config.IPRequestsPerMinute
		}

		// Vérifier la limite avec Redis pour persistance
		allowed, remaining, err := rl.checkRedisLimit(c.Request.Context(), key, limit)
		if err != nil {
			// En cas d'erreur Redis, utiliser le limiter local
			allowed = limiter.Allow()
			remaining = int(limiter.Tokens())
		}

		// Ajouter les headers de rate limiting
		c.Header("X-RateLimit-Limit", strconv.Itoa(limit))
		c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
		c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10))

		if !allowed {
			c.JSON(http.StatusTooManyRequests, gin.H{
				"error":       "Rate limit exceeded",
				"retry_after": 60,
			})
			c.Abort()
			return
		}

		c.Next()
	}
}

// checkRedisLimit vérifie la limite dans Redis
func (rl *RateLimiter) checkRedisLimit(ctx context.Context, key string, limit int) (bool, int, error) {
	// Utiliser un script Lua pour l'atomicité
	script := `
		local key = KEYS[1]
		local limit = tonumber(ARGV[1])
		local window = tonumber(ARGV[2])
		
		local current = redis.call('GET', key)
		if current == false then
			redis.call('SET', key, 1, 'EX', window)
			return {1, limit - 1}
		end
		
		local count = tonumber(current)
		if count < limit then
			redis.call('INCR', key)
			return {1, limit - count - 1}
		else
			return {0, 0}
		end
	`

	result, err := rl.config.RedisClient.Eval(
		ctx,
		script,
		[]string{key},
		limit,
		60, // 60 secondes
	).Result()

	if err != nil {
		return false, 0, err
	}

	results := result.([]interface{})
	allowed := results[0].(int64) == 1
	remaining := int(results[1].(int64))

	return allowed, remaining, nil
}

// RateLimitByIP middleware pour limiter par IP uniquement
func (rl *RateLimiter) RateLimitByIP() gin.HandlerFunc {
	return func(c *gin.Context) {
		key := fmt.Sprintf("%s:ip:%s", rl.config.KeyPrefix, c.ClientIP())
		allowed, remaining, err := rl.checkRedisLimit(c.Request.Context(), key, rl.config.IPRequestsPerMinute)

		if err != nil {
			allowed = rl.ipLimiter.Allow()
			remaining = int(rl.ipLimiter.Tokens())
		}

		// INT-013: Standardize rate limit response format
		resetTime := time.Now().Add(time.Minute).Unix()
		retryAfter := 60

		c.Header("X-RateLimit-Limit", strconv.Itoa(rl.config.IPRequestsPerMinute))
		c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
		c.Header("X-RateLimit-Reset", strconv.FormatInt(resetTime, 10))

		if !allowed {
			c.Header("Retry-After", strconv.Itoa(retryAfter))
			c.JSON(http.StatusTooManyRequests, gin.H{
				"success": false,
				"error": gin.H{
					"code":    429,
					"message": "Rate limit exceeded. Please try again later.",
					"details": []gin.H{
						{
							"field":   "rate_limit",
							"message": fmt.Sprintf("You have exceeded the rate limit of %d requests per minute", rl.config.IPRequestsPerMinute),
						},
					},
					"retry_after": retryAfter,
					"limit":       rl.config.IPRequestsPerMinute,
					"remaining":   0,
					"reset":       resetTime,
				},
			})
			c.Abort()
			return
		}

		c.Next()
	}
}

// UploadRateLimit middleware pour limiter les uploads de tracks par utilisateur
// Limite: 10 uploads par heure par utilisateur
func UploadRateLimit(redisClient *redis.Client) gin.HandlerFunc {
	return func(c *gin.Context) {
		userID := c.GetInt64("user_id")
		if userID == 0 {
			// Si pas d'utilisateur authentifié, passer au suivant
			c.Next()
			return
		}

		// Clé Redis pour cet utilisateur
		key := fmt.Sprintf("upload_rate_limit:%d", userID)
		limit := 10 // 10 uploads par heure
		window := time.Hour

		// Script Lua pour l'atomicité
		script := `
			local key = KEYS[1]
			local limit = tonumber(ARGV[1])
			local window = tonumber(ARGV[2])
			
			local current = redis.call('GET', key)
			if current == false then
				redis.call('SET', key, 1, 'EX', window)
				return {1, limit - 1}
			end
			
			local count = tonumber(current)
			if count < limit then
				redis.call('INCR', key)
				return {1, limit - count - 1}
			else
				return {0, 0}
			end
		`

		result, err := redisClient.Eval(
			c.Request.Context(),
			script,
			[]string{key},
			limit,
			int(window.Seconds()),
		).Result()

		if err != nil {
			// En cas d'erreur Redis, autoriser la requête (fail-open)
			c.Next()
			return
		}

		results := result.([]interface{})
		allowed := results[0].(int64) == 1
		remaining := int(results[1].(int64))

		// Ajouter les headers de rate limiting
		c.Header("X-RateLimit-Limit", strconv.Itoa(limit))
		c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
		c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(window).Unix(), 10))

		if !allowed {
			c.JSON(http.StatusTooManyRequests, gin.H{
				"error":       "upload rate limit exceeded",
				"retry_after": int(window.Seconds()),
			})
			c.Abort()
			return
		}

		c.Next()
	}
}
