// Package filters provides built-in filters for the MCP Filter SDK.
package filters

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/GopherSecurity/gopher-mcp/src/types"
)

// ErrRateLimited is returned when rate limit is exceeded.
var ErrRateLimited = fmt.Errorf("rate limit exceeded")

// RateLimiter is the interface for different rate limiting algorithms.
type RateLimiter interface {
	TryAcquire(n int) bool
	LastAccess() time.Time
}

// RedisClient interface for Redis operations (to avoid direct dependency).
type RedisClient interface {
	Eval(ctx context.Context, script string, keys []string, args ...interface{}) (interface{}, error)
	Get(ctx context.Context, key string) (string, error)
	SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error)
	Del(ctx context.Context, keys ...string) error
	Ping(ctx context.Context) error
}

// RedisLimiter implements distributed rate limiting using Redis.
type RedisLimiter struct {
	client     RedisClient
	key        string
	limit      int
	window     time.Duration
	lastAccess time.Time
	mu         sync.RWMutex
}

// NewRedisLimiter creates a new Redis-based rate limiter.
func NewRedisLimiter(client RedisClient, key string, limit int, window time.Duration) *RedisLimiter {
	return &RedisLimiter{
		client:     client,
		key:        fmt.Sprintf("ratelimit:%s", key),
		limit:      limit,
		window:     window,
		lastAccess: time.Now(),
	}
}

// Lua script for atomic rate limit check and increment
const rateLimitLuaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local current = redis.call('GET', key)
if current == false then
    redis.call('SET', key, 1, 'EX', window)
    return 1
end

current = tonumber(current)
if current < limit then
    redis.call('INCR', key)
    return 1
end

