<?php
namespace R2_Offload\Storage;

use R2_Offload\Logger;
use R2_Offload\Settings;

defined( 'ABSPATH' ) || exit;

/**
 * Best-effort client-side rate limiter + exponential backoff wrapper for GCS API calls.
 *
 * - throttle(): enforces a minimum gap between any two calls.
 * - wrap(): invokes a closure with retries; bails fast on 4xx except 408/429.
 */
final class Rate_Limiter {

	private Settings $settings;
	private Logger $logger;
	private float $last_call_at = 0.0;

	public function __construct( Settings $settings, Logger $logger ) {
		$this->settings = $settings;
		$this->logger   = $logger;
	}

	public function throttle(): void {
		$min_ms = max( 0, (int) $this->settings->get( 'rate_ms', 150 ) );
		if ( $min_ms === 0 ) {
			return;
		}
		$now     = microtime( true );
		$elapsed = ( $now - $this->last_call_at ) * 1000.0;
		if ( $this->last_call_at > 0 && $elapsed < $min_ms ) {
			usleep( (int) ( ( $min_ms - $elapsed ) * 1000 ) );
		}
		$this->last_call_at = microtime( true );
	}

	public function wrap( callable $fn, string $op = 'gcs' ) {
		$max  = max( 0, (int) $this->settings->get( 'max_retries', 4 ) );
		$base = max( 50, (int) $this->settings->get( 'backoff_base_ms', 500 ) );

		for ( $attempt = 0; $attempt <= $max; $attempt++ ) {
			$this->throttle();
			try {
				return $fn();
			} catch ( \Throwable $e ) {
				$status = self::status_from( $e );
				$retryable = $status === 0 || $status === 408 || $status === 429 || ( $status >= 500 && $status < 600 );
				if ( ! $retryable || $attempt === $max ) {
					throw $e;
				}
				$jitter = random_int( 0, (int) ( $base / 2 ) );
				$sleep  = (int) ( $base * ( 2 ** $attempt ) ) + $jitter;
				$this->logger->warn(
					sprintf( '%s: retry %d after %dms (status=%d)', $op, $attempt + 1, $sleep, $status ),
					'gcs',
					[ 'error' => $e->getMessage() ]
				);
				usleep( $sleep * 1000 );
			}
		}
		return null;
	}

	private static function status_from( \Throwable $e ): int {
		$method = [ $e, 'getCode' ];
		$code   = is_callable( $method ) ? (int) $e->getCode() : 0;
		if ( $code >= 400 && $code < 600 ) {
			return $code;
		}
		if ( preg_match( '/\b(4\d\d|5\d\d)\b/', $e->getMessage(), $m ) ) {
			return (int) $m[1];
		}
		return 0;
	}
}
