<?php
/**
 * Circuit Breaker Class
 *
 * Implements the circuit breaker pattern to prevent cascading failures
 * when external services are unavailable.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Http;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class CircuitBreaker
 *
 * Tracks service health and prevents requests to failing services.
 *
 * States:
 * - CLOSED: Normal operation, requests flow through
 * - OPEN: Service is down, requests fail immediately
 * - HALF_OPEN: Testing if service recovered, limited requests allowed
 */
final class CircuitBreaker {

	/**
	 * Circuit state: Normal operation.
	 *
	 * @var string
	 */
	public const STATE_CLOSED = 'closed';

	/**
	 * Circuit state: Service is down, fail fast.
	 *
	 * @var string
	 */
	public const STATE_OPEN = 'open';

	/**
	 * Circuit state: Testing recovery.
	 *
	 * @var string
	 */
	public const STATE_HALF_OPEN = 'half_open';

	/**
	 * Service identifier for this circuit.
	 *
	 * @var string
	 */
	private string $service_key;

	/**
	 * Storage instance for persisting state.
	 *
	 * @var CircuitBreakerStorage
	 */
	private CircuitBreakerStorage $storage;

	/**
	 * Current circuit state.
	 *
	 * @var array
	 */
	private array $state;

	/**
	 * Number of failures before opening the circuit.
	 *
	 * @var int
	 */
	private int $failure_threshold;

	/**
	 * Seconds to wait before testing recovery.
	 *
	 * @var int
	 */
	private int $recovery_timeout;

	/**
	 * Number of successes needed to close the circuit.
	 *
	 * @var int
	 */
	private int $success_threshold;

	/**
	 * Constructor.
	 *
	 * @param string                     $service_key Service identifier.
	 * @param CircuitBreakerStorage|null $storage     Optional storage instance.
	 */
	public function __construct( string $service_key, ?CircuitBreakerStorage $storage = null ) {
		$this->service_key = $service_key;
		$this->storage     = $storage ?? new CircuitBreakerStorage();

		// Load configuration with filterable defaults.
		$this->failure_threshold = (int) apply_filters( 'sequensy_circuit_failure_threshold', 5 );
		$this->recovery_timeout  = (int) apply_filters( 'sequensy_circuit_recovery_timeout', 300 );
		$this->success_threshold = (int) apply_filters( 'sequensy_circuit_success_threshold', 2 );

		// Load current state from storage.
		$this->state = $this->storage->getState( $service_key );

		// Check if we should transition from OPEN to HALF_OPEN.
		$this->check_recovery_timeout();
	}

	/**
	 * Check if a request is allowed.
	 *
	 * @return bool True if request should proceed, false if circuit is open.
	 */
	public function canRequest(): bool {
		// Refresh state in case of concurrent requests.
		$this->state = $this->storage->getState( $this->service_key );
		$this->check_recovery_timeout();

		switch ( $this->state['state'] ) {
			case self::STATE_CLOSED:
				return true;

			case self::STATE_HALF_OPEN:
				// Allow one request to test recovery.
				return true;

			case self::STATE_OPEN:
			default:
				return false;
		}
	}

	/**
	 * Record a successful request.
	 *
	 * @return void
	 */
	public function recordSuccess(): void {
		$current_state = $this->state['state'];

		switch ( $current_state ) {
			case self::STATE_CLOSED:
				// Reset failure count on success.
				$this->state['failure_count'] = 0;
				$this->state['last_error']    = null;
				break;

			case self::STATE_HALF_OPEN:
				// Increment success count.
				++$this->state['success_count'];

				// If we've reached the success threshold, close the circuit.
				if ( $this->state['success_count'] >= $this->success_threshold ) {
					$this->transition_to( self::STATE_CLOSED );
					$this->state['failure_count'] = 0;
					$this->state['success_count'] = 0;
					$this->state['opened_at']     = null;
				}
				break;

			case self::STATE_OPEN:
				// Shouldn't happen, but handle gracefully.
				break;
		}

		$this->save_state();
	}

	/**
	 * Record a failed request.
	 *
	 * @param string|null $error_message Optional error message for debugging.
	 * @return void
	 */
	public function recordFailure( ?string $error_message = null ): void {
		$current_state = $this->state['state'];

		++$this->state['failure_count'];
		$this->state['last_failure_time'] = time();

		if ( null !== $error_message ) {
			$this->state['last_error'] = $error_message;
		}

		switch ( $current_state ) {
			case self::STATE_CLOSED:
				// Check if we've reached the failure threshold.
				if ( $this->state['failure_count'] >= $this->failure_threshold ) {
					$this->transition_to( self::STATE_OPEN );
					$this->state['opened_at'] = time();
				}
				break;

			case self::STATE_HALF_OPEN:
				// Service still failing, reopen the circuit.
				$this->transition_to( self::STATE_OPEN );
				$this->state['opened_at']     = time();
				$this->state['success_count'] = 0;
				break;

			case self::STATE_OPEN:
				// Already open, just update failure time.
				break;
		}

		$this->save_state();
	}

