<?php

namespace Storeplugin\PDFInvoice\Framework\Utils;

defined('ABSPATH') || exit;

use Storeplugin\PDFInvoice\Config;

/**
 * Rate Limiter
 *
 * Provides rate limiting functionality to prevent abuse of AJAX endpoints
 * and other sensitive operations.
 */
class RateLimiter
{
    /**
     * Default rate limit: 60 requests per minute
     */
    private const DEFAULT_MAX_REQUESTS = 60;
    private const DEFAULT_TIME_WINDOW = 60; // seconds

    /**
     * Check if an action is rate limited
     *
     * @param string $action      The action identifier (e.g., 'ajax_update_order')
     * @param string $identifier  Unique identifier (e.g., user ID, IP address)
     * @param int    $maxRequests Maximum requests allowed in time window
     * @param int    $timeWindow  Time window in seconds
     *
     * @return bool True if rate limit exceeded, false otherwise
     */
    public static function isLimited(
        string $action,
        string $identifier,
        int $maxRequests = self::DEFAULT_MAX_REQUESTS,
        int $timeWindow = self::DEFAULT_TIME_WINDOW
    ): bool {
        $key = self::getTransientKey($action, $identifier);
        $data = \get_transient($key);

        // No data means first request - not limited
        if ($data === false) {
            self::recordRequest($action, $identifier, $timeWindow);
            return false;
        }

        self::registerKey($key);

        // Parse stored data
        $requests = isset($data['requests']) ? (int) $data['requests'] : 0;
        $firstRequest = isset($data['first_request']) ? (int) $data['first_request'] : time();

        // Check if time window has expired
        if (time() - $firstRequest >= $timeWindow) {
            // Reset the counter
            self::recordRequest($action, $identifier, $timeWindow);
            return false;
        }

        // Check if limit exceeded
        if ($requests >= $maxRequests) {
            return true;
        }

        // Increment counter
        $data['requests'] = $requests + 1;
        \set_transient($key, $data, $timeWindow);

        return false;
    }

    /**
     * Record a request for rate limiting
     *
     * @param string $action     The action identifier
     * @param string $identifier Unique identifier
     * @param int    $timeWindow Time window in seconds
     */
    private static function recordRequest(string $action, string $identifier, int $timeWindow): void
    {
        $key = self::getTransientKey($action, $identifier);
        $data = [
            'requests' => 1,
            'first_request' => time(),
        ];
        \set_transient($key, $data, $timeWindow);
        self::registerKey($key);
    }

    /**
     * Get the transient key for rate limiting
     *
     * @param string $action     The action identifier
     * @param string $identifier Unique identifier
     *
     * @return string
     */
    private static function getTransientKey(string $action, string $identifier): string
    {
        return Config::prefix() . '_rate_limit_' . \md5($action . $identifier);
    }

    /**
     * Clear rate limit for a specific action and identifier
     *
     * @param string $action     The action identifier
     * @param string $identifier Unique identifier
     */
    public static function clear(string $action, string $identifier): void
    {
        $key = self::getTransientKey($action, $identifier);
        \delete_transient($key);
        self::unregisterKey($key);
    }

    /**
     * Get remaining requests for an action
     *
     * @param string $action      The action identifier
     * @param string $identifier  Unique identifier
     * @param int    $maxRequests Maximum requests allowed
     *
     * @return int Number of remaining requests
     */
    public static function getRemainingRequests(
        string $action,
        string $identifier,
        int $maxRequests = self::DEFAULT_MAX_REQUESTS
    ): int {
        $key = self::getTransientKey($action, $identifier);
        $data = \get_transient($key);

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

        $requests = isset($data['requests']) ? (int) $data['requests'] : 0;
        return max(0, $maxRequests - $requests);
    }

    /**
     * Get time until rate limit reset
     *
     * @param string $action     The action identifier
     * @param string $identifier Unique identifier
     *
     * @return int Seconds until reset (0 if not limited)
     */
    public static function getTimeUntilReset(string $action, string $identifier): int
    {
        $key = self::getTransientKey($action, $identifier);
        $timeout = \get_option('_transient_timeout_' . $key);

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

        return max(0, (int) $timeout - time());
    }

    /**
     * Check rate limit and send error response if exceeded
     *
     * @param string $action      The action identifier
     * @param int    $maxRequests Maximum requests allowed in time window
     * @param int    $timeWindow  Time window in seconds
     *
     * @return bool True if request is allowed, false if rate limited (sends JSON response)
     */
    public static function checkAjax(
        string $action,
        int $maxRequests = self::DEFAULT_MAX_REQUESTS,
        int $timeWindow = self::DEFAULT_TIME_WINDOW
    ): bool {
        $userId = \get_current_user_id();
        $identifier = $userId > 0 ? "user_{$userId}" : Utils::getClientIP();

        if (self::isLimited($action, $identifier, $maxRequests, $timeWindow)) {
            $retryAfter = self::getTimeUntilReset($action, $identifier);

            \wp_send_json_error(
                [
                    'message' => sprintf(
                        /* translators: %d: seconds until retry */
                        __('Too many requests. Please try again in %d seconds.', 'storeinvoice-print-pdf-invoices-and-packing-slips'),
                        $retryAfter
                    ),
                    'retry_after' => $retryAfter,
                ],
                429 // HTTP 429 Too Many Requests
            );
            return false;
        }

        return true;
    }

    /**
     * Clear all rate limits (for testing/debugging)
     *
     * WARNING: Use with caution in production
     */
    public static function clearAll(): void
    {
        foreach (self::getRegisteredKeys() as $key) {
            \delete_transient($key);
        }

        \delete_option(self::getRegistryOptionKey());
    }

    /**
     * Get the option key used to track active rate-limit transients.
     *
     * @return string
     */
    private static function getRegistryOptionKey(): string
    {
        return Config::option('rate_limit_registry');
    }

    /**
     * Get all registered rate-limit transient keys.
     *
     * @return array<int, string>
     */
    private static function getRegisteredKeys(): array
    {
        $keys = \get_option(self::getRegistryOptionKey(), []);
        if (!is_array($keys)) {
            return [];
        }

        $keys = array_map(function ($key) {
            return \sanitize_key((string) $key);
        }, $keys);

        return array_values(array_filter($keys));
    }

    /**
     * Register a rate-limit transient key for bulk cleanup.
     *
     * @param string $key
     */
    private static function registerKey(string $key): void
    {
        $keys = self::getRegisteredKeys();
        if (in_array($key, $keys, true)) {
            return;
        }

        $keys[] = $key;
        \update_option(self::getRegistryOptionKey(), $keys, false);
    }

    /**
     * Remove a rate-limit transient key from the cleanup registry.
     *
     * @param string $key
     */
    private static function unregisterKey(string $key): void
    {
        $keys = self::getRegisteredKeys();
        if (empty($keys)) {
            return;
        }

        $keys = array_values(array_filter($keys, function ($registeredKey) use ($key) {
            return $registeredKey !== $key;
        }));

        if (empty($keys)) {
            \delete_option(self::getRegistryOptionKey());
            return;
        }

        \update_option(self::getRegistryOptionKey(), $keys, false);
    }
}
