<?php

declare(strict_types=1);

namespace Drupal\acquia_dam\Service;

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Utility\Error;

/**
 * Service for managing integration links queue operations.
 */
class IntegrationLinksQueueService {

  use StringTranslationTrait;

  /**
   * Maximum retry attempts for failed queue items.
   */
  private const MAX_RETRY_ATTEMPTS = 3;

  /**
   * Constructs a new IntegrationLinksQueueService.
   *
   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
   *   The queue factory.
   * @param \Drupal\Core\Queue\QueueWorkerManagerInterface $queueWorkerManager
   *   The queue worker manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   */
  public function __construct(
    private readonly QueueFactory $queueFactory,
    private readonly QueueWorkerManagerInterface $queueWorkerManager,
    private readonly LoggerChannelFactoryInterface $loggerFactory,
  ) {}

  /**
   * Process the integration links queue.
   *
   * @param int $limit
   *   Maximum number of queue items to process. 0 = no limit.
   * @param callable|null $progressCallback
   *   Optional callback for progress updates.
   *
   * @return array
   *   Processing results: processed, failed, skipped, remaining, limit_reached.
   */
  public function processQueue(int $limit = 0, ?callable $progressCallback = NULL): array {
    $queue = $this->queueFactory->get('acquia_dam_integration_links');
    $queue_worker = $this->queueWorkerManager->createInstance('acquia_dam_integration_links');

    $initial_count = $queue->numberOfItems();
    $processed = 0;
    $failed = 0;
    $skipped = 0;
    $failed_items = [];
    $limit_reached = FALSE;

    if ($initial_count === 0) {
      return [
        'processed' => 0,
        'failed' => 0,
        'skipped' => 0,
        'remaining' => 0,
        'initial_count' => 0,
        'limit_reached' => FALSE,
      ];
    }

    while ($item = $queue->claimItem()) {
      try {
        // Check if we should continue processing.
        if (!$this->shouldContinueProcessing($processed, $limit)) {
          $queue->releaseItem($item);
          $limit_reached = TRUE;
          break;
        }

        // Check if item is stuck.
        if ($this->isItemStuck($item)) {
          // Release the item back to the queue instead of deleting it.
          $this->queueFactory->get('acquia_dam_integration_links')->releaseItem($item);
          $this->loggerFactory->get('acquia_dam')->warning('Queue item lease expired - released back to queue for retry.', [
            'item_id' => $item->item_id ?? 'unknown',
          ]);
          $skipped++;
          continue;
        }

        // Check if item has failed multiple times.
        $item_id = $item->item_id ?? spl_object_hash($item);
        if (isset($failed_items[$item_id]) && $failed_items[$item_id] >= self::MAX_RETRY_ATTEMPTS) {
          $this->loggerFactory->get('acquia_dam')->warning('Skipping queue item after @attempts failed attempts.', [
            '@attempts' => self::MAX_RETRY_ATTEMPTS,
          ]);
          $queue->deleteItem($item);
          $skipped++;
          continue;
        }

        // Process the item.
        $queue_worker->processItem($item->data);
        $queue->deleteItem($item);
        $processed++;

        // Clear from failed tracking.
        unset($failed_items[$item_id]);

        // Progress callback every 10 items.
        if ($progressCallback && $processed % 10 === 0) {
          $remaining = $queue->numberOfItems();
          $progressCallback($processed, $failed, $skipped, $remaining);
        }

        // Check limit AFTER processing.
        if ($limit > 0 && $processed >= $limit) {
          $limit_reached = TRUE;
          break;
        }
      }
      catch (\Exception $e) {
        $error_context = Error::decodeException($e);
        $this->loggerFactory->get('acquia_dam')->error('Failed to process integration link queue item: @message in @file on line @line', $error_context);

        // Track failed attempts.
        $item_id = $item->item_id ?? spl_object_hash($item);
        $failed_items[$item_id] = ($failed_items[$item_id] ?? 0) + 1;

        // Release item back to queue for retry.
        $queue->releaseItem($item);
        $failed++;
      }
    }

    $remaining = $queue->numberOfItems();

    return [
      'processed' => $processed,
      'failed' => $failed,
      'skipped' => $skipped,
      'remaining' => $remaining,
      'initial_count' => $initial_count,
      'limit_reached' => $limit_reached,
    ];
  }

  /**
   * Get the number of items in the queue.
   *
   * @return int
   *   Number of queue items.
   */
  public function getQueueCount(): int {
    $queue = $this->queueFactory->get('acquia_dam_integration_links');
    return $queue->numberOfItems();
  }

  /**
   * Check if processing should continue.
   *
   * @param int $processed
   *   Number of items processed so far.
   * @param int $limit
   *   Processing limit (0 = no limit).
   *
   * @return bool
   *   TRUE if processing should continue.
   */
  protected function shouldContinueProcessing(int $processed, int $limit): bool {
    return !($limit > 0 && $processed >= $limit);
  }

  /**
   * Check if a queue item is stuck.
   *
   * A queue item is considered stuck if its lease has expired,
   * meaning it was claimed but never released or deleted.
   * This can happen when the DAM token expires or an API call times out,
   * so the item is skipped and released to be retried on the next run.
   *
   * @param object $item
   *   The queue item.
   *
   * @return bool
   *   TRUE if item's lease has expired (stuck).
   */
  protected function isItemStuck(object $item): bool {
    // Check if the item's lease has expired.
    // $item->expire is set when claimItem() is called and represents
    // when the current claim expires (not when item was created).
    if (!isset($item->expire)) {
      return FALSE;
    }

    $current_time = time();

    // If current time is past the expire time, the lease has expired.
    return $current_time > $item->expire;
  }

}