	/**
	 * Get the current circuit state.
	 *
	 * @return string Current state (closed, open, half_open).
	 */
	public function getState(): string {
		return $this->state['state'];
	}

	/**
	 * Check if the circuit is open.
	 *
	 * @return bool True if circuit is open.
	 */
	public function isOpen(): bool {
		return self::STATE_OPEN === $this->state['state'];
	}

	/**
	 * Get the time remaining until recovery can be attempted.
	 *
	 * @return int Seconds until half-open transition, 0 if already past timeout.
	 */
	public function getRetryAfter(): int {
		if ( self::STATE_OPEN !== $this->state['state'] ) {
			return 0;
		}

		$opened_at = $this->state['opened_at'] ?? 0;
		$elapsed   = time() - $opened_at;
		$remaining = $this->recovery_timeout - $elapsed;

		return max( 0, $remaining );
	}

	/**
	 * Force reset the circuit to closed state.
	 *
	 * @return void
	 */
	public function reset(): void {
		$old_state = $this->state['state'];

		$this->state = CircuitBreakerStorage::getDefaultState();
		$this->save_state();

		if ( self::STATE_CLOSED !== $old_state ) {
			$this->log_transition( $old_state, self::STATE_CLOSED, 'Manual reset' );
		}
	}

	/**
	 * Get circuit statistics for debugging.
	 *
	 * @return array Circuit statistics.
	 */
	public function getStats(): array {
		return array(
			'service_key'       => $this->service_key,
			'state'             => $this->state['state'],
			'failure_count'     => $this->state['failure_count'],
			'success_count'     => $this->state['success_count'],
			'last_failure_time' => $this->state['last_failure_time'],
			'opened_at'         => $this->state['opened_at'],
			'last_error'        => $this->state['last_error'],
			'retry_after'       => $this->getRetryAfter(),
			'config'            => array(
				'failure_threshold' => $this->failure_threshold,
				'recovery_timeout'  => $this->recovery_timeout,
				'success_threshold' => $this->success_threshold,
			),
		);
	}

	/**
	 * Check if recovery timeout has elapsed and transition to HALF_OPEN.
	 *
	 * @return void
	 */
	private function check_recovery_timeout(): void {
		if ( self::STATE_OPEN !== $this->state['state'] ) {
			return;
		}

		$opened_at = $this->state['opened_at'] ?? 0;
		$elapsed   = time() - $opened_at;

		if ( $elapsed >= $this->recovery_timeout ) {
			$this->transition_to( self::STATE_HALF_OPEN );
			$this->state['success_count'] = 0;
			$this->save_state();
		}
	}

	/**
	 * Transition to a new state.
	 *
	 * @param string $new_state New circuit state.
	 * @return void
	 */
	private function transition_to( string $new_state ): void {
		$old_state = $this->state['state'];

		if ( $old_state === $new_state ) {
			return;
		}

		$this->state['state'] = $new_state;
		$this->log_transition( $old_state, $new_state );

		/**
		 * Fires when circuit breaker state changes.
		 *
		 * @param string $service_key Service identifier.
		 * @param string $old_state   Previous state.
		 * @param string $new_state   New state.
		 * @param array  $stats       Current circuit statistics.
		 */
		do_action( 'sequensy_circuit_state_changed', $this->service_key, $old_state, $new_state, $this->getStats() );
	}

	/**
	 * Log a state transition.
	 *
	 * @param string      $old_state Previous state.
	 * @param string      $new_state New state.
	 * @param string|null $reason    Optional reason for transition.
	 * @return void
	 */
	private function log_transition( string $old_state, string $new_state, ?string $reason = null ): void {
		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
			return;
		}

		$message = sprintf(
			'Sequensy Pro Circuit Breaker [%s]: %s -> %s (failures: %d)',
			$this->service_key,
			$old_state,
			$new_state,
			$this->state['failure_count']
		);

		if ( null !== $reason ) {
			$message .= ' - ' . $reason;
		}

		error_log( $message );
	}

	/**
	 * Save current state to storage.
	 *
	 * @return void
	 */
	private function save_state(): void {
		$this->storage->setState( $this->service_key, $this->state );
	}
}
