<?php
/**
 * Rate Limiter Helper using Symfony Rate Limiter.
 *
 * @package PrimeLink
 */

namespace PrimeLink\Helpers;

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

/**
 * Rate Limiter Helper class for rate limiting API requests.
 * Uses Symfony Rate Limiter if available, falls back to simple in-memory tracking.
 */
final class RateLimiterHelper {

    /**
     * Rate limiter instances cache.
     *
     * @var array
     */
    private static $limiters = array();

    /**
     * Check if Symfony Rate Limiter is available.
     *
     * @return bool
     */
    private static function has_symfony_rate_limiter() {
        // Symfony Rate Limiter 6.x requires PHP 8.1+
        if ( version_compare( PHP_VERSION, '8.1.0', '<' ) ) {
            return false;
        }
        
        return self::get_fixed_window_limiter_class() !== null;
    }

    /**
     * Get FixedWindowLimiter class.
     *
     * @return string|null
     */
    private static function get_fixed_window_limiter_class() {
        if ( class_exists( '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\FixedWindowLimiter' ) ) {
            return '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\FixedWindowLimiter';
        } elseif ( class_exists( '\Symfony\Component\RateLimiter\Policy\FixedWindowLimiter' ) ) {
            return '\Symfony\Component\RateLimiter\Policy\FixedWindowLimiter';
        }
        return null;
    }

    /**
     * Get SlidingWindowLimiter class.
     *
     * @return string|null
     */
    private static function get_sliding_window_limiter_class() {
        if ( class_exists( '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter' ) ) {
            return '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter';
        } elseif ( class_exists( '\Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter' ) ) {
            return '\Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter';
        }
        return null;
    }

    /**
     * Get TokenBucketLimiter class.
     *
     * @return string|null
     */
    private static function get_token_bucket_limiter_class() {
        if ( class_exists( '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\TokenBucketLimiter' ) ) {
            return '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\TokenBucketLimiter';
        } elseif ( class_exists( '\Symfony\Component\RateLimiter\Policy\TokenBucketLimiter' ) ) {
            return '\Symfony\Component\RateLimiter\Policy\TokenBucketLimiter';
        }
        return null;
    }

    /**
     * Get Rate class for TokenBucketLimiter.
     *
     * @return string|null
     */
    private static function get_rate_class() {
        if ( class_exists( '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\Rate' ) ) {
            return '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Policy\Rate';
        } elseif ( class_exists( '\Symfony\Component\RateLimiter\Policy\Rate' ) ) {
            return '\Symfony\Component\RateLimiter\Policy\Rate';
        }
        return null;
    }

    /**
     * Get or create a rate limiter instance.
     *
     * @param string $key     Rate limiter key (e.g., 'api_user_123', 'api_ip_192.168.1.1').
     * @param array  $options Rate limiter options:
     *                       - 'limit' (int): Maximum number of tokens (default: 100)
     *                       - 'interval' (int): Time interval in seconds (default: 60)
     *                       - 'policy' (string): 'fixed_window', 'sliding_window', or 'token_bucket' (default: 'fixed_window')
     * @return object|null Rate limiter instance or null.
     */
    private static function get_rate_limiter( $key, $options = array() ) {
        $defaults = array(
            'limit'    => 100,      // 100 requests
            'interval' => 60,       // per 60 seconds
            'policy'   => 'fixed_window',
        );
        $options = wp_parse_args( $options, $defaults );

        // Create cache key for this limiter configuration
        $cache_key = 'rate_limiter_' . md5( $key . serialize( $options ) );

        // Return cached instance if available
        if ( isset( self::$limiters[ $cache_key ] ) ) {
            return self::$limiters[ $cache_key ];
        }

        // Try Symfony Rate Limiter first
        if ( self::has_symfony_rate_limiter() ) {
            $limiter = self::create_symfony_rate_limiter( $key, $options );
            if ( $limiter ) {
                self::$limiters[ $cache_key ] = $limiter;
                return $limiter;
            }
        }

        // Fallback to simple in-memory rate limiter
        $limiter = self::create_simple_rate_limiter( $key, $options );
        self::$limiters[ $cache_key ] = $limiter;
        return $limiter;
    }

