<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Statement\FetchAs;

/**
 * Database-backed dead-letter storage for permanently failed conversion jobs.
 *
 * @internal
 */
final class DeadLetterStorage implements DeadLetterStorageInterface {

  /**
   * Table name for dead-letter storage.
   */
  protected const string TABLE = 'ffmpeg_media_dead_letters';

  /**
   * Seconds in one day.
   */
  private const int SECONDS_PER_DAY = 86400;

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

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function store(int $mediaId, string $errorMessage, int $retryCount, ?string $profileId = NULL, ?string $lastErrorCode = NULL, string $fieldName = ''): void {
    $now = time();

    $this->database->merge(self::TABLE)
      ->keys([
        'media_id' => $mediaId,
        'field_name' => $fieldName,
      ])
      ->fields([
        'error_message' => $errorMessage,
        'retry_count' => $retryCount,
        'failed_at' => $now,
        'profile_id' => $profileId,
        'last_error_code' => $lastErrorCode,
      ])
      ->insertFields([
        'media_id' => $mediaId,
        'field_name' => $fieldName,
        'error_message' => $errorMessage,
        'retry_count' => $retryCount,
        'failed_at' => $now,
        'profile_id' => $profileId,
        'last_error_code' => $lastErrorCode,
      ])
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getAll(int $limit = 50, int $offset = 0): array {
    $query = $this->database->select(self::TABLE, 'd')
      ->fields('d')
      ->orderBy('failed_at', 'DESC')
      ->range($offset, $limit);

    $statement = $query->execute();

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

    /** @var array<int, \stdClass> $rows */
    $rows = $statement->fetchAll(FetchAs::Object);

    return array_map($this->mapRow(...), $rows);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getByMediaId(int $mediaId, string $fieldName = ''): ?array {
    if ($fieldName !== '') {
      $statement = $this->database->select(self::TABLE, 'd')
        ->fields('d')
        ->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 $this->mapRow($rows[$fieldName]);
      }
      if (isset($rows[''])) {
        return $this->mapRow($rows['']);
      }

      return NULL;
    }

    $statement = $this->database->select(self::TABLE, 'd')
      ->fields('d')
      ->condition('media_id', $mediaId)
      ->orderBy('failed_at', 'DESC')
      ->range(0, 1)
      ->execute();

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

    $row = $statement->fetchObject();

    if ($row === FALSE) {
      return NULL;
    }

    return $this->mapRow($row);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function remove(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]
  public function purge(int $olderThanDays = 30): int {
    $cutoff = time() - ($olderThanDays * self::SECONDS_PER_DAY);

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

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function count(): int {
    $statement = $this->database->select(self::TABLE, 'd')
      ->countQuery()
      ->execute();

    return (int) ($statement?->fetchField() ?? 0);
  }

  /**
   * Maps a database row object to a typed array.
   *
   * @param \stdClass $row
   *   Database row object.
   *
   * @phpstan-return array{id: int, media_id: int, field_name: string, error_message: string, retry_count: int, failed_at: int, profile_id: string|null, last_error_code: string|null}
   */
  private function mapRow(\stdClass $row): array {
    return [
      'id' => (int) $row->id,
      'media_id' => (int) $row->media_id,
      'field_name' => (string) ($row->field_name ?? ''),
      'error_message' => (string) $row->error_message,
      'retry_count' => (int) $row->retry_count,
      'failed_at' => (int) $row->failed_at,
      'profile_id' => $row->profile_id !== NULL ? (string) $row->profile_id : NULL,
      'last_error_code' => $row->last_error_code !== NULL ? (string) $row->last_error_code : NULL,
    ];
  }

}
