<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;

/**
 * Service for managing conversion retry logic with exponential backoff.
 *
 * @internal
 */
final class RetryManager implements RetryManagerInterface {

  /**
   * Table name for retry tracking.
   */
  protected const string TABLE = 'ffmpeg_media_retries';

  /**
   * Constructs a RetryManager object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   Database connection.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   */
  public function __construct(
    protected readonly Connection $database,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function shouldRetry(int $mediaId, string $fieldName = ''): bool {
    if (!$this->isEnabled()) {
      return FALSE;
    }

    $retryCount = $this->getRetryCount($mediaId, $fieldName);
    $maxRetries = $this->getMaxRetries();

    return $retryCount < $maxRetries;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getNextRetryTime(int $mediaId, string $fieldName = ''): int {
    $record = $this->getRetryRecord($mediaId, $fieldName);

    if ($record === NULL) {
      return time();
    }

    return (int) $record['next_retry_at'];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRetryDelay(int $mediaId, string $fieldName = ''): int {
    $retryCount = $this->getRetryCount($mediaId, $fieldName);
    $baseDelay = $this->getBaseDelay();
    $maxDelay = $this->getMaxDelay();

    // Exponential backoff: base_delay * 2^(attempt-1).
    // Attempt 1 = base_delay, attempt 2 = base_delay*2.
    $delay = $baseDelay
      * (2 ** max(0, $retryCount - 1));

    return min($delay, $maxDelay);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function recordRetry(int $mediaId, ?string $errorMessage = NULL, string $fieldName = ''): void {
    $now = time();
    $retryCount = $this->getRetryCount($mediaId, $fieldName) + 1;
    $nextRetryDelay = $this->calculateDelayForAttempt($retryCount);
    $nextRetryAt = $now + $nextRetryDelay;

    $this->database->merge(self::TABLE)
      ->keys([
        'media_id' => $mediaId,
        'field_name' => $fieldName,
      ])
      ->fields([
        'field_name' => $fieldName,
        'retry_count' => $retryCount,
        'last_attempt_at' => $now,
        'next_retry_at' => $nextRetryAt,
        'last_error' => $errorMessage,
        'updated' => $now,
      ])
      ->insertFields([
        'media_id' => $mediaId,
        'field_name' => $fieldName,
        'retry_count' => $retryCount,
        'first_attempt_at' => $now,
        'last_attempt_at' => $now,
        'next_retry_at' => $nextRetryAt,
        'last_error' => $errorMessage,
        'created' => $now,
        'updated' => $now,
      ])
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRetryCount(int $mediaId, string $fieldName = ''): int {
    $record = $this->getRetryRecord($mediaId, $fieldName);

    return $record !== NULL ? (int) $record['retry_count'] : 0;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function resetRetries(int $mediaId, string $fieldName = ''): void {
    $delete = $this->database->delete(self::TABLE)
      ->condition('media_id', $mediaId);

    if ($fieldName !== '') {
      $delete->condition('field_name', [$fieldName, ''], 'IN');
    }

    $delete->execute();
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isRetryDue(int $mediaId, string $fieldName = ''): bool {
    $record = $this->getRetryRecord($mediaId, $fieldName);

    if ($record === NULL) {
      return TRUE;
    }

    return time() >= (int) $record['next_retry_at'];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getPendingRetries(): array {
    if (!$this->isEnabled()) {
      return [];
    }

    $maxRetries = $this->getMaxRetries();
    $now = time();

    $query = $this->database->select(self::TABLE, 'r')
      ->fields('r', ['media_id', 'field_name', 'retry_count', 'next_retry_at'])
      ->condition('retry_count', $maxRetries, '<')
      ->condition('next_retry_at', $now, '<=')
      ->orderBy('next_retry_at', 'ASC');

    $results = [];
    $statement = $query->execute();
    if ($statement !== NULL) {
      foreach ($statement as $row) {
        $results[] = [
          'media_id' => (int) $row->media_id,
          'field_name' => (string) ($row->field_name ?? ''),
          'retry_count' => (int) $row->retry_count,
          'next_retry' => (int) $row->next_retry_at,
        ];
      }
    }

    return $results;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function cleanup(int $maxAge = 604800): int {
    $cutoff = time() - $maxAge;

    return (int) $this->database->delete(self::TABLE)
      ->condition('updated', $cutoff, '<')
      ->execute();
  }

  /**
   * Gets a retry record for a media entity.
   *
   * @param int $mediaId
   *   The media entity ID.
   * @param string $fieldName
   *   Optional source field name for field-scoped retry tracking.
   *
   * @return array<string, string|null>|null
   *   The retry record or NULL.
   */
  protected function getRetryRecord(int $mediaId, string $fieldName = ''): ?array {
    if ($fieldName !== '') {
      $statement = $this->database->select(self::TABLE, 'r')
        ->fields('r')
        ->condition('media_id', $mediaId)
        ->condition('field_name', [$fieldName, ''], 'IN')
        ->execute();

      if ($statement === NULL) {
        return NULL;
      }

      $rows = $statement->fetchAllAssoc('field_name');
      if (!is_array($rows) || $rows === []) {
        return NULL;
      }
      if (isset($rows[$fieldName])) {
        return (array) $rows[$fieldName];
      }
      if (isset($rows[''])) {
        return (array) $rows[''];
      }

      return NULL;
    }

    $statement = $this->database->select(self::TABLE, 'r')
      ->fields('r')
      ->condition('media_id', $mediaId)
      ->execute();

    if ($statement === NULL) {
      return NULL;
    }

    $rows = $statement->fetchAll();
    if (!is_array($rows) || $rows === []) {
      return NULL;
    }

    $records = array_map(
      static fn(array|object $row): array => (array) $row,
      $rows,
    );

    usort($records, static function (array $a, array $b): int {
      $retryComparison = (int) ($b['retry_count'] ?? 0) <=> (int) ($a['retry_count'] ?? 0);
      if ($retryComparison !== 0) {
        return $retryComparison;
      }

      return (int) ($b['updated'] ?? 0) <=> (int) ($a['updated'] ?? 0);
    });

    return $records[0];
  }

  /**
   * Calculates delay for a specific attempt number.
   *
   * @param int $attemptNumber
   *   The attempt number (1-based).
   *
   * @return int
   *   Delay in seconds.
   */
  protected function calculateDelayForAttempt(int $attemptNumber): int {
    $baseDelay = $this->getBaseDelay();
    $maxDelay = $this->getMaxDelay();

    // Exponential backoff: base_delay * 2^(attempt-1).
    // Attempt 1 = base_delay, Attempt 2 = base_delay * 2, etc.
    $delay = $baseDelay * (2 ** ($attemptNumber - 1));

    return min($delay, $maxDelay);
  }

  /**
   * Gets the base delay configuration.
   *
   * @return int
   *   Base delay in seconds.
   */
  protected function getBaseDelay(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return (int) ($config->get('retry.base_delay') ?? self::DEFAULT_BASE_DELAY);
  }

  /**
   * Gets the maximum delay configuration.
   *
   * @return int
   *   Maximum delay in seconds.
   */
  protected function getMaxDelay(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return (int) ($config->get('retry.max_delay') ?? self::DEFAULT_MAX_DELAY);
  }

  /**
   * Gets the maximum retries configuration.
   *
   * @return int
   *   Maximum retry attempts.
   */
  protected function getMaxRetries(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return (int) ($config->get('retry.max_retries') ?? self::DEFAULT_MAX_RETRIES);
  }

  /**
   * Checks if retry functionality is enabled.
   *
   * @return bool
   *   TRUE if retries are enabled in config.
   */
  protected function isEnabled(): bool {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return (bool) ($config->get('retry.enabled') ?? FALSE);
  }

}
