<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Component\Serialization\PhpSerialize;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Utility\QueueItemKey;

/**
 * Stores queue assignment metadata and migrates legacy assignment blobs.
 *
 * @internal
 */
final class QueueAssignmentRepository implements QueueAssignmentRepositoryInterface {

  /**
   * Key-value collection for queue assignment metadata.
   */
  private const string ASSIGNMENT_COLLECTION = 'ffmpeg_media.queue_assignments';

  /**
   * Legacy key for media_id => queue_name assignments.
   */
  private const string KEY_QUEUE_BY_MEDIA = 'queue_by_media';

  /**
   * Constructs a QueueAssignmentRepository object.
   */
  public function __construct(
    private readonly ?KeyValueFactoryInterface $keyValueFactory,
    private readonly Connection $database,
    private readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function isAvailable(): bool {
    return $this->getAssignmentStore() !== NULL;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getAssignments(array $itemIds = []): array {
    $store = $this->getAssignmentStore();
    if ($store === NULL) {
      return [];
    }

    if ($itemIds === []) {
      $assignments = $this->sanitizeQueueAssignments($store->getAll(), TRUE);
      $legacyAssignments = $this->getLegacyQueueAssignments($store);
      foreach ($legacyAssignments as $itemId => $queueName) {
        $assignments[$itemId] ??= $queueName;
      }

      return $assignments;
    }

    $keys = [];
    foreach ($itemIds as $itemId) {
      if (QueueItemKey::isValid($itemId)) {
        $keys[] = $itemId;
      }
    }
    $keys = array_values(array_unique($keys));
    if ($keys === []) {
      return [];
    }

    $assignments = $this->sanitizeQueueAssignments($store->getMultiple($keys), FALSE);
    $missingKeys = [];
    foreach ($keys as $key) {
      if (!isset($assignments[$key])) {
        $missingKeys[] = $key;
      }
    }

    if ($missingKeys !== []) {
      $legacyAssignments = $this->getLegacyQueueAssignments($store, $missingKeys);
      if ($legacyAssignments !== []) {
        $store->setMultiple($legacyAssignments);
        foreach ($legacyAssignments as $itemId => $queueName) {
          $assignments[$itemId] ??= $queueName;
        }
      }
    }

    $stillMissing = [];
    foreach ($keys as $key) {
      if (!isset($assignments[$key])) {
        $stillMissing[] = $key;
      }
    }
    if ($stillMissing !== []) {
      $backfilledAssignments = $this->backfillAssignmentsFromDatabaseQueue($stillMissing);
      if ($backfilledAssignments !== []) {
        foreach ($backfilledAssignments as $itemId => $queueName) {
          $assignments[$itemId] ??= $queueName;
        }
      }
    }

    return $assignments;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function persistAssignment(string $itemId, string $queueName): void {
    if (!QueueItemKey::isValid($itemId) || $queueName === '') {
      return;
    }

    $store = $this->getAssignmentStore();
    if ($store === NULL) {
      return;
    }

    $store->set($itemId, $queueName);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function clearAssignment(string $itemId): void {
    if (!QueueItemKey::isValid($itemId)) {
      return;
    }

    $store = $this->getAssignmentStore();
    if ($store === NULL) {
      return;
    }

    $store->delete($itemId);
    $this->pruneLegacyQueueAssignments($store, [$itemId]);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function clearAssignments(array $itemIds): void {
    if ($itemIds === []) {
      return;
    }

    $store = $this->getAssignmentStore();
    if ($store === NULL) {
      return;
    }

    $keys = [];
    foreach ($itemIds as $itemId) {
      if (QueueItemKey::isValid($itemId)) {
        $keys[] = $itemId;
      }
    }

    if ($keys !== []) {
      $store->deleteMultiple(array_values(array_unique($keys)));
    }

    $this->pruneLegacyQueueAssignments($store, $itemIds);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function clearAssignmentsForMedia(int $mediaId): void {
    if ($mediaId <= 0) {
      return;
    }

    $store = $this->getAssignmentStore();
    $itemIds = [];
    if ($store !== NULL) {
      foreach (array_keys($store->getAll()) as $key) {
        $itemId = (string) $key;
        if ($itemId === self::KEY_QUEUE_BY_MEDIA || !QueueItemKey::isValid($itemId)) {
          continue;
        }
        if (QueueItemKey::parse($itemId)['media_id'] === $mediaId) {
          $itemIds[] = $itemId;
        }
      }
    }
    $itemIds[] = QueueItemKey::fromParts($mediaId);
    $this->clearAssignments($itemIds);
  }

  /**
   * Attempts to backfill queue assignments from the DB queue backend.
   *
   * @param array<int, string> $itemIds
   *   Queue item IDs missing assignment metadata.
   *
   * @return array<string, string>
   *   Backfilled assignments.
   */
  private function backfillAssignmentsFromDatabaseQueue(array $itemIds): array {
    $store = $this->getAssignmentStore();
    if ($store === NULL || $itemIds === []) {
      return [];
    }

    $missingLookup = [];
    foreach ($itemIds as $itemId) {
      if (QueueItemKey::isValid($itemId)) {
        $missingLookup[$itemId] = TRUE;
      }
    }
    if ($missingLookup === []) {
      return [];
    }

    try {
      $statement = $this->database->select('queue', 'q')
        ->fields('q', ['item_id', 'name', 'data'])
        ->condition('name', [QueueManagerInterface::QUEUE_NAME, QueueManagerInterface::QUEUE_LARGE_NAME], 'IN')
        ->orderBy('created', 'ASC')
        ->orderBy('item_id', 'ASC')
        ->execute();
    }
    catch (DatabaseExceptionWrapper) {
      return [];
    }

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

    $assignments = [];

    foreach ($statement as $row) {
      $queueName = (string) ($row->name ?? '');
      if ($queueName !== QueueManagerInterface::QUEUE_NAME && $queueName !== QueueManagerInterface::QUEUE_LARGE_NAME) {
        continue;
      }

      try {
        $decoded = PhpSerialize::decode((string) ($row->data ?? ''));
      }
      catch (\Throwable $e) {
        $this->logger->warning('Skipping unreadable queue item @item_id while backfilling queue assignments for @queue: @error', [
          '@item_id' => (int) ($row->item_id ?? 0),
          '@queue' => $queueName,
          '@error' => $e->getMessage(),
        ]);
        continue;
      }

      if (!is_array($decoded)) {
        continue;
      }

      $mediaId = (int) ($decoded['media_id'] ?? 0);
      $fieldName = (string) ($decoded['field_name'] ?? '');
      $itemId = QueueItemKey::fromParts($mediaId, $fieldName);
      if ($mediaId <= 0 || !isset($missingLookup[$itemId])) {
        continue;
      }

      $assignments[$itemId] = $queueName;
    }

    if ($assignments !== []) {
      $store->setMultiple($assignments);
    }

    return $assignments;
  }

  /**
   * Returns queue assignment metadata store.
   */
  private function getAssignmentStore(): ?KeyValueStoreInterface {
    return $this->keyValueFactory?->get(self::ASSIGNMENT_COLLECTION);
  }

  /**
   * Sanitizes queue assignment data keyed by queue item ID.
   *
   * @param array<mixed, mixed> $raw
   *   Raw queue assignments.
   * @param bool $skipLegacyKey
   *   TRUE to skip the legacy queue_by_media blob key.
   *
   * @return array<string, string>
   *   Sanitized assignments.
   */
  private function sanitizeQueueAssignments(array $raw, bool $skipLegacyKey): array {
    $sanitized = [];
    foreach ($raw as $index => $value) {
      $key = (string) $index;
      if ($skipLegacyKey && $key === self::KEY_QUEUE_BY_MEDIA) {
        continue;
      }
      $queueName = is_string($value) ? trim($value) : '';
      if (!QueueItemKey::isValid($key) || $queueName === '') {
        continue;
      }
      $sanitized[$key] = $queueName;
    }

    return $sanitized;
  }

  /**
   * Returns sanitized legacy queue assignments keyed by queue item ID.
   *
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $store
   *   Assignment metadata store.
   * @param array<int, string> $itemIds
   *   Optional list of queue item IDs to filter to.
   *
   * @return array<string, string>
   *   Sanitized legacy assignments.
   */
  private function getLegacyQueueAssignments(KeyValueStoreInterface $store, array $itemIds = []): array {
    $legacyAssignments = [];
    foreach ((array) $store->get(self::KEY_QUEUE_BY_MEDIA, []) as $index => $value) {
      $queueName = is_string($value) ? trim($value) : '';
      if ($queueName === '') {
        continue;
      }

      $legacyMediaId = (int) $index;
      if ($legacyMediaId <= 0) {
        continue;
      }

      $legacyAssignments[QueueItemKey::fromParts($legacyMediaId)] = $queueName;
    }

    if ($itemIds === []) {
      return $legacyAssignments;
    }

    $filtered = [];
    foreach ($itemIds as $itemId) {
      $parsed = QueueItemKey::parse($itemId);
      $legacyKey = QueueItemKey::fromParts($parsed['media_id']);
      if (isset($legacyAssignments[$legacyKey])) {
        $filtered[$itemId] = $legacyAssignments[$legacyKey];
      }
    }

    return $filtered;
  }

  /**
   * Removes stale entries from the legacy queue assignment blob when present.
   *
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $store
   *   Assignment metadata store.
   * @param array<int, string> $itemIds
   *   Queue item IDs to remove from the legacy blob.
   */
  private function pruneLegacyQueueAssignments(KeyValueStoreInterface $store, array $itemIds): void {
    if ($itemIds === []) {
      return;
    }

    $legacyAssignments = $this->getLegacyQueueAssignments($store);
    if ($legacyAssignments === []) {
      return;
    }

    $changed = FALSE;
    foreach ($itemIds as $itemId) {
      $parsed = QueueItemKey::parse($itemId);
      $key = QueueItemKey::fromParts($parsed['media_id']);
      if (!isset($legacyAssignments[$key])) {
        continue;
      }

      unset($legacyAssignments[$key]);
      $changed = TRUE;
    }

    if ($changed) {
      $store->set(self::KEY_QUEUE_BY_MEDIA, $legacyAssignments);
    }
  }

}
