<?php
/**
 * Rate Limiter
 *
 * Handles API rate limiting protection with transient-based tracking.
 *
 * @package    Wisewand
 * @subpackage Utils
 * @since      1.0.3
 * @license    GPL-2.0-or-later
 */

namespace Wisewand\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Rate Limiter Class
 *
 * Provides rate limiting protection for API requests.
 *
 * @since 1.0.3
 */
class Rate_Limiter {

	/**
	 * Transient prefix for rate limit tracking
	 */
	private const TRANSIENT_PREFIX = 'wisewand_rate_limit_';

	/**
	 * Default minimum delays per API endpoint (seconds)
	 *
	 * @since 2.8.2
	 */
	private const API_LIMITS = array(
		'wisewand_api'       => 2.0,
		'wisewand_import'    => 5.0,
		'wisewand_feed_sync' => 2.0,
		'dataforseo'         => 0.2,
		'openai'             => 0.5,
		'cpu_yield'          => 0.1,
	);

	/**
	 * In-memory timestamps for smart_delay tracking
	 *
	 * @since 2.8.2
	 * @var array<string, float>
	 */
	private $call_timestamps = array();

	/**
	 * Minimum interval between requests (seconds)
	 *
	 * OPTIMIZED FOR WISEWAND API: 60 requests per minute allowed
	 *
	 * Rate limit calculation:
	 * - API limit: 60 req/min (per Wisewand documentation)
	 * - Target utilization: 70% = 42 req/min (30% safety margin)
	 * - Optimal interval: 60s ÷ 42 = 1.43s → rounded to 2.0s
	 * - Effective rate: 30 req/min base + 12 req/min retry capacity
	 *
	 * Safety margins account for:
	 * - Article retries (3x multiplier on failures)
	 * - Multiple concurrent requests (discovery + import)
	 * - Multiple browser tabs
	 * - Background CRON operations
	 *
	 * Performance: 2s = 10x faster than previous 20s (3 req/min)
	 *
	 * @since 1.0.3
	 * @since 1.0.14 Increased from 6 to 12 seconds
	 * @since 1.0.17 Increased from 12 to 20 seconds (incorrect - too conservative)
	 * @since 1.1.0 OPTIMIZED to 2 seconds based on actual 60 req/min API spec
	 */
	private const MIN_REQUEST_INTERVAL = 2.0;

	/**
	 * Maximum retry attempts
	 */
	const MAX_RETRY_ATTEMPTS = 3;

	/**
	 * Whitelisted endpoints that bypass rate limiting
	 *
	 * These endpoints are critical for user experience and should not be rate limited.
	 * SECURITY: This is a hardcoded list - no user input allowed.
	 *
	 * @since 1.0.10
	 */
	/**
	 * Endpoints that bypass rate limiting
	 *
	 * Rate limiting should ONLY apply to bulk RSS sync operations.
	 * All other API calls (UI, settings, metadata, content fetches) should be instant.
	 *
	 * NOTE: Prefix matching is used, so '/v1/articles/' matches:
	 * - /v1/articles/
	 * - /v1/articles/123
	 * - /v1/articles/123/output
	 * - /v1/articles/123/output?query=param
	 *
	 * This means content fetch operations (/output endpoints) are automatically
	 * whitelisted and will NOT be rate limited.
	 *
	 * @since 1.0.10
	 * @since 1.1.0 Expanded to include ALL non-RSS endpoints for better UX
	 * @since 1.2.1 Fixed prefix matching - /output endpoints now properly whitelisted
	 */
	private const WHITELISTED_ENDPOINTS = array(
		// Feed management (UI operations)
		'wisewand_list_feeds',
		'wisewand_feed_refresh',
		'wisewand_test_connection',
		'/v1/feeds/',

		// Settings page (UI operations)
		'wisewand_personas',
		'/v1/personas/',
		'wisewand_projects',
		'/v1/projects/',

		// Content metadata (quick lookups)
		'/v1/articles/',          // Article list (metadata only)
		'/v1/discoverarticles/',  // Discovery list (metadata only, not content fetch)

		// Note: Content fetch operations (/output endpoints) are now WHITELISTED
		// via prefix matching on '/v1/articles/' - they should be instant, not rate limited
	);