    /**
     * Create Symfony Rate Limiter instance.
     *
     * @param string $key     Rate limiter key.
     * @param array  $options Options.
     * @return object|null Rate limiter instance or null.
     */
    private static function create_symfony_rate_limiter( $key, $options ) {
        try {
            // Get storage (use CacheStorage with our CacheHelper)
            $storage = self::get_symfony_storage();
            if ( ! $storage ) {
                return null;
            }

            // Create DateInterval for the time window
            $interval = \DateInterval::createFromDateString( $options['interval'] . ' seconds' );
            if ( ! $interval ) {
                $interval = new \DateInterval( 'PT' . $options['interval'] . 'S' );
            }

            // Create limiter based on policy
            switch ( $options['policy'] ) {
                case 'sliding_window':
                    $limiter_class = self::get_sliding_window_limiter_class();
                    if ( $limiter_class ) {
                        // SlidingWindowLimiter: id, limit, interval, storage, lock (optional)
                        return new $limiter_class( $key, $options['limit'], $interval, $storage, null );
                    }
                    // Fallback to fixed window
                    break;

                case 'token_bucket':
                    $limiter_class = self::get_token_bucket_limiter_class();
                    $rate_class = self::get_rate_class();
                    if ( $limiter_class && $rate_class ) {
                        // TokenBucketLimiter: id, maxBurst, Rate, storage, lock (optional)
                        // Rate: interval (DateInterval), amount (int)
                        $rate = new $rate_class( $interval, 1 ); // 1 token per interval
                        return new $limiter_class( $key, $options['limit'], $rate, $storage, null );
                    }
                    // Fallback to fixed window
                    break;

                case 'fixed_window':
                default:
                    $limiter_class = self::get_fixed_window_limiter_class();
                    if ( $limiter_class ) {
                        // FixedWindowLimiter: id, limit, interval, storage, lock (optional)
                        return new $limiter_class( $key, $options['limit'], $interval, $storage, null );
                    }
                    break;
            }
        } catch ( \Exception $e ) {
            LoggerHelper::warning( 'Failed to create Symfony Rate Limiter, using fallback', array(
                'key'     => $key,
                'error'   => $e->getMessage(),
            ) );
            return null;
        }

        return null;
    }

    /**
     * Get Symfony Storage instance (using CacheHelper).
     *
     * @return object|null Storage instance or null.
     */
    private static function get_symfony_storage() {
        // Try to find CacheStorage class (prefixed or normal)
        $cache_storage_class = null;
        if ( class_exists( '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Storage\CacheStorage' ) ) {
            $cache_storage_class = '\PrimeLink\Vendor\Symfony\Component\RateLimiter\Storage\CacheStorage';
        } elseif ( class_exists( '\Symfony\Component\RateLimiter\Storage\CacheStorage' ) ) {
            $cache_storage_class = '\Symfony\Component\RateLimiter\Storage\CacheStorage';
        }

        if ( ! $cache_storage_class ) {
            return null;
        }

        // Use CacheHelper's cache adapter as storage
        if ( CacheHelper::has_symfony_cache() ) {
            try {
                // Get cache adapter using reflection (CacheHelper doesn't expose it publicly)
                $reflection = new \ReflectionClass( 'PrimeLink\Helpers\CacheHelper' );
                $method = $reflection->getMethod( 'get_cache_adapter' );
                $method->setAccessible( true );
                $cache_adapter = $method->invoke( null );
                
                if ( $cache_adapter && $cache_adapter !== false ) {
                    return new $cache_storage_class( $cache_adapter );
                }
            } catch ( \Exception $e ) {
                LoggerHelper::warning( 'Failed to create Symfony Rate Limiter Storage', array(
                    'error' => $e->getMessage(),
                ) );
            }
        }

        return null;
    }

