package graph

import (
	"context"
	"sync"
	"time"
)

// RateLimiter implements a token bucket rate limiter with adaptive throttling
// based on graph query complexity. It allows cooperative scheduling by inserting
// pauses between operations to allow other work to proceed.
type RateLimiter struct {
	mu sync.Mutex

	// Token bucket parameters
	tokens        float64   // Current available tokens
	maxTokens     float64   // Maximum token capacity
	refillRate    float64   // Tokens per second to add
	lastRefill    time.Time // Last time tokens were refilled

	// Throttling parameters
	baseDelay     time.Duration // Minimum delay between operations
	maxDelay      time.Duration // Maximum delay for complex queries
	depthFactor   float64       // Multiplier per depth level
	limitFactor   float64       // Multiplier based on result limit
}

// RateLimiterConfig configures the rate limiter behavior.
type RateLimiterConfig struct {
	// MaxTokens is the maximum number of tokens in the bucket (default: 100)
	MaxTokens float64

	// RefillRate is tokens added per second (default: 10)
	RefillRate float64

	// BaseDelay is the minimum delay between operations (default: 1ms)
	BaseDelay time.Duration

	// MaxDelay is the maximum delay for complex queries (default: 100ms)
	MaxDelay time.Duration

	// DepthFactor is the cost multiplier per depth level (default: 2.0)
	// A depth-3 query costs 2^3 = 8x more tokens than depth-1
	DepthFactor float64

	// LimitFactor is additional cost per 100 results requested (default: 0.1)
	LimitFactor float64
}

// DefaultRateLimiterConfig returns sensible defaults for the rate limiter.
func DefaultRateLimiterConfig() RateLimiterConfig {
	return RateLimiterConfig{
		MaxTokens:   100.0,
		RefillRate:  10.0, // Refills fully in 10 seconds
		BaseDelay:   1 * time.Millisecond,
		MaxDelay:    100 * time.Millisecond,
		DepthFactor: 2.0,
		LimitFactor: 0.1,
	}
}

// NewRateLimiter creates a new rate limiter with the given configuration.
func NewRateLimiter(cfg RateLimiterConfig) *RateLimiter {
	if cfg.MaxTokens <= 0 {
		cfg.MaxTokens = DefaultRateLimiterConfig().MaxTokens
	}
	if cfg.RefillRate <= 0 {
		cfg.RefillRate = DefaultRateLimiterConfig().RefillRate
	}
	if cfg.BaseDelay <= 0 {
		cfg.BaseDelay = DefaultRateLimiterConfig().BaseDelay
	}
	if cfg.MaxDelay <= 0 {
		cfg.MaxDelay = DefaultRateLimiterConfig().MaxDelay
	}
	if cfg.DepthFactor <= 0 {
		cfg.DepthFactor = DefaultRateLimiterConfig().DepthFactor
	}
	if cfg.LimitFactor <= 0 {
		cfg.LimitFactor = DefaultRateLimiterConfig().LimitFactor
	}

	return &RateLimiter{
		tokens:      cfg.MaxTokens,
		maxTokens:   cfg.MaxTokens,
		refillRate:  cfg.RefillRate,
		lastRefill:  time.Now(),
		baseDelay:   cfg.BaseDelay,
		maxDelay:    cfg.MaxDelay,
		depthFactor: cfg.DepthFactor,
		limitFactor: cfg.LimitFactor,
	}
}

// QueryCost calculates the token cost for a graph query based on its complexity.
// Higher depths and larger limits cost exponentially more tokens.
func (rl *RateLimiter) QueryCost(q *Query) float64 {
	if q == nil {
		return 1.0
	}

	// Base cost is exponential in depth: depthFactor^depth
	// This models the exponential growth of traversal work
	cost := 1.0
	for i := 0; i < q.Depth; i++ {
		cost *= rl.depthFactor
	}

	// Add cost for reference collection (adds ~50% per ref spec)
	refCost := float64(len(q.InboundRefs)+len(q.OutboundRefs)) * 0.5
	cost += refCost

	return cost
}

