<?php
/**
 * Rate Limiter for BoostBot.
 *
 * @package BoostBot
 */

// Prevent direct access to the file.
if (!defined('ABSPATH')) {
    exit;
}

/**
 * BoostBot_Rate_Limiter class.
 *
 * Provides rate limiting functionality to prevent abuse.
 *
 * @since 1.0.0
 */
class BoostBot_Rate_Limiter
{
    /**
     * Rate limit configurations by type.
     *
     * @since 1.0.0
     * @access private static
     * @var array $limits
     */
    private static $limits = [
        'chat' => [
            'requests' => 120,   // Max requests (each message = ~4 API calls, so ~30 messages/10min)
            'window'   => 600,   // 10 minutes
        ],
        'api'  => [
            'requests' => 100,   // Max requests
            'window'   => 3600,  // 1 hour
        ],
        'commerce' => [
            'requests' => 45,    // Max transactional requests
            'window'   => 600,   // 10 minutes
        ],
        'lead' => [
            'requests' => 10,    // Max lead submissions
            'window'   => 300,   // 5 minutes
        ],
        'export' => [
            'requests' => 3,     // Max exports
            'window'   => 3600,  // 1 hour
        ],
    ];

    /**
     * Check if an identifier is rate limited.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier (IP hash, user ID, etc.).
     * @param string $type       The type of rate limit to check.
     * @return bool True if rate limited, false if allowed.
     */
    public static function is_limited($identifier, $type = 'chat')
    {
        // Whitelisted identifiers are exempt (the whitelist API was previously never
        // consulted here, so exemptions silently did nothing).
        if (self::is_whitelisted($identifier)) {
            return false;
        }

        // Get limit config
        $config = self::get_limit_config($type);
        if (!$config) {
            return false; // Unknown type, allow
        }

        $key = self::get_cache_key($identifier, $type);

        // When a persistent object cache is available, use wp_cache_incr for an
        // atomic read-modify-write of the counter. This avoids the lost-update
        // race inherent in the get_transient/set_transient path below.
        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
            return self::is_limited_atomic($key, $config, $identifier, $type);
        }

        $data = get_transient($key);

        // First request - not limited
        if ($data === false) {
            self::init_counter($key, $config['window']);
            return false;
        }

        // Check if window expired
        if (time() - $data['start'] >= $config['window']) {
            // Reset counter
            self::init_counter($key, $config['window']);
            return false;
        }

        // Check if over limit
        if ($data['count'] >= $config['requests']) {
            BoostBot_Logger::warning('Rate limit exceeded', [
                'identifier' => substr($identifier, 0, 8) . '...',
                'type'       => $type,
                'count'      => $data['count'],
                'limit'      => $config['requests'],
            ]);
            return true;
        }

        // Increment counter.
        // NOTE (residual race): without a persistent object cache this
        // get_transient()+set_transient() sequence is non-atomic, so concurrent
        // requests can lose an increment. Acceptable trade-off — the fix above
        // uses atomic wp_cache_incr whenever an external object cache exists.
        $data['count']++;
        set_transient($key, $data, $config['window']);

