<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\ffmpeg_media\Enum\CircuitBreakerState;

/**
 * Circuit breaker implementation for hardware acceleration failures.
 *
 * This service implements the circuit breaker pattern to prevent repeated
 * attempts to use failing hardware encoders. When a hardware encoder fails
 * multiple times, the circuit "opens" and subsequent requests fail fast,
 * falling back to software encoding.
 *
 * After a timeout period, the circuit enters "half-open" state and allows
 * a test request. If it succeeds, the circuit closes; if it fails, the
 * circuit reopens.
 *
 * @internal
 */
final class CircuitBreaker implements CircuitBreakerInterface {

  /**
   * Cache key prefix for circuit breaker state.
   */
  private const string CACHE_PREFIX = 'ffmpeg_media:circuit_breaker:';

  /**
   * Default failure threshold before opening circuit.
   */
  private const int DEFAULT_FAILURE_THRESHOLD = 5;

  /**
   * Default success threshold in half-open to close circuit.
   */
  private const int DEFAULT_SUCCESS_THRESHOLD = 2;

  /**
   * Default timeout in seconds before transitioning to half-open.
   */
  private const int DEFAULT_TIMEOUT = 300;

  /**
   * Constructs a CircuitBreaker object.
   *
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend for persistent state storage.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory for threshold settings.
   */
  public function __construct(
    private readonly CacheBackendInterface $cache,
    private readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isOpen(string $service): bool {
    $state = $this->getStateEnum($service);

    return $state->isBlocking();
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function recordSuccess(string $service): void {
    $circuitState = $this->loadState($service);
    $state = CircuitBreakerState::tryFrom($circuitState['state']) ?? CircuitBreakerState::Closed;

    match ($state) {
      CircuitBreakerState::HalfOpen => $this->handleHalfOpenSuccess($service, $circuitState),
      CircuitBreakerState::Closed => $this->resetFailures($service, $circuitState),
      // In open state, success shouldn't happen
      // (request should have been skipped).
      CircuitBreakerState::Open => NULL,
    };
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function recordFailure(string $service): void {
    $circuitState = $this->loadState($service);
    $circuitState['failures']++;
    $circuitState['last_failure_time'] = time();
    $circuitState['successes'] = 0;

    $failureThreshold = $this->getFailureThreshold();

    if ($circuitState['failures'] >= $failureThreshold) {
      // Open the circuit.
      $circuitState['state'] = CircuitBreakerState::Open->value;
      $circuitState['opened_at'] = time();
    }

    $this->saveState($service, $circuitState);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getState(string $service): string {
    return $this->getStateEnum($service)->value;
  }

  /**
   * Gets the current state as a CircuitBreakerState enum.
   *
   * @param string $service
   *   Service identifier.
   *
   * @return \Drupal\ffmpeg_media\Enum\CircuitBreakerState
   *   The current circuit breaker state.
   */
  #[\NoDiscard]
  public function getStateEnum(string $service): CircuitBreakerState {
    $circuitState = $this->loadState($service);
    $state = CircuitBreakerState::tryFrom($circuitState['state']) ?? CircuitBreakerState::Closed;

    // Check for state transition from open to half-open.
    if ($state === CircuitBreakerState::Open) {
      $timeout = $this->getTimeout();
      $timeInOpenState = time() - $circuitState['opened_at'];

      if ($timeInOpenState >= $timeout) {
        // Transition to half-open.
        $circuitState['state'] = CircuitBreakerState::HalfOpen->value;
        $circuitState['successes'] = 0;
        $this->saveState($service, $circuitState);
        return CircuitBreakerState::HalfOpen;
      }
    }

    return $state;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function reset(string $service): void {
    $circuitState = $this->createDefaultState();
    $this->saveState($service, $circuitState);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getFailureCount(string $service): int {
    $circuitState = $this->loadState($service);

    return $circuitState['failures'];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getTimeUntilHalfOpen(string $service): int {
    $circuitState = $this->loadState($service);

    $state = CircuitBreakerState::tryFrom($circuitState['state']) ?? CircuitBreakerState::Closed;
    if ($state !== CircuitBreakerState::Open) {
      return 0;
    }

    $timeout = $this->getTimeout();
    $timeInOpenState = time() - $circuitState['opened_at'];
    $remaining = $timeout - $timeInOpenState;

    return max(0, $remaining);
  }

  /**
   * Loads circuit state from cache or returns default.
   *
   * @param string $service
   *   Service identifier.
   *
   * @return array{
   *   state: string,
   *   failures: int,
   *   successes: int,
   *   last_failure_time: int,
   *   opened_at: int
   *   }
   *   Circuit state array.
   */
  private function loadState(string $service): array {
    $cacheKey = self::CACHE_PREFIX . $service;
    $cached = $this->cache->get($cacheKey);

    if ($cached !== FALSE && is_array($cached->data)) {
      return $this->normalizeState($cached->data);
    }

    return $this->createDefaultState();
  }

  /**
   * Saves circuit state to cache.
   *
   * @param string $service
   *   Service identifier.
   * @param array{state: string, failures: int, successes: int, last_failure_time: int, opened_at: int} $circuitState
   *   Circuit state with keys: state, failures, successes, last_failure_time,
   *   opened_at.
   */
  private function saveState(string $service, array $circuitState): void {
    $cacheKey = self::CACHE_PREFIX . $service;
    // Cache for 24 hours, state will be refreshed on access.
    $this->cache->set($cacheKey, $circuitState, time() + 86400);
  }

  /**
   * Creates default circuit state (closed, no failures).
   *
   * @return array{
   *   state: string,
   *   failures: int,
   *   successes: int,
   *   last_failure_time: int,
   *   opened_at: int
   *   }
   *   Default circuit state.
   */
  private function createDefaultState(): array {
    return [
      'state' => CircuitBreakerState::Closed->value,
      'failures' => 0,
      'successes' => 0,
      'last_failure_time' => 0,
      'opened_at' => 0,
    ];
  }

  /**
   * Handles successful request in half-open state.
   *
   * @param string $service
   *   Service identifier.
   * @param array{state: string, failures: int, successes: int, last_failure_time: int, opened_at: int} $circuitState
   *   Current circuit state.
   */
  private function handleHalfOpenSuccess(string $service, array $circuitState): void {
    $circuitState['successes']++;
    $successThreshold = $this->getSuccessThreshold();

    if ($circuitState['successes'] >= $successThreshold) {
      // Close the circuit.
      $circuitState = $this->createDefaultState();
    }

    $this->saveState($service, $circuitState);
  }

  /**
   * Resets failure count on success in closed state.
   *
   * @param string $service
   *   Service identifier.
   * @param array{state: string, failures: int, successes: int, last_failure_time: int, opened_at: int} $circuitState
   *   Current circuit state.
   */
  private function resetFailures(string $service, array $circuitState): void {
    if ($circuitState['failures'] > 0) {
      $circuitState['failures'] = 0;
      $this->saveState($service, $circuitState);
    }
  }

  /**
   * Normalizes circuit state data from cache.
   *
   * @param array{state?: string, failures?: int, successes?: int, last_failure_time?: int, opened_at?: int} $state
   *   Raw state data.
   *
   * @return array{state: string, failures: int, successes: int, last_failure_time: int, opened_at: int}
   *   Normalized circuit state.
   */
  private function normalizeState(array $state): array {
    $defaults = $this->createDefaultState();
    $merged = array_merge($defaults, $state);

    return [
      'state' => (string) $merged['state'],
      'failures' => (int) $merged['failures'],
      'successes' => (int) $merged['successes'],
      'last_failure_time' => (int) $merged['last_failure_time'],
      'opened_at' => (int) $merged['opened_at'],
    ];
  }

  /**
   * Gets the failure threshold from config or default.
   *
   * @return int
   *   Number of failures before opening circuit.
   */
  #[\NoDiscard]
  private function getFailureThreshold(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $threshold = $config->get('circuit_breaker.failure_threshold');

    return is_int($threshold) && $threshold > 0
      ? $threshold
      : self::DEFAULT_FAILURE_THRESHOLD;
  }

  /**
   * Gets the success threshold from config or default.
   *
   * @return int
   *   Number of successes in half-open to close circuit.
   */
  #[\NoDiscard]
  private function getSuccessThreshold(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $threshold = $config->get('circuit_breaker.success_threshold');

    return is_int($threshold) && $threshold > 0
      ? $threshold
      : self::DEFAULT_SUCCESS_THRESHOLD;
  }

  /**
   * Gets the timeout from config or default.
   *
   * @return int
   *   Seconds before transitioning from open to half-open.
   */
  #[\NoDiscard]
  private function getTimeout(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $timeout = $config->get('circuit_breaker.timeout');

    return is_int($timeout) && $timeout > 0
      ? $timeout
      : self::DEFAULT_TIMEOUT;
  }

}