    /**
     * Create simple in-memory rate limiter (fallback).
     *
     * @param string $key     Rate limiter key.
     * @param array  $options Options.
     * @return object Simple rate limiter object.
     */
    private static function create_simple_rate_limiter( $key, $options ) {
        return new class( $key, $options ) {
            private $key;
            private $limit;
            private $interval;
            private $cache_key;

            public function __construct( $key, $options ) {
                $this->key      = $key;
                $this->limit    = (int) $options['limit'];
                $this->interval = (int) $options['interval'];
                $this->cache_key = 'mt_primelink_rate_limit_' . md5( $key );
            }

            public function consume( $tokens = 1 ) {
                $tokens = max( 1, (int) $tokens );
                $data = get_transient( $this->cache_key );

                if ( false === $data ) {
                    // First request in this window
                    set_transient( $this->cache_key, array(
                        'count'   => $tokens,
                        'reset_at' => time() + $this->interval,
                    ), $this->interval );
                    return true;
                }

                $data = wp_parse_args( $data, array(
                    'count'   => 0,
                    'reset_at' => time() + $this->interval,
                ) );

                // Check if window has expired
                if ( time() >= $data['reset_at'] ) {
                    // Reset window
                    set_transient( $this->cache_key, array(
                        'count'   => $tokens,
                        'reset_at' => time() + $this->interval,
                    ), $this->interval );
                    return true;
                }

                // Check if limit exceeded
                if ( $data['count'] >= $this->limit ) {
                    return false;
                }

                // Increment count
                $data['count'] += $tokens;
                $remaining_ttl = max( 1, $data['reset_at'] - time() );
                set_transient( $this->cache_key, $data, $remaining_ttl );

                return true;
            }

            public function getRemainingTokens() {
                $data = get_transient( $this->cache_key );
                if ( false === $data ) {
                    return $this->limit;
                }

                $data = wp_parse_args( $data, array(
                    'count'   => 0,
                    'reset_at' => time() + $this->interval,
                ) );

                // Check if window has expired
                if ( time() >= $data['reset_at'] ) {
                    return $this->limit;
                }

                return max( 0, $this->limit - $data['count'] );
            }

            public function getRetryAfter() {
                $data = get_transient( $this->cache_key );
                if ( false === $data ) {
                    return 0;
                }

                $data = wp_parse_args( $data, array(
                    'reset_at' => time() + $this->interval,
                ) );

                $retry_after = max( 0, $data['reset_at'] - time() );
                return $retry_after;
            }
        };
    }

    /**
     * Check if a request should be rate limited.
     *
     * @param string $identifier Identifier (user ID, IP address, etc.).
     * @param array  $options    Rate limiter options.
     * @return array{allowed: bool, remaining: int, retry_after: int, limit: int} Rate limit result.
     */
    public static function check( $identifier, $options = array() ) {
        $defaults = array(
            'limit'    => 100,
            'interval' => 60,
            'policy'   => 'fixed_window',
        );
        $options = wp_parse_args( $options, $defaults );

        $key = 'api_' . md5( $identifier );
        $limiter = self::get_rate_limiter( $key, $options );

        if ( ! $limiter ) {
            // If limiter creation failed, allow request (fail open)
            return array(
                'allowed'     => true,
                'remaining'   => $options['limit'],
                'retry_after' => 0,
                'limit'       => $options['limit'],
            );
        }

        // Try to consume a token
        // Symfony Rate Limiter's consume() returns RateLimit object, not boolean
        try {
            $rate_limit = $limiter->consume( 1 );
            
            // Check if it's a Symfony RateLimit object
            if ( is_object( $rate_limit ) && method_exists( $rate_limit, 'isAccepted' ) ) {
                $allowed = $rate_limit->isAccepted();
                $remaining = method_exists( $rate_limit, 'getRemainingTokens' ) 
                    ? $rate_limit->getRemainingTokens() 
                    : 0;
                
                // Get retry after from retryAfter DateTimeImmutable
                $retry_after = 0;
                if ( method_exists( $rate_limit, 'getRetryAfter' ) ) {
                    $retry_after_date = $rate_limit->getRetryAfter();
                    if ( $retry_after_date instanceof \DateTimeInterface ) {
                        $retry_after = max( 0, $retry_after_date->getTimestamp() - time() );
                    }
                }
            } else {
                // Fallback for simple rate limiter
                $allowed = (bool) $rate_limit;
                $remaining = method_exists( $limiter, 'getRemainingTokens' ) 
                    ? $limiter->getRemainingTokens() 
                    : 0;
                $retry_after = method_exists( $limiter, 'getRetryAfter' ) 
                    ? $limiter->getRetryAfter() 
                    : 0;
            }
        } catch ( \Exception $e ) {
            // If consume throws exception, rate limit exceeded
            LoggerHelper::warning( 'Rate limiter consume failed', array(
                'identifier' => $identifier,
                'error'      => $e->getMessage(),
            ) );
            $allowed = false;
            $remaining = 0;
            $retry_after = $options['interval'];
        }

        return array(
            'allowed'     => $allowed,
            'remaining'   => max( 0, $remaining ),
            'retry_after' => $retry_after,
            'limit'       => $options['limit'],
        );
    }

