<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\ffmpeg_media\Utility\QueueItemKey;

/**
 * Computes effective queue scheduling from queue rows and persisted metadata.
 *
 * @internal
 */
final class QueueSchedulingService implements QueueSchedulingServiceInterface {

  /**
   * Queue scheduling runtime collection name.
   */
  private const string METADATA_COLLECTION = 'ffmpeg_media.queue_scheduling';

  /**
   * Metadata key for global pause state.
   */
  private const string KEY_GLOBAL_PAUSED = 'global_paused';

  /**
   * Constructs a QueueSchedulingService object.
   */
  public function __construct(
    protected readonly QueueManagerInterface $queueManager,
    protected readonly QueueMetadataRepositoryInterface $queueMetadataRepository,
    protected readonly KeyValueFactoryInterface $keyValueFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isPaused(string|int $itemId): bool {
    $itemId = (string) $itemId;
    if (!QueueItemKey::isValid($itemId)) {
      return FALSE;
    }

    $metadata = $this->loadMetadata([$itemId]);
    $paused = $metadata['paused'];
    return ($paused[$itemId] ?? FALSE) === TRUE;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRunnableItemIds(string $queueName): array {
    if ($this->isGloballyPaused()) {
      return [];
    }

    $itemIds = $this->getQueueItemIds($queueName);
    if ($itemIds === []) {
      return [];
    }

    $metadata = $this->loadMetadata($itemIds);
    $order = $metadata['order'];
    $priorities = $metadata['priorities'];
    $paused = $metadata['paused'];

    $queueIndex = [];
    foreach ($itemIds as $index => $id) {
      $queueIndex[$id] = $index;
    }

    usort($itemIds, static function (string $a, string $b) use ($order, $priorities, $queueIndex): int {
      $positionA = $order[$a] ?? PHP_INT_MAX;
      $positionB = $order[$b] ?? PHP_INT_MAX;
      if ($positionA !== $positionB) {
        return $positionA <=> $positionB;
      }

      $priorityA = max(0, min(2, (int) ($priorities[$a] ?? 0)));
      $priorityB = max(0, min(2, (int) ($priorities[$b] ?? 0)));
      if ($priorityA !== $priorityB) {
        return $priorityB <=> $priorityA;
      }

      return ($queueIndex[$a] ?? PHP_INT_MAX) <=> ($queueIndex[$b] ?? PHP_INT_MAX);
    });

    return array_values(array_filter(
      $itemIds,
      static fn(string $id): bool => ($paused[$id] ?? FALSE) !== TRUE,
    ));
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRunnableMediaIds(string $queueName): array {
    $itemIds = $this->getRunnableItemIds($queueName);
    if ($itemIds === []) {
      return [];
    }

    $mediaIds = [];
    foreach ($itemIds as $itemId) {
      $mediaId = QueueItemKey::parse($itemId)['media_id'];
      if ($mediaId > 0) {
        $mediaIds[$mediaId] = $mediaId;
      }
    }

    return array_values($mediaIds);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isProcessable(string|int $itemId, string $queueName): bool {
    $itemId = (string) $itemId;
    if ($this->isGloballyPaused()) {
      return FALSE;
    }
    if (!QueueItemKey::isValid($itemId) || $this->isPaused($itemId)) {
      return FALSE;
    }

    $itemIds = $this->getQueueItemIds($queueName);
    if ($itemIds === [] || !in_array($itemId, $itemIds, TRUE)) {
      return FALSE;
    }

    // O(n) check: verify no other non-paused item has higher scheduling
    // priority than the current item. This avoids the full O(n log n) sort
    // that getRunnableItemIds() performs.
    $metadata = $this->loadMetadata($itemIds);
    $order = $metadata['order'];
    $priorities = $metadata['priorities'];
    $paused = $metadata['paused'];

    $queueIndex = [];
    foreach ($itemIds as $index => $id) {
      $queueIndex[$id] = $index;
    }

    $myOrder = $order[$itemId] ?? PHP_INT_MAX;
    $myPriority = max(0, min(2, (int) ($priorities[$itemId] ?? 0)));
    $myQueuePos = $queueIndex[$itemId] ?? PHP_INT_MAX;

    foreach ($itemIds as $otherId) {
      if ($otherId === $itemId) {
        continue;
      }
      if (($paused[$otherId] ?? FALSE) === TRUE) {
        continue;
      }

      $otherOrder = $order[$otherId] ?? PHP_INT_MAX;
      if ($otherOrder < $myOrder) {
        return FALSE;
      }
      if ($otherOrder > $myOrder) {
        continue;
      }

      $otherPriority = max(0, min(2, (int) ($priorities[$otherId] ?? 0)));
      if ($otherPriority > $myPriority) {
        return FALSE;
      }
      if ($otherPriority < $myPriority) {
        continue;
      }

      if (($queueIndex[$otherId] ?? PHP_INT_MAX) < $myQueuePos) {
        return FALSE;
      }
    }

    return TRUE;
  }

  /**
   * Returns queue item IDs currently present in the target queue.
   *
   * @param string $queueName
   *   Queue plugin ID / queue name.
   *
   * @return array<int, string>
   *   Queue item IDs in native claim order.
   */
  private function getQueueItemIds(string $queueName): array {
    if ($queueName === '') {
      return [];
    }

    return $this->queueManager->getQueuedItemIdsForQueue($queueName);
  }

  /**
   * Loads the current scheduling metadata from storage.
   *
   * Queue metadata now lives on the tracked queue rows so the Queue Editor,
   * queue workers, and CLI commands observe the same persisted scheduling
   * state without a UI-owned sidecar store.
   *
   * @param array<int, string> $itemIds
   *   Optional queue item IDs to limit the lookup to.
   *
   * @return array{order: array<string, int>, priorities: array<string, int>, paused: array<string, bool>}
   *   Sanitized metadata keyed by queue item ID.
   */
  private function loadMetadata(array $itemIds = []): array {
    return $this->queueMetadataRepository->loadSchedulingMaps($itemIds);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function pauseQueue(): void {
    $this->getMetadataStore()->set(self::KEY_GLOBAL_PAUSED, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function resumeQueue(): void {
    $this->getMetadataStore()->delete(self::KEY_GLOBAL_PAUSED);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isGloballyPaused(): bool {
    return (bool) $this->getMetadataStore()->get(self::KEY_GLOBAL_PAUSED, FALSE);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function resetCache(): void {
    // Live metadata is re-read on every scheduling decision. The method is
    // retained as a backwards-compatible no-op for internal callers.
  }

  /**
   * Returns queue editor metadata store.
   */
  private function getMetadataStore(): KeyValueStoreInterface {
    return $this->keyValueFactory->get(self::METADATA_COLLECTION);
  }

}
