<?php

namespace Drupal\acquia_contenthub\Libs\ServiceQueue;

use Drupal\acquia_contenthub\AcquiaContentHubEvents;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\Event\ServiceQueue\ServiceQueueItemsFailedEvent;
use Drupal\acquia_contenthub\Event\ServiceQueue\ServiceQueueItemsProcessingFinishedEvent;
use Drupal\acquia_contenthub\Libs\Common\EnsureContentHubClientTrait;
use Drupal\acquia_contenthub\Libs\Traits\ResponseCheckerTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Handles service queue operations for Content Hub pull syndication.
 *
 * @internal
 * @package Drupal\acquia_contenthub\Libs\ServiceQueue
 */
class ServiceQueueHandler {

  use DependencySerializationTrait;
  use EnsureContentHubClientTrait;
  use ResponseCheckerTrait;
  use StringTranslationTrait;

  /**
   * The maximum number of items to be fetched from CH service.
   */
  public const LIMIT = 50;

  /**
   * The visibility timeout for fetched items.
   */
  public const VISIBILITY_TIMEOUT = '24h';

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $dispatcher;

  /**
   * The acquia_contenthub logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * Collection of registered queue item actions.
   *
   * @var \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItemActionInterface[]
   */
  protected array $actions = [];

  /**
   * Constructs a ServiceQueueHandler object.
   *
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $client_factory
   *   The Content Hub client factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The event dispatcher.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger channel.
   */
  public function __construct(ClientFactory $client_factory, EventDispatcherInterface $dispatcher, LoggerInterface $logger) {
    $this->clientFactory = $client_factory;
    $this->dispatcher = $dispatcher;
    $this->logger = $logger;
  }

  /**
   * Adds a queue item action to the collection.
   *
   * This method is called by the compiler pass to register tagged services.
   *
   * @param \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItemActionInterface $action
   *   The action to add.
   */
  public function addAction(QueueItemActionInterface $action): void {
    $this->actions[] = $action;
  }

  /**
   * Processes queue items.
   *
   * @param array $queue_items
   *   Queue items as arrays.
   *
   * @throws \Exception
   */
  public function processQueueItems(array $queue_items): void {
    $processed_items = [];
    $failed_items = [];
    $queue_item_actions = [];

    foreach ($queue_items as $queue_item_data) {
      $queue_item = QueueItem::fromArray($queue_item_data);
      $queue_item_action = $this->getActionForQueueItem($queue_item);

      if (!$queue_item_action) {
        $action = $queue_item->getAction();
        $failed_items[$queue_item->getEntityUuid()]['reason'] = sprintf('Could not process queue item; associated handler not found for "%s" action.', $action);
        $failed_items[$queue_item->getEntityUuid()]['queue_item'] = $queue_item;
        continue;
      }

      $action_type = $queue_item->getAction();
      $queue_item_actions[$action_type]['items'][] = $queue_item;
      $queue_item_actions[$action_type]['handler'] = $queue_item_action;
    }

    foreach ($queue_item_actions as $action_data) {
      try {
        $action_data['handler']->execute($action_data['items']);
        $processed_items = array_merge($processed_items, $action_data['items']);
      }
      catch (\Exception $e) {
        foreach ($action_data['items'] as $queue_item) {
          $failed_items[$queue_item->getEntityUuid()]['reason'] = sprintf('Exception during processing: %s', $e->getMessage());
          $failed_items[$queue_item->getEntityUuid()]['queue_item'] = $queue_item;
        }
      }
    }

    if (!empty($processed_items)) {
      $this->handleProcessedQueueItems($processed_items);
    }

    if (!empty($failed_items)) {
      $this->handleFailedQueueItems($failed_items);
    }

    $this->confirmQueueItems($processed_items, $failed_items);
  }

  /**
   * Gets the appropriate action strategy for a queue item.
   *
   * @param \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItem $queue_item
   *   The queue item to process.
   *
   * @return \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItemActionInterface|null
   *   The action strategy or NULL if no suitable strategy is found.
   */
  protected function getActionForQueueItem(QueueItem $queue_item): ?QueueItemActionInterface {
    foreach ($this->actions as $action) {
      if ($action->canHandle($queue_item)) {
        return $action;
      }
    }

    return NULL;
  }

  /**
   * Handles successfully processed queue items.
   *
   * @param \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItem[] $queue_items
   *   Successfully processed queue items.
   *
   * @throws \Exception
   */
  public function handleProcessedQueueItems(array $queue_items): ServiceQueueItemsProcessingFinishedEvent {
    $event = new ServiceQueueItemsProcessingFinishedEvent($queue_items, $this->getClient());
    $this->dispatcher->dispatch($event, AcquiaContentHubEvents::SERVICE_QUEUE_ITEMS_PROCESSING_FINISHED);
    return $event;
  }

  /**
   * Handles failed queue items.
   *
   * @param array $failed_queue_items
   *   Failed queue items keyed by entity UUID with values containing:
   *   - 'queue_item': \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItem
   *   - 'reason': string describing the failure reason.
   *
   * @throws \Exception
   */
  public function handleFailedQueueItems(array $failed_queue_items): ServiceQueueItemsFailedEvent {
    $event = new ServiceQueueItemsFailedEvent($failed_queue_items, $this->getClient());
    $this->dispatcher->dispatch($event, AcquiaContentHubEvents::SERVICE_QUEUE_ITEMS_FAILED);
    return $event;
  }

  /**
   * Confirms queue items status with the Content Hub service.
   *
   * @param \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItem[] $processed_items
   *   Successfully processed queue items.
   * @param array $failed_items
   *   Failed queue items keyed by entity UUID with values containing:
   *   - 'queue_item': \Drupal\acquia_contenthub\Libs\ServiceQueue\QueueItem
   *   - 'reason': string describing the failure reason.
   *   Example structure: ['uuid1' => ['queue_item' => QueueItem, 'reason' =>
   *   'error message'], ...].
   *
   * @throws \Exception
   */
  protected function confirmQueueItems(array $processed_items, array $failed_items): void {
    $processed_uuids = [];
    foreach ($processed_items as $item) {
      $processed_uuids[] = $item->getEntityUuid();
    }

    $failed_uuids = array_keys($failed_items);

    $client = $this->getClient();
    $response = $client->confirmProcessedQueueItems($processed_uuids, $failed_uuids);

    if (!$this->isResponseSuccessful($response)) {
      $error_message = $response['error']['message'] ?? 'Unknown error occurred';
      $this->logger->error('Failed to confirm queue items status: @error', [
        '@error' => $error_message,
      ]);
      return;
    }

    if (!empty($processed_uuids)) {
      $this->logger->info('Processed queue items: @count items (@uuids)', [
        '@count' => count($processed_uuids),
        '@uuids' => implode(', ', $processed_uuids),
      ]);
    }

    if (!empty($failed_items)) {
      foreach ($failed_items as $uuid => $failure_data) {
        $reason = $failure_data['reason'] ?? 'Unknown reason';
        $this->logger->error('Failed queue item @uuid: @reason', [
          '@uuid' => $uuid,
          '@reason' => $reason,
        ]);
      }
    }
  }

}