	/**
	 * Logger instance
	 *
	 * @var \Wisewand\Logging\Logger
	 */
	private $logger;

	/**
	 * Constructor
	 *
	 * @param \Wisewand\Logging\Logger $logger Logger instance.
	 */
	public function __construct( $logger ) {
		$this->logger = $logger;
	}

	/**
	 * Check if endpoint is whitelisted
	 *
	 * Uses prefix matching to support dynamic endpoints (e.g., /v1/articles/123/output).
	 * Sanitization is NOT applied because it would destroy URL structure.
	 *
	 * Security: Whitelist entries are hardcoded constants, never user input.
	 *
	 * @since 1.0.10
	 * @since 1.2.1 Fixed to use prefix matching instead of sanitize_key() exact match
	 * @param string $endpoint API endpoint identifier (e.g., '/v1/articles/123/output')
	 * @return bool True if whitelisted (bypass rate limit).
	 */
	private function is_whitelisted( $endpoint ) {
		// Normalize endpoint for comparison (lowercase, trim).
		$normalized_endpoint = strtolower( trim( $endpoint ) );

		// Check each whitelist entry.
		foreach ( self::WHITELISTED_ENDPOINTS as $whitelisted_pattern ) {
			$normalized_pattern = strtolower( trim( $whitelisted_pattern ) );

			// Check if endpoint starts with whitelisted pattern (prefix match).
			if ( strpos( $normalized_endpoint, $normalized_pattern ) === 0 ) {
				if ( $this->logger ) {
					$this->logger->debug(
						'Endpoint whitelisted (bypassing rate limit)',
						array(
							'endpoint'        => $endpoint,
							'matched_pattern' => $whitelisted_pattern,
						)
					);
				}
				return true;
			}
		}

		// Not whitelisted - apply rate limiting.
		if ( $this->logger && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			$this->logger->debug(
				'Endpoint NOT whitelisted (rate limiting applied)',
				array( 'endpoint' => $endpoint )
			);
		}

		return false;
	}

	/**
	 * Check if request is allowed (not rate limited)
	 *
	 * @since 1.0.3
	 * @param string $endpoint API endpoint identifier.
	 * @return bool|array False if allowed, array with wait_time if rate limited.
	 */
	public function check_rate_limit( $endpoint = 'global' ) {
		// Check whitelist first - whitelisted endpoints bypass rate limiting.
		if ( $this->is_whitelisted( $endpoint ) ) {
			return false; // Allow request - no rate limit.
		}

		$transient_key = self::TRANSIENT_PREFIX . sanitize_key( $endpoint );
		$last_request  = get_transient( $transient_key );

		if ( false !== $last_request ) {
			$elapsed = time() - $last_request;

			if ( $elapsed < self::MIN_REQUEST_INTERVAL ) {
				$wait_time = ceil( self::MIN_REQUEST_INTERVAL - $elapsed );
				return array(
					'rate_limited' => true,
					'wait_time'    => $wait_time,
					'message'      => sprintf(
						/* translators: %d: seconds to wait */
						__( 'Please wait %d seconds before retrying', 'wisewand-ai-content-writer-seo-content-generator' ),
						$wait_time
					),
				);
			}
		}

		return false;
	}

	/**
	 * Record API request timestamp
	 *
	 * @since 1.0.3
	 * @param string $endpoint API endpoint identifier.
	 */
	public function record_request( $endpoint = 'global' ) {
		$transient_key = self::TRANSIENT_PREFIX . sanitize_key( $endpoint );
		set_transient( $transient_key, time(), 60 ); // 60 seconds TTL.
	}

