<?php

namespace Drupal\acquia_contenthub_publisher\Plugin\QueueWorker;

use Acquia\ContentHubClient\CDFDocument;
use Acquia\ContentHubClient\Syndication\SyndicationStatus;
use Drupal\acquia_contenthub\AcquiaContentHubEvents;
use Drupal\acquia_contenthub\Client\CdfMetricsManager;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\ContentHubCommonActions;
use Drupal\acquia_contenthub\Event\PrunePublishCdfEntitiesEvent;
use Drupal\acquia_contenthub\Event\Queue\QueueItemProcessFinishedEvent;
use Drupal\acquia_contenthub\Libs\InterestList\InterestListTrait;
use Drupal\acquia_contenthub\Libs\Logging\ContentHubLoggerInterface;
use Drupal\acquia_contenthub\Libs\Traits\ResponseCheckerTrait;
use Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface;
use Drupal\acquia_contenthub_publisher\Libs\ConfigSyndicationSettingsInterface;
use Drupal\acquia_contenthub_publisher\PublisherTracker;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\Queue\SuspendQueueException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Acquia ContentHub queue worker.
 *
 * @QueueWorker(
 *   id = "acquia_contenthub_publish_export",
 *   title = "Queue Worker to export entities to contenthub."
 * )
 */
class ContentHubExportQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  use InterestListTrait;
  use ResponseCheckerTrait;

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

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The common contenthub actions object.
   *
   * @var \Drupal\acquia_contenthub\ContentHubCommonActions
   */
  protected $common;

  /**
   * The published entity tracker.
   *
   * @var \Drupal\acquia_contenthub_publisher\PublisherTracker
   */
  protected $tracker;

  /**
   * CH configurations.
   *
   * @var \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface
   */
  protected ContentHubConfigurationInterface $achConfigurations;

  /**
   * The Content Hub Client.
   *
   * @var \Acquia\ContentHubClient\ContentHubClient|bool
   */
  protected $client;

  /**
   * Content Hub logger.
   *
   * @var \Drupal\acquia_contenthub\Libs\Logging\ContentHubLoggerInterface
   */
  protected ContentHubLoggerInterface $chLogger;

  /**
   * Content Hub config entities syndication setting.
   *
   * @var \Drupal\acquia_contenthub_publisher\Libs\ConfigSyndicationSettingsInterface
   */
  protected ConfigSyndicationSettingsInterface $configSyndicationSetting;

  /**
   * Cdf Metrics Manager.
   *
   * @var \Drupal\acquia_contenthub\Client\CdfMetricsManager
   */
  protected CdfMetricsManager $cdfMetricsManager;

  /**
   * ContentHubExportQueueWorker constructor.
   *
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\acquia_contenthub\ContentHubCommonActions $common
   *   The common contenthub actions object.
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $factory
   *   The client factory.
   * @param \Drupal\acquia_contenthub_publisher\PublisherTracker $tracker
   *   The published entity tracker.
   *   The event dispatcher.
   * @param \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface $ch_configurations
   *   CH configurations.
   * @param \Drupal\acquia_contenthub\Libs\Logging\ContentHubLoggerInterface $ch_logger
   *   Content Hub logger service.
   * @param \Drupal\acquia_contenthub_publisher\Libs\ConfigSyndicationSettingsInterface $config_syndication_settings
   *   Content Hub config entities syndication setting.
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin id.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Drupal\acquia_contenthub\Client\CdfMetricsManager $cdf_metrics_manager
   *   Cdf metrics manager.
   *
   * @throws \Exception
   */
  public function __construct(EventDispatcherInterface $dispatcher, EntityTypeManagerInterface $entity_type_manager, ContentHubCommonActions $common, ClientFactory $factory, PublisherTracker $tracker, ContentHubConfigurationInterface $ch_configurations, ContentHubLoggerInterface $ch_logger, ConfigSyndicationSettingsInterface $config_syndication_settings, array $configuration, $plugin_id, $plugin_definition, CdfMetricsManager $cdf_metrics_manager) {
    $this->dispatcher = $dispatcher;
    $this->common = $common;
    if (!empty($this->common->getUpdateDbStatus())) {
      throw new \Exception("Site has pending database updates. Apply these updates before exporting content.");
    }
    $this->entityTypeManager = $entity_type_manager;
    $this->client = $factory->getClient();
    $this->tracker = $tracker;
    $this->achConfigurations = $ch_configurations;
    $this->chLogger = $ch_logger;
    $this->configSyndicationSetting = $config_syndication_settings;
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->cdfMetricsManager = $cdf_metrics_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $container->get('event_dispatcher'),
      $container->get('entity_type.manager'),
      $container->get('acquia_contenthub_common_actions'),
      $container->get('acquia_contenthub.client.factory'),
      $container->get('acquia_contenthub_publisher.tracker'),
      $container->get('acquia_contenthub.configuration'),
      $container->get('acquia_contenthub_publisher.ch_logger'),
      $container->get('acquia_contenthub_publisher.config_syndication.settings'),
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('acquia_contenthub.cdf_metrics_manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    $result = $this->doProcess($data->type, $data->uuid, $data->calculate_dependencies ?? TRUE);
    $event = new QueueItemProcessFinishedEvent($this->getPluginId(), [$data->uuid], $result);
    $this->dispatcher->dispatch($event, AcquiaContentHubEvents::QUEUE_ITEM_PROCESS_FINISHED);
    return $result;
  }

  /**
   * Executes the actual processing.
   *
   * This method return values will be used within ContentHubExportQueue.
   * Different return values will log different messages and will indicate
   * different behaviours.
   *
   * @param string $type
   *   The entity's type.
   * @param string $uuid
   *   The uuid of the entity.
   * @param bool $calculate_dependencies
   *   Whether the entity needs calculation or not.
   *
   * @return bool|int
   *   FALSE => Error processing entities, queue item not deleted.
   *   0 => No processing done, queue item is not deleted
   *   TRUE or return int which is not 0 => Entities processed and queue item
   *   will be deleted.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function doProcess(string $type, string $uuid, bool $calculate_dependencies) {
    // When the platfrom interceptor intercepts the client, it returns NULL.
    // Problem is that it is not identifyable if the it's service under mainte-
    // nance.
    if (!$this->client) {
      $this->chLogger->getChannel()->error('Acquia Content Hub client cannot be initialized.');
      throw new SuspendQueueException('Acquia Content Hub client cannot be initialized.');
    }
    $storage = $this->entityTypeManager->getStorage($type);
    $entity = $storage->loadByProperties(['uuid' => $uuid]);

    // Entity missing so remove it from the tracker and stop processing.
    if (!$entity) {
      $this->tracker->delete('entity_uuid', $uuid);
      $this->chLogger->logWarning(
        'Entity ("{entity_type}", "{uuid}") being exported no longer exists on the publisher. Deleting item from the publisher queue.',
        [
          'entity_type' => $type,
          'uuid' => $uuid,
        ]
      );
      return TRUE;
    }

    /** @var \Drupal\Core\Entity\EntityInterface $entity */
    $entity = reset($entity);
    $entities = [];
    try {
      $output = $this->common->getEntityCdf($entity, $entities, TRUE, $calculate_dependencies, $this->configSyndicationSetting->isConfigSyndicationDisabled());
    }
    catch (\Exception $ex) {
      $this->chLogger->logError('Entity: {entity_type} - {uuid}. Error: {error}',
        [
          'entity_type' => $entity->getEntityType()->getBundleLabel(),
          'uuid' => $entity->uuid(),
          'error' => $ex->getMessage(),
        ]
      );

      throw $ex;
    }

    $document = new CDFDocument(...$output);

    // $output is a cdf of ALLLLLL entities that support the entity we wanted
    // to export. What happens if some of the entities which support the entity
    // we want to export were imported initially? We should dispatch an event
    // to look at the output and see if there are entries in our subscriber
    // table and then compare the rest against plexus data.
    $event = new PrunePublishCdfEntitiesEvent($this->client, $document, $this->client->getSettings()->getUuid());
    $this->dispatcher->dispatch($event, AcquiaContentHubEvents::PRUNE_PUBLISH_CDF_ENTITIES);
    $output = array_values($event->getDocument()->getEntities());
    if (empty($output)) {
      $this->chLogger->logWarning('You are trying to export an empty CDF. Triggering entity: {entity}, {uuid}',
        [
          'entity' => $type,
          'uuid' => $uuid,
        ]
      );
      return 0;
    }

    $entity_uuids = [];
    foreach ($output as $item) {
      $wrapper = !empty($entities[$item->getUuid()]) ? $entities[$item->getUuid()] : NULL;
      if ($wrapper) {
        $this->tracker->track($wrapper->getEntity(), $wrapper->getHash());
        $this->tracker->nullifyQueueId($item->getUuid());
      }
      $entity_uuids[] = $item->getUuid();
    }
    $exported_entities = implode(', ', $entity_uuids);
    // ContentHub backend determines new or update on the PUT endpoint.
    $response = $this->client->putEntities(...$output);

    $webhook = $this->client->getSettings()->getWebhook('uuid') ?? '';
    if (!Uuid::isValid($webhook)) {
      $this->chLogger->getChannel()->warning(
        sprintf(
          'Site does not have a valid registered webhook and it is required to add entities (%s) to the site\'s interest list in Content Hub.',
          $exported_entities
        )
      );
      return FALSE;
    }

    if (!$this->isSuccessful($response)) {
      if ($this->isMaintenanceError($response)) {
        throw new SuspendQueueException('Service is under maintenance, suspending export queue');
      }

      $event_ref = $this->chLogger->logError(
        'Request to Content Hub "/entities" endpoint returned with status code = {status_code}. Triggering entity: {uuid}.',
        [
          'status_code' => $response->getStatusCode(),
          'uuid' => $uuid,
        ]
      );
      $this->updateInterestList($entity_uuids, $webhook, SyndicationStatus::EXPORT_FAILED, $event_ref);

      return FALSE;
    }

    $this->cdfMetricsManager->sendClientCdfUpdates();
    $this->updateInterestList($entity_uuids, $webhook, SyndicationStatus::EXPORT_SUCCESSFUL);

    return count($output);
  }

  /**
   * The extended interest list to add based on site role.
   *
   * @param array $uuids
   *   The entity uuids to build interest list from.
   * @param string $webhook_uuid
   *   The webhook uuid to register interest items for.
   * @param string $status
   *   The syndication status.
   * @param string|null $event_ref
   *   The id of the event.
   */
  protected function updateInterestList(array $uuids, string $webhook_uuid, string $status, ?string $event_ref = NULL): void {
    $send_update = $this->achConfigurations->getContentHubConfig()->shouldSendContentHubUpdates();
    if (!$send_update) {
      return;
    }
    $interest_list = $this->buildInterestList(
      $uuids,
      $status,
      NULL,
      $event_ref
    );
    $exported_entities = implode(', ', $uuids);
    try {
      $this->client->updateInterestListBySiteRole($webhook_uuid, 'PUBLISHER', $interest_list);

      $this->chLogger->getChannel()
        ->info('The following exported entities have been added to the interest list with status "{syndication_status}" for webhook {webhook}: [{exported_entities}].',
          [
            'webhook' => $webhook_uuid,
            'syndication_status' => $status,
            'exported_entities' => $exported_entities,
          ]
        );
    }
    catch (\Exception $e) {
      $this->chLogger->getChannel()
        ->error('Error adding the following entities to the interest list with status "{syndication_status}" for webhook {webhook}: [{exported_entities}]. Error message: {exception}.',
          [
            'webhook' => $webhook_uuid,
            'syndication_status' => $status,
            'exported_entities' => $exported_entities,
            'exception' => $e->getMessage(),
          ]
        );
    }
  }

}