return 0
`

// TryAcquire attempts to acquire n permits using Redis.
func (rl *RedisLimiter) TryAcquire(n int) bool {
	rl.mu.Lock()
	rl.lastAccess = time.Now()
	rl.mu.Unlock()

	ctx := context.Background()

	// Execute Lua script for atomic operation
	result, err := rl.client.Eval(
		ctx,
		rateLimitLuaScript,
		[]string{rl.key},
		rl.limit,
		int(rl.window.Seconds()),
		time.Now().Unix(),
	)

	// Handle Redis failures gracefully - fail open (allow request)
	if err != nil {
		// Log error (would use actual logger in production)
		// For now, fail open to avoid blocking legitimate traffic
		return true
	}

	// Check result
	if allowed, ok := result.(int64); ok {
		return allowed == 1
	}

	// Default to allowing on unexpected response
	return true
}

// LastAccess returns the last access time.
func (rl *RedisLimiter) LastAccess() time.Time {
	rl.mu.RLock()
	defer rl.mu.RUnlock()
	return rl.lastAccess
}

// SetFailureMode configures behavior when Redis is unavailable.
type FailureMode int

const (
	FailOpen   FailureMode = iota // Allow requests when Redis fails
	FailClosed                    // Deny requests when Redis fails
)

// RedisLimiterWithFailureMode extends RedisLimiter with configurable failure mode.
type RedisLimiterWithFailureMode struct {
	*RedisLimiter
	failureMode FailureMode
}

// TryAcquireWithFailureMode respects the configured failure mode.
func (rl *RedisLimiterWithFailureMode) TryAcquire(n int) bool {
	result := rl.RedisLimiter.TryAcquire(n)

	// Check if Redis is healthy
	ctx := context.Background()
	if err := rl.client.Ping(ctx); err != nil {
		// Redis is down, use failure mode
		return rl.failureMode == FailOpen
	}

	return result
}

// TokenBucket implements token bucket rate limiting algorithm.
type TokenBucket struct {
	// Current number of tokens
	tokens float64

	// Maximum token capacity
	capacity float64

	// Token refill rate per second
	refillRate float64

	// Last refill timestamp
	lastRefill time.Time

	// Synchronization
	mu sync.Mutex
}

// NewTokenBucket creates a new token bucket rate limiter.
func NewTokenBucket(capacity float64, refillRate float64) *TokenBucket {
	return &TokenBucket{
		tokens:     capacity,
		capacity:   capacity,
		refillRate: refillRate,
		lastRefill: time.Now(),
	}
}

// TryAcquire attempts to acquire n tokens from the bucket.
// Returns true if successful, false if insufficient tokens.
func (tb *TokenBucket) TryAcquire(n int) bool {
	tb.mu.Lock()
	defer tb.mu.Unlock()

	// Refill tokens based on elapsed time
	now := time.Now()
	elapsed := now.Sub(tb.lastRefill).Seconds()
	tb.lastRefill = now

	// Add tokens based on refill rate
	tokensToAdd := elapsed * tb.refillRate
	tb.tokens = tb.tokens + tokensToAdd

	// Cap at maximum capacity
	if tb.tokens > tb.capacity {
		tb.tokens = tb.capacity
	}

	// Check if we have enough tokens
	if tb.tokens >= float64(n) {
		tb.tokens -= float64(n)
		return true
	}

	return false
}

// LastAccess returns the last time the bucket was accessed.
func (tb *TokenBucket) LastAccess() time.Time {
	tb.mu.Lock()
	defer tb.mu.Unlock()
	return tb.lastRefill
}

// SlidingWindow implements sliding window rate limiting algorithm.
type SlidingWindow struct {
	// Ring buffer of request timestamps
	timestamps []time.Time

	// Current position in ring buffer
	position int

	// Window duration
	windowSize time.Duration

	// Maximum requests in window
	limit int

	// Last access time
	lastAccess time.Time

	// Synchronization
	mu sync.Mutex
}

// NewSlidingWindow creates a new sliding window rate limiter.
func NewSlidingWindow(limit int, windowSize time.Duration) *SlidingWindow {
	return &SlidingWindow{
		timestamps: make([]time.Time, 0, limit*2),
		windowSize: windowSize,
		limit:      limit,
		lastAccess: time.Now(),
	}
}

// TryAcquire attempts to acquire n permits from the sliding window.
// Returns true if successful, false if limit exceeded.
func (sw *SlidingWindow) TryAcquire(n int) bool {
	sw.mu.Lock()
	defer sw.mu.Unlock()

	now := time.Now()
	sw.lastAccess = now
	windowStart := now.Add(-sw.windowSize)

	// Remove expired entries
	validTimestamps := make([]time.Time, 0, len(sw.timestamps))
	for _, ts := range sw.timestamps {
		if ts.After(windowStart) {
			validTimestamps = append(validTimestamps, ts)
		}
	}
	sw.timestamps = validTimestamps

	// Check if adding n requests would exceed limit
	if len(sw.timestamps)+n > sw.limit {
		return false
	}

	// Add new timestamps
	for i := 0; i < n; i++ {
		sw.timestamps = append(sw.timestamps, now)
	}

	return true
}

// LastAccess returns the last time the window was accessed.
func (sw *SlidingWindow) LastAccess() time.Time {
	sw.mu.Lock()
	defer sw.mu.Unlock()
	return sw.lastAccess
}

// FixedWindow implements fixed window rate limiting algorithm.
type FixedWindow struct {
	// Current request count in window
	count int

	// Window start time
	windowStart time.Time

	// Maximum requests per window
	limit int

	// Window duration
	windowSize time.Duration

	// Last access time
	lastAccess time.Time

	// Synchronization
	mu sync.Mutex
}

// NewFixedWindow creates a new fixed window rate limiter.
func NewFixedWindow(limit int, windowSize time.Duration) *FixedWindow {
	now := time.Now()
	return &FixedWindow{
		count:       0,
		windowStart: now,
		limit:       limit,
		windowSize:  windowSize,
		lastAccess:  now,
	}
}

// TryAcquire attempts to acquire n permits from the fixed window.
// Returns true if successful, false if limit exceeded.
func (fw *FixedWindow) TryAcquire(n int) bool {
	fw.mu.Lock()
	defer fw.mu.Unlock()

	now := time.Now()
	fw.lastAccess = now

	// Reset count if window has expired
	if now.Sub(fw.windowStart) >= fw.windowSize {
		fw.windowStart = now
		fw.count = 0
	}

	// Check if adding n requests would exceed limit
	if fw.count+n > fw.limit {
		return false
	}

	// Increment counter
	fw.count += n
	return true
}

// LastAccess returns the last time the window was accessed.
func (fw *FixedWindow) LastAccess() time.Time {
	fw.mu.Lock()
	defer fw.mu.Unlock()
	return fw.lastAccess
}

// RateLimitStatistics tracks rate limiting metrics.
type RateLimitStatistics struct {
	TotalRequests   uint64
	AllowedRequests uint64
	DeniedRequests  uint64
	ActiveLimiters  int
	ByKeyStats      map[string]*KeyStatistics
	AllowRate       float64 // Percentage of allowed requests
	DenyRate        float64 // Percentage of denied requests
}

// KeyStatistics tracks per-key rate limit metrics.
type KeyStatistics struct {
	Allowed  uint64
	Denied   uint64
	LastSeen time.Time
}

// RateLimitConfig configures the rate limiting behavior.
// Supports multiple algorithms for different use cases.
type RateLimitConfig struct {
	// Algorithm specifies the rate limiting algorithm to use.
	// Options: "token-bucket", "sliding-window", "fixed-window"
	Algorithm string

	// RequestsPerSecond defines the sustained request rate.
	RequestsPerSecond int

	// BurstSize defines the maximum burst capacity.
	// Only used with token-bucket algorithm.
	BurstSize int

	// KeyExtractor extracts the rate limit key from context.
	// If nil, a global rate limit is applied.
	KeyExtractor func(context.Context) string

	// WindowSize defines the time window for rate limiting.
	// Used with sliding-window and fixed-window algorithms.
	WindowSize time.Duration

	// WebhookURL to call when rate limit is exceeded (optional).
	WebhookURL string
}

// RateLimitFilter implements rate limiting with multiple algorithms.
type RateLimitFilter struct {
	*FilterBase

	// Rate limiters per key
	limiters sync.Map // map[string]RateLimiter

	// Configuration
	config RateLimitConfig

	// Cleanup timer
	cleanupTicker *time.Ticker

	// Statistics
	stats RateLimitStatistics

	// Synchronization
	statsMu sync.RWMutex
}

// NewRateLimitFilter creates a new rate limit filter.
func NewRateLimitFilter(config RateLimitConfig) *RateLimitFilter {
	f := &RateLimitFilter{
		FilterBase: NewFilterBase("rate-limit", "security"),
		config:     config,
		stats: RateLimitStatistics{
			ByKeyStats: make(map[string]*KeyStatistics),
		},
	}

	// Start cleanup ticker
	f.cleanupTicker = time.NewTicker(1 * time.Minute)
	go f.cleanupLoop()

	return f
}

// Process implements the Filter interface.
func (f *RateLimitFilter) Process(ctx context.Context, data []byte) (*types.FilterResult, error) {
	// Extract key using KeyExtractor
	key := "global"
	if f.config.KeyExtractor != nil {
		key = f.config.KeyExtractor(ctx)
	}

	// Get or create limiter for key
	limiterI, _ := f.limiters.LoadOrStore(key, f.createLimiter())
	limiter := limiterI.(RateLimiter)

	// Try to acquire permit
	allowed := limiter.TryAcquire(1)

	// Update statistics
	f.updateStats(key, allowed)

	// Return rate limit error if exceeded
	if !allowed {
		return f.handleRateLimitExceeded(key)
	}

	// Process normally if allowed
	return types.ContinueWith(data), nil
}

// createLimiter creates a new rate limiter based on configured algorithm.
func (f *RateLimitFilter) createLimiter() RateLimiter {
	switch f.config.Algorithm {
	case "token-bucket":
		return NewTokenBucket(
			float64(f.config.BurstSize),
			float64(f.config.RequestsPerSecond),
		)
	case "sliding-window":
		limit := int(f.config.RequestsPerSecond * int(f.config.WindowSize.Seconds()))
		return NewSlidingWindow(limit, f.config.WindowSize)
	case "fixed-window":
		limit := int(f.config.RequestsPerSecond * int(f.config.WindowSize.Seconds()))
		return NewFixedWindow(limit, f.config.WindowSize)
	default:
		// Default to token bucket
		return NewTokenBucket(
			float64(f.config.BurstSize),
			float64(f.config.RequestsPerSecond),
		)
	}
}

// updateStats updates rate limiting statistics.
func (f *RateLimitFilter) updateStats(key string, allowed bool) {
	f.statsMu.Lock()
	defer f.statsMu.Unlock()

	f.stats.TotalRequests++

	if allowed {
		f.stats.AllowedRequests++
	} else {
		f.stats.DeniedRequests++
	}

	// Update per-key stats
	keyStats, exists := f.stats.ByKeyStats[key]
	if !exists {
		keyStats = &KeyStatistics{}
		f.stats.ByKeyStats[key] = keyStats
	}

	if allowed {
		keyStats.Allowed++
	} else {
		keyStats.Denied++
	}
	keyStats.LastSeen = time.Now()
}

// handleRateLimitExceeded handles rate limit exceeded scenario.
func (f *RateLimitFilter) handleRateLimitExceeded(key string) (*types.FilterResult, error) {
	// Calculate retry-after based on algorithm
	retryAfter := f.calculateRetryAfter()

	// Create metadata with retry information
	metadata := map[string]interface{}{
		"retry-after": retryAfter.Seconds(),
		"key":         key,
		"algorithm":   f.config.Algorithm,
	}

	// Update rate limit statistics
	f.statsMu.Lock()
	f.stats.DeniedRequests++
	f.statsMu.Unlock()

	// Optionally call webhook (would be configured separately)
	if f.config.WebhookURL != "" {
		go f.callWebhook(key, metadata)
	}

	// Return error result with metadata
	result := types.ErrorResult(ErrRateLimited, types.TooManyRequests)
	result.Metadata = metadata

	return result, nil
}

// calculateRetryAfter calculates when the client should retry.
func (f *RateLimitFilter) calculateRetryAfter() time.Duration {
	switch f.config.Algorithm {
	case "fixed-window":
		// For fixed window, retry after current window expires
		return f.config.WindowSize
	case "sliding-window":
		// For sliding window, retry after 1/rate seconds
		if f.config.RequestsPerSecond > 0 {
			return time.Second / time.Duration(f.config.RequestsPerSecond)
		}
		return time.Second
	case "token-bucket":
		// For token bucket, retry after one token refills
		if f.config.RequestsPerSecond > 0 {
			return time.Second / time.Duration(f.config.RequestsPerSecond)
		}
		return time.Second
	default:
		return time.Second
	}
}

// callWebhook notifies external service about rate limit event.
func (f *RateLimitFilter) callWebhook(key string, metadata map[string]interface{}) {
	// This would implement webhook calling logic
	// Placeholder for now
	_ = key
	_ = metadata
}

// cleanupLoop periodically removes expired limiters to prevent memory leak.
func (f *RateLimitFilter) cleanupLoop() {
	staleThreshold := 5 * time.Minute // Remove limiters not accessed for 5 minutes

	for range f.cleanupTicker.C {
		now := time.Now()
		keysToDelete := []string{}

		// Find stale limiters
		f.limiters.Range(func(key, value interface{}) bool {
			limiter := value.(RateLimiter)
			if now.Sub(limiter.LastAccess()) > staleThreshold {
				keysToDelete = append(keysToDelete, key.(string))
			}
			return true
		})

		// Remove stale limiters
		for _, key := range keysToDelete {
			f.limiters.Delete(key)

			// Remove from statistics
			f.statsMu.Lock()
			delete(f.stats.ByKeyStats, key)
			f.statsMu.Unlock()
		}

		// Update active limiter count
		activeCount := 0
		f.limiters.Range(func(_, _ interface{}) bool {
			activeCount++
			return true
		})

		f.statsMu.Lock()
		f.stats.ActiveLimiters = activeCount
		f.statsMu.Unlock()
	}
}

// Close stops the cleanup timer and releases resources.
func (f *RateLimitFilter) Close() error {
	if f.cleanupTicker != nil {
		f.cleanupTicker.Stop()
	}

	// Clear all limiters
	f.limiters.Range(func(key, _ interface{}) bool {
		f.limiters.Delete(key)
		return true
	})

	// Call parent Close
	if f.FilterBase != nil {
		return f.FilterBase.Close()
	}

	return nil
}

// GetStatistics returns current rate limiting statistics.
func (f *RateLimitFilter) GetStatistics() RateLimitStatistics {
	f.statsMu.RLock()
	defer f.statsMu.RUnlock()

	// Create a copy of statistics
	statsCopy := RateLimitStatistics{
		TotalRequests:   f.stats.TotalRequests,
		AllowedRequests: f.stats.AllowedRequests,
		DeniedRequests:  f.stats.DeniedRequests,
		ActiveLimiters:  f.stats.ActiveLimiters,
		ByKeyStats:      make(map[string]*KeyStatistics),
	}

	// Copy per-key statistics
	for key, keyStats := range f.stats.ByKeyStats {
		statsCopy.ByKeyStats[key] = &KeyStatistics{
			Allowed:  keyStats.Allowed,
			Denied:   keyStats.Denied,
			LastSeen: keyStats.LastSeen,
		}
	}

	// Calculate rates and percentages
	if statsCopy.TotalRequests > 0 {
		statsCopy.AllowRate = float64(statsCopy.AllowedRequests) / float64(statsCopy.TotalRequests) * 100.0
		statsCopy.DenyRate = float64(statsCopy.DeniedRequests) / float64(statsCopy.TotalRequests) * 100.0
	}

	return statsCopy
}
