<?php
/**
 * Costudio_Circuit_Breaker — Circuit breaker per AI provider.
 *
 * States: CLOSED (normal) → OPEN (failing, block calls) → HALF_OPEN (test one call).
 * Uses WordPress transients for state persistence.
 *
 * @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_Circuit_Breaker {

	const STATE_CLOSED    = 'closed';
	const STATE_OPEN      = 'open';
	const STATE_HALF_OPEN = 'half_open';

	/** Number of consecutive failures before opening the circuit. */
	const FAILURE_THRESHOLD = 3;

	/** Initial cooldown in seconds when the circuit opens. */
	const COOLDOWN_BASE = 30;

	/** Maximum cooldown in seconds (caps exponential backoff). */
	const COOLDOWN_MAX = 300;

	/**
	 * Check if the circuit allows a request to proceed.
	 *
	 * @param string $provider Provider name (gemini, claude, openai).
	 * @param string $api_key  API key (hashed for storage).
	 * @return void
	 * @throws \Exception If the circuit is OPEN and the cooldown hasn't expired.
	 */
	public static function check( string $provider, string $api_key ): void {
		$state = self::get_state( $provider, $api_key );

		if ( $state['status'] === self::STATE_OPEN ) {
			$remaining = $state['cooldown_until'] - time();
			if ( $remaining > 0 ) {
				throw new \Exception(
					sprintf(
						/* translators: %1$s: provider, %2$d: seconds remaining */
						__( '%1$s circuit breaker is OPEN after %2$d consecutive failures. Cooling down for %3$d seconds. The pipeline will retry automatically.', 'contentosapp-studio' ),
						ucfirst( $provider ),
						$state['failures'],
						$remaining
					)
				);
			}

			// Cooldown expired — transition to HALF_OPEN (allow one test request).
			self::set_state( $provider, $api_key, [
				'status'         => self::STATE_HALF_OPEN,
				'failures'       => $state['failures'],
				'cooldown_until' => 0,
			] );
		}
		// CLOSED and HALF_OPEN both allow the request to proceed.
	}

	/**
	 * Record a successful API call — resets the circuit to CLOSED.
	 *
	 * @param string $provider
	 * @param string $api_key
	 */
	public static function record_success( string $provider, string $api_key ): void {
		self::set_state( $provider, $api_key, [
			'status'         => self::STATE_CLOSED,
			'failures'       => 0,
			'cooldown_until' => 0,
		] );
	}

	/**
	 * Record a failed API call.
	 *
	 * Increments the failure counter. If it reaches the threshold,
	 * opens the circuit with exponential backoff cooldown.
	 *
	 * @param string $provider
	 * @param string $api_key
	 */
	public static function record_failure( string $provider, string $api_key ): void {
		$state    = self::get_state( $provider, $api_key );
		$failures = $state['failures'] + 1;

		if ( $failures >= self::FAILURE_THRESHOLD ) {
			// Exponential backoff: 30s, 60s, 120s, 240s, capped at 300s.
			$backoff_power = min( $failures - self::FAILURE_THRESHOLD, 4 );
			$cooldown      = min( self::COOLDOWN_BASE * pow( 2, $backoff_power ), self::COOLDOWN_MAX );

			self::set_state( $provider, $api_key, [
				'status'         => self::STATE_OPEN,
				'failures'       => $failures,
				'cooldown_until' => time() + (int) $cooldown,
			] );
		} else {
			self::set_state( $provider, $api_key, [
				'status'         => self::STATE_CLOSED,
				'failures'       => $failures,
				'cooldown_until' => 0,
			] );
		}
	}

	/**
	 * Get the current circuit state for a provider.
	 *
	 * @param string $provider
	 * @param string $api_key
	 * @return array { status: string, failures: int, cooldown_until: int }
	 */
	private static function get_state( string $provider, string $api_key ): array {
		$key  = self::transient_key( $provider, $api_key );
		$data = get_transient( $key );

		if ( false === $data ) {
			return [
				'status'         => self::STATE_CLOSED,
				'failures'       => 0,
				'cooldown_until' => 0,
			];
		}

		return json_decode( $data, true ) ?: [
			'status'         => self::STATE_CLOSED,
			'failures'       => 0,
			'cooldown_until' => 0,
		];
	}

	/**
	 * Persist circuit state.
	 *
	 * @param string $provider
	 * @param string $api_key
	 * @param array  $state
	 */
	private static function set_state( string $provider, string $api_key, array $state ): void {
		$key = self::transient_key( $provider, $api_key );
		// TTL of 10 minutes — stale state auto-cleans to CLOSED.
		set_transient( $key, wp_json_encode( $state ), 600 );
	}

	/**
	 * Build a unique transient key.
	 *
	 * @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_cb_{$provider}_{$hash}";
	}
}
