<?php

namespace Drupal\acquia_contenthub\Commands;

use Acquia\ContentHubClient\Syndication\SyndicationEvents;
use Drupal\acquia_contenthub\Libs\Logging\ContentHubEventLogger;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\Queue\SuspendQueueException;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Validator\Exception\MissingOptionsException;

/**
 * Base class for Queue Run Command.
 */
abstract class AcquiaContentHubQueueCommands extends DrushCommands {

  /**
   * Event logger.
   *
   * @var \Drupal\acquia_contenthub\Libs\Logging\ContentHubEventLogger
   */
  protected $eventLogger;

  /**
   * CH logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $chLogger;

  /**
   * Event log queue process.
   */
  public const EVENT_LOG_OBJECT_TYPE = 'queue_process';

  /**
   * Queue worker.
   *
   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface
   */
  protected $queueWorker;

  /**
   * Queue factory.
   *
   * @var \Drupal\Core\Queue\QueueFactory
   */
  protected $queueFactory;

  /**
   * AcquiaContentHubPublisherExportQueueCommands constructor.
   *
   * @param \Drupal\Core\Queue\QueueWorkerManagerInterface $worker_manager
   *   QueueWorker Manager.
   * @param \Drupal\Core\Queue\QueueFactory $queue_factory
   *   Queue Factory.
   * @param \Drupal\acquia_contenthub\Libs\Logging\ContentHubEventLogger $event_logger
   *   CH Event logger.
   * @param \Psr\Log\LoggerInterface $logger
   *   Drupal logger channel.
   */
  public function __construct(QueueWorkerManagerInterface $worker_manager, QueueFactory $queue_factory, ContentHubEventLogger $event_logger, LoggerInterface $logger) {
    $this->queueWorker = $worker_manager;
    $this->queueFactory = $queue_factory;
    $this->eventLogger = $event_logger;
    $this->chLogger = $logger;
  }

  /**
   * Validate uri option availability.
   */
  public function validateOptions(): void {
    if (empty($this->input->getOption('uri'))) {
      throw new MissingOptionsException('Uri option not passed, it is necessary for running Content Hub queues.', []);
    }
  }

  /**
   * Log queue processing in watchdog and event logs.
   *
   * @param string $message
   *   Log message.
   * @param string $event_name
   *   Event name.
   */
  protected function logQueueProcess(string $message, string $event_name): void {
    $this->chLogger->info($message);
    $this->eventLogger->logEvent(
      SyndicationEvents::SEVERITY_INFO,
      $message,
      self::EVENT_LOG_OBJECT_TYPE,
      $event_name
    );
  }

  /**
   * Runs queue and logs the process.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function runQueue(): void {
    $this->validateOptions();
    $start_formatted_time = DrupalDateTime::createFromTimestamp(time());
    $queue_name = $this->getQueueName();
    $queue = $this->getQueue($queue_name);
    if (!$queue->numberOfItems()) {
      $this->logger()->notice(dt('No items in local queue.'));
      return;
    }
    $queue_process = $this->getQueueProcessName();
    $items_limit = $this->getItemslimit();
    $start_message = $queue_process . ' process started for ' . $items_limit . ' items at ' . $start_formatted_time . '.';
    $this->logQueueProcess($start_message, $queue_name);
    $this->processQueueItems($queue_name, $items_limit);
    $end_formatted_time = DrupalDateTime::createFromTimestamp(time());
    $finish_message = $queue_process . ' process finished for ' . $items_limit . ' items at ' . $end_formatted_time . ' in ' . $this->getFormattedTime($start_formatted_time, $end_formatted_time) . '.';
    $this->logQueueProcess($finish_message, $queue_name);
  }

  /**
   * Calculate the number of items allowed to run the queue.
   *
   * @return int
   *   The number of items allowed to run the queue.
   */
  protected function getItemslimit(): int {
    $queue_name = $this->getQueueName();
    $no_items = $this->getQueue($queue_name)->numberOfItems();
    $items_limit = (int) $this->input()->getOption('items-limit') ?: $no_items;
    $items_limit = ($items_limit <= $no_items) ? $items_limit : $no_items;
    return $items_limit;
  }

  /**
   * Processes queue items.
   *
   * @param string $queue_name
   *   Queue name.
   * @param int $items_limit
   *   The maximum number of items allowed to run the queue.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function processQueueItems(string $queue_name, int $items_limit): void {
    $queue_worker = $this->queueWorker->createInstance($queue_name);
    $queue = $this->getQueue($queue_name);
    $symfony_progressbar = $this->initializeProgress($items_limit);
    $symfony_progressbar->start();
    $start = microtime(TRUE);
    $time_limit = (int) $this->input()->getOption('time-limit');
    $end = time() + $time_limit;
    $remaining = $time_limit;
    $count = 0;
    while ((!$time_limit || $remaining > 0) && ($count < $items_limit)) {
      /** @var object $item */
      $item = $queue->claimItem();
      $symfony_progressbar->advance();
      if (!$item) {
        break;
      }
      try {
        $this->logQueueProcess($this->getItemProcessingMessage($item->data), $queue_name);
        $queue_worker->processItem($item->data);
        $queue->deleteItem($item);
      }
      catch (SuspendQueueException $e) {
        $this->logger->error($e->getMessage());
        $queue->releaseItem($item);
        throw new \Exception($e->getMessage(), $e->getCode(), $e);
      }
      catch (\Exception $exception) {
        $queue->releaseItem($item);
      }
      $remaining = $end - time();
      $count++;
    }
    $elapsed = microtime(TRUE) - $start;
    $symfony_progressbar->finish();
    $this->logger()->success(dt('Processed @count items in @elapsed sec.',
      ['@count' => $count, '@elapsed' => round($elapsed, 2)]));
  }

  /**
   * Initializes the progress bar.
   *
   * @param int $source_count
   *   Maximum count of progress bar.
   *
   * @return \Symfony\Component\Console\Helper\ProgressBar
   *   The ProgressBar object.
   */
  protected function initializeProgress(int $source_count): ProgressBar {
    return new ProgressBar($this->output, $source_count);
  }

  /**
   * Get queue object.
   *
   * @param string $queue_name
   *   Queue name.
   *
   * @return \Drupal\Core\Queue\QueueInterface
   *   Queue object.
   */
  protected function getQueue(string $queue_name): QueueInterface {
    return $this->queueFactory->get($queue_name);
  }

  /**
   * Creates processing message for queue item.
   *
   * @param mixed $data
   *   Data object from queue.
   *
   * @return string
   *   Queue item processing message.
   */
  abstract protected function getItemProcessingMessage($data): string;

  /**
   * Returns CH queue name.
   *
   * @return string
   *   CH queue name.
   */
  abstract protected function getQueueName(): string;

  /**
   * Returns CH queue process name.
   *
   * @return string
   *   CH queue process name.
   */
  abstract protected function getQueueProcessName(): string;

  /**
   * Returns formatted time difference.
   *
   * @param \Drupal\Core\Datetime\DrupalDateTime $start_time
   *   Start time formatted.
   * @param \Drupal\Core\Datetime\DrupalDateTime $end_time
   *   End time formatted.
   *
   * @return string
   *   Formatted time difference.
   */
  protected function getFormattedTime(DrupalDateTime $start_time, DrupalDateTime $end_time): string {
    return $start_time->diff($end_time)->format('%H hours %i minutes %s seconds');
  }

}