        return false;
    }

    /**
     * Atomic rate-limit check backed by a persistent object cache.
     *
     * Uses wp_cache_incr so the counter increment is atomic across concurrent
     * requests. The window is enforced by the cache entry's TTL.
     *
     * @since 1.0.0
     * @param string $key        Cache key.
     * @param array  $config     Limit config with 'requests' and 'window'.
     * @param string $identifier Unique identifier (for logging).
     * @param string $type       Rate limit type (for logging).
     * @return bool True if rate limited, false if allowed.
     */
    private static function is_limited_atomic($key, $config, $identifier, $type)
    {
        $group    = 'boostbot_rl';
        $current  = wp_cache_get($key, $group);

        // First request in this window - seed the counter with its TTL.
        if (false === $current) {
            wp_cache_set($key, 1, $group, $config['window']);
            return false;
        }

        // Already at/over the limit for this window.
        if ((int) $current >= $config['requests']) {
            BoostBot_Logger::warning('Rate limit exceeded', [
                'identifier' => substr($identifier, 0, 8) . '...',
                'type'       => $type,
                'count'      => (int) $current,
                'limit'      => $config['requests'],
            ]);
            return true;
        }

        // Atomic increment. If the entry expired between the get and the incr,
        // wp_cache_incr returns false; re-seed the counter in that case.
        $new = wp_cache_incr($key, 1, $group);
        if (false === $new) {
            wp_cache_set($key, 1, $group, $config['window']);
        }

        return false;
    }

    /**
     * Get remaining requests for an identifier.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @param string $type       Rate limit type.
     * @return int Remaining requests.
     */
    public static function get_remaining($identifier, $type = 'chat')
    {
        $config = self::get_limit_config($type);
        if (!$config) {
            return PHP_INT_MAX;
        }

        $key = self::get_cache_key($identifier, $type);
        $data = get_transient($key);

        if ($data === false) {
            return $config['requests'];
        }

        // Check if window expired
        if (time() - $data['start'] >= $config['window']) {
            return $config['requests'];
        }

        return max(0, $config['requests'] - $data['count']);
    }

    /**
     * Get time until rate limit resets.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @param string $type       Rate limit type.
     * @return int Seconds until reset, 0 if not limited.
     */
    public static function get_reset_time($identifier, $type = 'chat')
    {
        $config = self::get_limit_config($type);
        if (!$config) {
            return 0;
        }

        $key = self::get_cache_key($identifier, $type);
        $data = get_transient($key);

        if ($data === false) {
            return 0;
        }

        $elapsed = time() - $data['start'];
        $remaining = $config['window'] - $elapsed;

        return max(0, $remaining);
    }

    /**
     * Reset rate limit for an identifier.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @param string $type       Rate limit type.
     */
    public static function reset($identifier, $type = 'chat')
    {
        $key = self::get_cache_key($identifier, $type);
        delete_transient($key);

        BoostBot_Logger::debug('Rate limit reset', [
            'identifier' => substr($identifier, 0, 8) . '...',
            'type'       => $type,
        ]);
    }

    /**
     * Reset all rate limits for an identifier.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     */
    public static function reset_all($identifier)
    {
        foreach (array_keys(self::$limits) as $type) {
            self::reset($identifier, $type);
        }
    }

    /**
     * Get rate limit info for headers.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @param string $type       Rate limit type.
     * @return array Header info.
     */
    public static function get_headers_info($identifier, $type = 'chat')
    {
        $config = self::get_limit_config($type);
        if (!$config) {
            return [];
        }

        return [
            'X-RateLimit-Limit'     => $config['requests'],
            'X-RateLimit-Remaining' => self::get_remaining($identifier, $type),
            'X-RateLimit-Reset'     => time() + self::get_reset_time($identifier, $type),
        ];
    }

    /**
     * Add rate limit headers to response.
     *
     * @since 1.0.0
     * @param WP_REST_Response $response   The response object.
     * @param string           $identifier Unique identifier.
     * @param string           $type       Rate limit type.
     * @return WP_REST_Response Modified response.
     */
    public static function add_headers($response, $identifier, $type = 'chat')
    {
        $headers = self::get_headers_info($identifier, $type);

        foreach ($headers as $name => $value) {
            $response->header($name, $value);
        }

        return $response;
    }

    /**
     * Whitelist an identifier (exempt from rate limiting).
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     */
    public static function whitelist($identifier)
    {
        $whitelist = get_option('boostbot_rate_limit_whitelist', []);
        $whitelist[] = $identifier;
        $whitelist = array_unique($whitelist);
        update_option('boostbot_rate_limit_whitelist', $whitelist);
    }

    /**
     * Remove from whitelist.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     */
    public static function remove_from_whitelist($identifier)
    {
        $whitelist = get_option('boostbot_rate_limit_whitelist', []);
        $whitelist = array_diff($whitelist, [$identifier]);
        update_option('boostbot_rate_limit_whitelist', $whitelist);
    }

    /**
     * Check if identifier is whitelisted.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @return bool True if whitelisted.
     */
    public static function is_whitelisted($identifier)
    {
        $whitelist = get_option('boostbot_rate_limit_whitelist', []);
        return in_array($identifier, $whitelist, true);
    }

    /**
     * Get limit configuration for a type.
     *
     * @since 1.0.0
     * @param string $type Rate limit type.
     * @return array|null Configuration or null if not found.
     */
    private static function get_limit_config($type)
    {
        return self::$limits[$type] ?? null;
    }

    /**
     * Get cache key for transient storage.
     *
     * @since 1.0.0
     * @param string $identifier Unique identifier.
     * @param string $type       Rate limit type.
     * @return string Cache key.
     */
    private static function get_cache_key($identifier, $type)
    {
        return 'boostbot_rl_' . $type . '_' . substr(md5($identifier), 0, 12);
    }

    /**
     * Initialize rate limit counter.
     *
     * @since 1.0.0
     * @param string $key    Cache key.
     * @param int    $window Window duration in seconds.
     */
    private static function init_counter($key, $window)
    {
        $data = [
            'count' => 1,
            'start' => time(),
        ];
        set_transient($key, $data, $window);
    }

    /**
     * Get all configured rate limit types and their configs.
     *
     * @since 1.0.0
     * @return array Rate limit configurations.
     */
    public static function get_all_configs()
    {
        return self::$limits;
    }

    /**
     * Update rate limit configuration (for admin customization).
     *
     * @since 1.0.0
     * @param string $type     Rate limit type.
     * @param int    $requests Max requests.
     * @param int    $window   Window in seconds.
     */
    public static function update_config($type, $requests, $window)
    {
        if (isset(self::$limits[$type])) {
            $custom_limits = get_option('boostbot_custom_rate_limits', []);
            $custom_limits[$type] = [
                'requests' => absint($requests),
                'window'   => absint($window),
            ];
            update_option('boostbot_custom_rate_limits', $custom_limits);

            BoostBot_Logger::info('Rate limit config updated', [
                'type'     => $type,
                'requests' => $requests,
                'window'   => $window,
            ]);
        }
    }
}










