<?php
/**
 * @file qtrRateLimiter.php
 * @brief Token bucket rate limiter implementation
 *
 * WHAT IT DOES (high-level)
 *  - Implements token bucket algorithm for per-lane rate limiting
 *  - Supports configurable refill rate, window, and burst capacity
 *  - Multiple "lanes" allow different endpoints to have independent limits
 *  - Automatic refill calculation based on elapsed time
 *  - Uses CQtrRateLimitCache for fast TTL-based storage
 *
 * FEATURES
 *  - Per-lane tracking: 'global', 'rest', 'xmlrpc', 'checkout', 'cart', etc.
 *  - Per-client tracking: Client key (IP + UA hash) isolates rate limits
 *  - Smooth refill: Tokens added continuously based on elapsed time
 *  - Burst support: Allows brief traffic spikes up to configured burst capacity
 *  - Zero external dependencies: Uses only WordPress transients
 *
 * ALGORITHM
 *  - Each lane + client has a "bucket" with: tokens, last_refill_time
 *  - On each consume():
 *    1. Load current bucket state (or create if missing)
 *    2. Calculate elapsed time since last refill
 *    3. Add refilled tokens: elapsed * (refill_tokens / refill_window_sec)
 *    4. Cap tokens at burst level
 *    5. Consume 1 token if available, return success
 *    6. Store updated bucket with TTL
 *  - TTL is set to 2x the refill window to avoid expiring active clients
 *
 * USAGE EXAMPLE
 *   $cfg = [
 *       'burst' => 30,
 *       'refill_tokens' => 120,
 *       'refill_window_sec' => 60,
 *   ];
 *   if (!CQtrRateLimiter::consume('rest', $clientKey, $cfg)) {
 *       // Rate limited
 *       wp_die('Too many requests');
 *   }
 *
 * @author Quttera
 */

if (!defined('ABSPATH')) exit;

require_once('qtrRateLimitCache.php');

final class CQtrRateLimiter
{
    /**
     * consume()
     * ---------
     * Attempt to consume one token from a rate limit lane.
     *
     * Implements token bucket algorithm:
     *  1. Load current bucket state (or create with burst tokens)
     *  2. Calculate elapsed time since last refill
     *  3. Add refilled tokens based on elapsed time and refill rate
     *  4. Cap tokens at burst capacity
     *  5. Consume 1 token if available
     *  6. Store updated bucket with TTL
     *
     * Token bucket state: ['tokens' => float, 'last' => int]
     *  - tokens: Current token count (0.0 to burst)
     *  - last: Timestamp of last refill (for elapsed calculation)
     *
     * IMPORTANT
     *  - Tokens are floating-point to support smooth refill rates
     *  - Checks >= 1.0 before allowing (at least one full token needed)
     *  - TTL is 2x refill window to avoid expiring active clients
     *  - O(1) complexity: single cache read/write per call
     *
     * @param string $lane      Lane identifier (e.g., 'global', 'rest', 'xmlrpc')
     * @param string $client_key Client fingerprint key (IP + UA hash)
     * @param array  $cfg       Configuration array with keys:
     *                          - 'burst': int, max tokens in bucket (e.g., 30)
     *                          - 'refill_tokens': int, tokens per refill window (e.g., 120)
     *                          - 'refill_window_sec': int, window duration (e.g., 60)
     *
     * @return bool True if token consumed (allowed), false if bucket empty (rate limited)
     */
    public static function consume(string $lane, string $client_key, array $cfg): bool
    {
        $now = time();
        $key = "tb:{$lane}:{$client_key}";

        // Load current bucket state, or create new if missing/corrupt
        $state = CQtrRateLimitCache::get($key);
        if (!is_array($state) || !isset($state['tokens'], $state['last'])) {
            $state = [
                'tokens' => (float)$cfg['burst'],
                'last'   => $now,
            ];
        }

        // Calculate elapsed time and refill rate
        $elapsed = max(0, $now - (int)$state['last']);
        $refill_rate = ((float)$cfg['refill_tokens']) / max(1, (int)$cfg['refill_window_sec']);
        
        // Add refilled tokens and cap at burst
        $state['tokens'] = min((float)$cfg['burst'], (float)$state['tokens'] + ($elapsed * $refill_rate));
        $state['last'] = $now;

        // Attempt to consume one token
        $allowed = ($state['tokens'] >= 1.0);
        if ($allowed) {
            $state['tokens'] -= 1.0;
        }

        // Store updated state with TTL (2x refill window to avoid expiry during active use)
        $ttl = max(60, ((int)$cfg['refill_window_sec']) * 2);
        CQtrRateLimitCache::set($key, $state, $ttl);

        return $allowed;
    }
}
