<?php
/**
 * Costudio_Rate_Limiter — Token-bucket rate limiter per provider.
 *
 * Uses WordPress transients (compatible with object cache / Redis).
 * Each provider+key combination has its own bucket.
 *
 * @package Contentosapp_Studio
 * @since   3.1.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages go to PHP error log, never to HTML output.

class Costudio_Rate_Limiter {

	/**
	 * Default rate limits per provider (requests per minute).
	 * Can be overridden via the 'costudio_rate_limits' filter.
	 */
	const DEFAULT_LIMITS = [
		'gemini' => 15,
		'claude' => 50,
		'openai' => 60,
	];

	/**
	 * Wait (blocking) until a request slot is available.
	 *
	 * Uses a simple sliding-window counter stored in a transient.
	 * If the limit is reached, sleeps until the window resets.
	 *
	 * @param string $provider Provider name (gemini, claude, openai).
	 * @param string $api_key  API key (hashed for storage).
	 * @return void
	 * @throws \Exception If waiting exceeds the maximum wait time (120s).
	 */
	public static function wait( string $provider, string $api_key ): void {
		$limits    = apply_filters( 'costudio_rate_limits', self::DEFAULT_LIMITS );
		$rpm       = (int) ( $limits[ $provider ] ?? 60 );
		$window    = 60; // 1 minute window
		$max_wait  = 120; // Max seconds to wait before giving up
		$key       = self::transient_key( $provider, $api_key );
		$waited    = 0;

		while ( $waited < $max_wait ) {
			$bucket = get_transient( $key );

			if ( false === $bucket ) {
				// First request — initialize bucket.
				set_transient( $key, wp_json_encode( [
					'count'    => 1,
					'window_start' => time(),
				] ), $window );
				return;
			}

			$data         = json_decode( $bucket, true );
			$count        = (int) ( $data['count'] ?? 0 );
			$window_start = (int) ( $data['window_start'] ?? time() );
			$elapsed      = time() - $window_start;

			// Window expired — reset.
			if ( $elapsed >= $window ) {
				set_transient( $key, wp_json_encode( [
					'count'        => 1,
					'window_start' => time(),
				] ), $window );
				return;
			}

			// Under limit — increment and proceed.
			if ( $count < $rpm ) {
				$data['count'] = $count + 1;
				set_transient( $key, wp_json_encode( $data ), $window - $elapsed );
				return;
			}

			// At limit — wait for window to expire.
			$sleep_time = min( $window - $elapsed + 1, 10 );
			sleep( $sleep_time );
			$waited += $sleep_time;
		}

		throw new \Exception(
			sprintf(
				/* translators: %1$s: provider name, %2$d: requests per minute limit */
				__( 'Rate limit exceeded for %1$s (%2$d RPM). Please wait before retrying.', 'contentosapp-studio' ),
				$provider,
				$rpm
			)
		);
	}

	/**
	 * Record a Retry-After response from the API.
	 *
	 * Fills the bucket to capacity and sets the transient TTL to the
	 * retry-after value so subsequent calls will wait.
	 *
	 * @param string $provider    Provider name.
	 * @param string $api_key     API key.
	 * @param int    $retry_after Seconds to wait (from Retry-After header).
	 */
	public static function record_retry_after( string $provider, string $api_key, int $retry_after ): void {
		$limits = apply_filters( 'costudio_rate_limits', self::DEFAULT_LIMITS );
		$rpm    = (int) ( $limits[ $provider ] ?? 60 );
		$key    = self::transient_key( $provider, $api_key );

		set_transient( $key, wp_json_encode( [
			'count'        => $rpm, // Fill to capacity — forces wait.
			'window_start' => time(),
		] ), max( 1, $retry_after ) );
	}

	/**
	 * Build a unique transient key for a provider+key pair.
	 *
	 * @param string $provider
	 * @param string $api_key
	 * @return string
	 */
	private static function transient_key( string $provider, string $api_key ): string {
		$hash = substr( md5( $api_key ), 0, 8 );
		return "costudio_rl_{$provider}_{$hash}";
	}
}