// OperationCost calculates the token cost for a single traversal operation.
// This is used during query execution for per-operation throttling.
func (rl *RateLimiter) OperationCost(depth int, nodesAtDepth int) float64 {
	// Cost increases with depth and number of nodes to process
	depthMultiplier := 1.0
	for i := 0; i < depth; i++ {
		depthMultiplier *= rl.depthFactor
	}

	// More nodes at this depth = more work
	nodeFactor := 1.0 + float64(nodesAtDepth)*0.01

	return depthMultiplier * nodeFactor
}

// refillTokens adds tokens based on elapsed time since last refill.
func (rl *RateLimiter) refillTokens() {
	now := time.Now()
	elapsed := now.Sub(rl.lastRefill).Seconds()
	rl.lastRefill = now

	rl.tokens += elapsed * rl.refillRate
	if rl.tokens > rl.maxTokens {
		rl.tokens = rl.maxTokens
	}
}

// Acquire tries to acquire tokens for a query. If not enough tokens are available,
// it waits until they become available or the context is cancelled.
// Returns the delay that was applied, or an error if context was cancelled.
func (rl *RateLimiter) Acquire(ctx context.Context, cost float64) (time.Duration, error) {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	rl.refillTokens()

	var totalDelay time.Duration

	// Wait until we have enough tokens
	for rl.tokens < cost {
		// Calculate how long we need to wait for tokens to refill
		tokensNeeded := cost - rl.tokens
		waitTime := time.Duration(tokensNeeded/rl.refillRate*1000) * time.Millisecond

		// Clamp to max delay
		if waitTime > rl.maxDelay {
			waitTime = rl.maxDelay
		}
		if waitTime < rl.baseDelay {
			waitTime = rl.baseDelay
		}

		// Release lock while waiting
		rl.mu.Unlock()

		select {
		case <-ctx.Done():
			rl.mu.Lock()
			return totalDelay, ctx.Err()
		case <-time.After(waitTime):
		}

		totalDelay += waitTime
		rl.mu.Lock()
		rl.refillTokens()
	}

	// Consume tokens
	rl.tokens -= cost
	return totalDelay, nil
}

// TryAcquire attempts to acquire tokens without waiting.
// Returns true if successful, false if insufficient tokens.
func (rl *RateLimiter) TryAcquire(cost float64) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	rl.refillTokens()

	if rl.tokens >= cost {
		rl.tokens -= cost
		return true
	}
	return false
}

// Pause inserts a cooperative delay to allow other work to proceed.
// The delay is proportional to the current depth and load.
// This should be called periodically during long-running traversals.
func (rl *RateLimiter) Pause(ctx context.Context, depth int, itemsProcessed int) error {
	// Calculate adaptive delay based on depth and progress
	// Deeper traversals and more processed items = longer pauses
	delay := rl.baseDelay

	// Increase delay with depth
	for i := 0; i < depth; i++ {
		delay += rl.baseDelay
	}

	// Add extra delay every N items to allow other work
	if itemsProcessed > 0 && itemsProcessed%100 == 0 {
		delay += rl.baseDelay * 5
	}

	// Cap at max delay
	if delay > rl.maxDelay {
		delay = rl.maxDelay
	}

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-time.After(delay):
		return nil
	}
}

// AvailableTokens returns the current number of available tokens.
func (rl *RateLimiter) AvailableTokens() float64 {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	rl.refillTokens()
	return rl.tokens
}

// Throttler provides a simple interface for cooperative scheduling during traversal.
// It wraps the rate limiter and provides depth-aware throttling.
type Throttler struct {
	rl            *RateLimiter
	depth         int
	itemsProcessed int
}

// NewThrottler creates a throttler for a specific traversal operation.
func NewThrottler(rl *RateLimiter, depth int) *Throttler {
	return &Throttler{
		rl:    rl,
		depth: depth,
	}
}

// Tick should be called after processing each item.
// It tracks progress and inserts pauses as needed.
func (t *Throttler) Tick(ctx context.Context) error {
	t.itemsProcessed++

	// Insert cooperative pause periodically
	// More frequent pauses at higher depths
	interval := 50
	if t.depth >= 2 {
		interval = 25
	}
	if t.depth >= 4 {
		interval = 10
	}

	if t.itemsProcessed%interval == 0 {
		return t.rl.Pause(ctx, t.depth, t.itemsProcessed)
	}
	return nil
}

// Complete marks the throttler as complete and returns stats.
func (t *Throttler) Complete() (itemsProcessed int) {
	return t.itemsProcessed
}