	/**
	 * Calculate exponential backoff delay
	 *
	 * @since 1.0.3
	 * @param int      $attempt_number Current retry attempt (1-based).
	 * @param int|null $retry_after Optional Retry-After header value.
	 * @return int Seconds to wait.
	 */
	public function calculate_backoff( $attempt_number, $retry_after = null ) {
		if ( null !== $retry_after ) {
			return (int) $retry_after;
		}

		// Exponential backoff: 2^attempt seconds.
		// Attempt 1: 2s, Attempt 2: 4s, Attempt 3: 8s.
		return min( pow( 2, $attempt_number ), 30 ); // Max 30 seconds.
	}

	/**
	 * Sleep with rate limit delay
	 *
	 * @since 1.0.3
	 * @param int $seconds Seconds to wait.
	 */
	public function wait( $seconds ) {
		if ( $seconds > 0 ) {
			if ( $this->logger ) {
				$this->logger->info(
					'Rate limit wait',
					array( 'wait_seconds' => $seconds )
				);
			}
			sleep( $seconds );
		}
	}

	/**
	 * Clear rate limit for an endpoint
	 *
	 * @since 1.0.3
	 * @param string $endpoint API endpoint identifier.
	 */
	public function clear_limit( $endpoint = 'global' ) {
		$transient_key = self::TRANSIENT_PREFIX . sanitize_key( $endpoint );
		delete_transient( $transient_key );
	}

	/**
	 * Get remaining time until endpoint is available
	 *
	 * @since 1.0.3
	 * @param string $endpoint API endpoint identifier.
	 * @return int Seconds remaining, 0 if available.
	 */
	public function get_remaining_wait_time( $endpoint = 'global' ) {
		$check = $this->check_rate_limit( $endpoint );

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

		return $check['wait_time'];
	}

	/**
	 * Smart delay between API calls
	 *
	 * Tracks per-endpoint call timestamps in memory and sleeps only the
	 * remaining delta when calls are too close together. Replaces scattered
	 * sleep()/usleep() calls throughout the codebase.
	 *
	 * @since 2.8.2
	 * @param string     $endpoint    Endpoint identifier (e.g. 'wisewand_api', 'dataforseo').
	 * @param float|null $min_seconds Minimum seconds between calls. Null uses API_LIMITS default.
	 */
	public function smart_delay( $endpoint, $min_seconds = null ) {
		if ( null === $min_seconds ) {
			$min_seconds = self::API_LIMITS[ $endpoint ] ?? 1.0;
		}

		$now = microtime( true );

		if ( ! isset( $this->call_timestamps[ $endpoint ] ) ) {
			$this->call_timestamps[ $endpoint ] = $now;
			return;
		}

		$elapsed       = $now - $this->call_timestamps[ $endpoint ];
		$required_wait = $min_seconds - $elapsed;

		if ( $required_wait > 0 ) {
			usleep( (int) ( $required_wait * 1000000 ) );

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $this->logger ) {
				$this->logger->debug(
					'Smart delay applied',
					array(
						'endpoint' => $endpoint,
						'elapsed'  => round( $elapsed, 3 ),
						'delayed'  => round( $required_wait, 3 ),
					)
				);
			}
		}

		$this->call_timestamps[ $endpoint ] = microtime( true );
	}

	/**
	 * Clear all rate limits
	 *
	 * Removes all rate limit transients to allow immediate requests.
	 * Useful after bulk operations to prevent blocking subsequent UI refreshes.
	 *
	 * @since 1.0.10
	 * @return int Number of rate limits cleared.
	 */
	public function clear_all_limits() {
		global $wpdb;

		// Delete all transients with our prefix.
		$pattern = $wpdb->esc_like( '_transient_' . self::TRANSIENT_PREFIX ) . '%';

		// Count before deletion for logging.
		$count = $wpdb->query(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
				$pattern
			)
		);

		// Delete transients.
		$deleted = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
				$pattern
			)
		);

		if ( $this->logger && $deleted > 0 ) {
			$this->logger->info(
				'Cleared all rate limits',
				array(
					'cleared_count' => $deleted,
					'found_count'   => $count,
				)
			);
		}

		return $deleted;
	}
}