    /**
     * Get rate limit identifier from request.
     *
     * @param \WP_REST_Request $request Request object.
     * @return string Identifier (user ID or IP address).
     */
    public static function get_identifier( $request ) {
        // Try to get user ID first (for authenticated users)
        $user_id = get_current_user_id();
        if ( $user_id > 0 ) {
            return 'user_' . $user_id;
        }

        // Fallback to IP address
        $ip = self::get_client_ip();
        return 'ip_' . $ip;
    }

    /**
     * Get client IP address.
     *
     * @return string IP address.
     */
    private static function get_client_ip() {
        $ip_keys = array(
            'HTTP_CF_CONNECTING_IP', // Cloudflare
            'HTTP_X_REAL_IP',        // Nginx proxy
            'HTTP_X_FORWARDED_FOR',  // Proxy
            'REMOTE_ADDR',           // Standard
        );

        foreach ( $ip_keys as $key ) {
            if ( ! empty( $_SERVER[ $key ] ) ) {
                $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
                // Handle comma-separated IPs (X-Forwarded-For)
                if ( strpos( $ip, ',' ) !== false ) {
                    $ips = explode( ',', $ip );
                    $ip = trim( $ips[0] );
                }
                if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                    return $ip;
                }
            }
        }

        return '0.0.0.0';
    }

    /**
     * Check rate limit for REST API request.
     *
     * @param \WP_REST_Request $request Request object.
     * @param array            $options Rate limiter options.
     * @return bool|WP_Error True if allowed, WP_Error if rate limited.
     */
    public static function check_request( $request, $options = array() ) {
        $identifier = self::get_identifier( $request );
        $result = self::check( $identifier, $options );

        if ( ! $result['allowed'] ) {
            return new \WP_Error(
                'rate_limit_exceeded',
                sprintf(
                    __( 'Rate limit exceeded. Please try again in %d seconds.', 'primelink' ),
                    $result['retry_after']
                ),
                array(
                    'status'       => 429,
                    'retry_after'  => $result['retry_after'],
                    'limit'        => $result['limit'],
                    'remaining'    => 0,
                )
            );
        }

        // Add rate limit headers to response
        add_filter( 'rest_pre_serve_request', function( $served, $rest_result, $request, $server ) use ( $result ) {
            if ( ! headers_sent() ) {
                header( 'X-RateLimit-Limit: ' . $result['limit'] );
                header( 'X-RateLimit-Remaining: ' . $result['remaining'] );
                if ( $result['retry_after'] > 0 ) {
                    header( 'Retry-After: ' . $result['retry_after'] );
                }
            }
            return $served;
        }, 10, 4 );

        return true;
    }
}

