<?php

namespace Drupal\acquia_contenthub_subscriber;

use Acquia\Hmac\Key;
use Drupal\acquia_contenthub\AcquiaContentHubEntityTrackerInterface;
use Drupal\acquia_contenthub\AcquiaContentHubEvents;
use Drupal\acquia_contenthub\AcquiaContentHubRequeueBase;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\Event\HandleWebhookEvent;
use Drupal\acquia_contenthub\Settings\ConnectionDetailsInterface;
use Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\depcalc\Cache\DepcalcCacheBackend;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * The SubscriberRequeuer re-enqueues the entities to import.
 */
class SubscriberRequeuer extends AcquiaContentHubRequeueBase {

  /**
   * Thirty minutes as scroll time window as there can be a lot of entities.
   */
  public const SCROLL_TIME_WINDOW = '30m';

  /**
   * Scroll size.
   */
  public const SCROLL_SIZE = 1000;

  /**
   * Scroll identifier in scroll api response.
   */
  public const SCROLL_IDENTIFIER = '_scroll_id';

  /**
   * Content hub client.
   *
   * @var \Acquia\ContentHubClient\ContentHubClient
   */
  protected $client;

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

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

  /**
   * The subscriber requeuer constructor.
   *
   * @param \Drupal\acquia_contenthub\AcquiaContentHubEntityTrackerInterface $subscriber_tracker
   *   The entity tracker.
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $client_factory
   *   The client factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The event dispatcher.
   * @param \Drupal\depcalc\Cache\DepcalcCacheBackend $depcalc_cache
   *   Instance of depclac cache backend.
   * @param \Psr\Log\LoggerInterface $logger_channel
   *   The logger instance.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface $ach_configurations
   *   Content Hub configurations.
   */
  public function __construct(AcquiaContentHubEntityTrackerInterface $subscriber_tracker, ClientFactory $client_factory, EventDispatcherInterface $dispatcher, DepcalcCacheBackend $depcalc_cache, LoggerInterface $logger_channel, EntityTypeManagerInterface $entityTypeManager, ContentHubConfigurationInterface $ach_configurations) {
    parent::__construct($depcalc_cache, $logger_channel, $entityTypeManager, $subscriber_tracker);

    $this->dispatcher = $dispatcher;
    $this->client = $client_factory->getClient();
    $this->achConfigurations = $ach_configurations;
  }

  /**
   * {@inheritDoc}
   */
  public function reQueue(string $entity_type, string $bundle, string $uuid, bool $only_queued_entities, bool $use_tracking_table, string $reason = 'manual', string $action = 'entity_create', bool $service = FALSE): array {
    $this->validateParameters(
      $entity_type,
      $bundle,
      $uuid,
      $only_queued_entities,
      $use_tracking_table
    );

    $ch_connection = $this->achConfigurations->getConnectionDetails();
    if ($service) {
      if ($ch_connection->getSyndicationMode() === ConnectionDetailsInterface::SYNDICATION_MODE_PULL) {
        return $this->reQueueItemsInServiceQueue($entity_type, $bundle, $uuid, $only_queued_entities, $use_tracking_table, $reason, $action);
      }
      else {
        $this->logger->error('Service flag should be used in Pull Syndication mode only. Current mode: @mode', [
          '@mode' => $ch_connection->getSyndicationMode(),
        ]);
        throw new \Exception('Service flag should be used in Pull Syndication mode only.');
      }
    }

    if ($only_queued_entities || $use_tracking_table) {
      if ($only_queued_entities) {
        if (!empty($entity_type) || !empty($bundle)) {
          throw new \Exception('Cannot pass entity type or entity bundle name while passing only_queued_entities flag.');
        }
        // We are going to re-queue all the queued entities.
        $queued_entities = $this->entityTracker->listTrackedEntities(AcquiaContentHubEntityTrackerInterface::QUEUED);
        $this->enqueueOnlyQueuedEntities($queued_entities);
      }
      else {
        // We are going to re-queue ALL entities.
        $tracked_entities = $this->entityTracker->listTrackedEntities([
          SubscriberTracker::IMPORTED,
          AcquiaContentHubEntityTrackerInterface::QUEUED,
        ], $entity_type
        );
        $this->enqueueEntities($tracked_entities, $entity_type, $bundle);
      }
      return [];
    }

    // Build elastic query to re-enqueue the entities.
    $query = $this->getQuery($entity_type, $bundle, $uuid);
    if (!empty($query)) {
      $uuids = $this->getEntityUuids($query);
      $this->addToImportQueue($uuids);
    }
    return [];
  }

  /**
   * Creates new items in Service Queue.
   *
   * @param string $entity_type
   *   The entity type.
   * @param string $bundle
   *   The bundle name.
   * @param string $uuid
   *   The entity uuid.
   * @param bool $only_queued_entities
   *   True value if only queued entities to be re-enqueued.
   * @param bool $use_tracking_table
   *   True value if only tracking table entities to be re-enqueued.
   * @param string $reason
   *   Reason for requeuing entities, default value is 'manual'.
   * @param string $action
   *   Action for requeuing entities, default value is 'entity_create'.
   *
   * @return array
   *   Array of failed items from the response.
   *
   * @throws \Exception
   */
  private function reQueueItemsInServiceQueue(string $entity_type, string $bundle, string $uuid, bool $only_queued_entities, bool $use_tracking_table, string $reason, string $action): array {

    if ($only_queued_entities || $use_tracking_table) {
      if ($only_queued_entities) {
        if (!empty($entity_type) || !empty($bundle)) {
          throw new \Exception('Cannot pass entity type or entity bundle name while passing only_queued_entities flag.');
        }
        // We are going to re-queue all the queued entities.
        $entities = $this->entityTracker->listTrackedEntities(AcquiaContentHubEntityTrackerInterface::QUEUED);
      }
      else {
        // We are going to re-queue ALL entities.
        $entities = $this->entityTracker->listTrackedEntities([
          SubscriberTracker::IMPORTED,
          AcquiaContentHubEntityTrackerInterface::QUEUED,
        ], $entity_type
        );
      }
      $uuids = array_column($entities, 'entity_uuid');
      return $this->sendItemsToServiceQueue($uuids, $reason, $action);
    }

    // Build elastic query to re-enqueue the entities.
    $query = $this->getQuery($entity_type, $bundle, $uuid);
    if (!empty($query)) {
      $uuids = array_column($this->getEntityUuids($query), 'uuid');
      return $this->sendItemsToServiceQueue($uuids, $reason, $action);
    }

    return [];
  }

  /**
   * Creates new items in Service Queue.
   *
   * @param array $uuids
   *   Array of uuids to send to create items in Service Queue.
   * @param string $reason
   *   Reason for requeuing entities, default value is 'manual'.
   * @param string $action
   *   Action for requeuing entities, default value is 'entity_create'.
   *
   * @return array
   *   Array of failed items from the response.
   *
   * @throws \Exception
   */
  private function sendItemsToServiceQueue(array $uuids, string $reason, string $action): array {
    if (empty($uuids)) {
      return [];
    }
    $items = [];
    $ch_connection = $this->achConfigurations->getConnectionDetails();
    $client_uuid = $ch_connection->getClientUuid();
    foreach ($uuids as $uuid) {
      $items[] = [
        'client_uuid' => $client_uuid,
        'entity_uuid' => $uuid,
        'payload' => [
          'reason' => $reason,
          'action' => $action,
          'type' => 'drupal8_content_entity',
        ],
      ];
    }
    try {
      $data = $this->client->createQueueItems($items);
      return $data['failed'] ?? [];
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to create queue items: ' . $e->getMessage());
      throw $e;
    }
  }

  /**
   * Checks whether scroll response reach the final page.
   *
   * @param array $scroll_response
   *   The response from startScroll or continueScroll.
   *
   * @return bool
   *   If this is the final return TRUE, FALSE otherwise.
   */
  private function isFinalPage(array $scroll_response): bool {
    return empty($scroll_response['hits']['hits']) || !isset($scroll_response[self::SCROLL_IDENTIFIER]);
  }

  /**
   * Extract entity uuids from search entity response.
   *
   * @param array $entities
   *   The response from searchentity.
   *
   * @return array
   *   Returns entity UUIDs from scroll response.
   */
  private function extractEntityUuids(array $entities): array {
    $uuids = [];

    foreach ($entities['hits']['hits'] as $entity) {
      $uuid = $entity['_source']['data']['uuid'];
      $type = $entity['_source']['data']['type'];
      if ($type === 'client') {
        continue;
      }
      $uuids[$uuid] = [
        'uuid' => $uuid,
        'type' => $type,
      ];
    }

    return $uuids;
  }

  /**
   * Calculates elastic search query based on input options.
   *
   * @param string $entity_type
   *   The entity type.
   * @param string $bundle
   *   The bundle name.
   * @param string $uuid
   *   The entity uuid.
   *
   * @return array
   *   Returns elastic search query.
   */
  private function getQuery(string $entity_type, string $bundle, string $uuid): array {
    $fields = [];
    if ($entity_type) {
      $fields[] = ['data.attributes.entity_type.value.und' => $entity_type];
    }

    if ($uuid) {
      $fields[] = ['data.uuid' => $uuid];
    }

    if ($bundle) {
      $fields[] = ['data.attributes.bundle.value.und' => $bundle];
    }

    $query = [];
    foreach ($fields as $field) {
      $query['query']['bool']['must'][] = ['match' => $field];
    }

    return $query;
  }

  /**
   * Extracts entity uuids based on the input query.
   *
   * @param array $query
   *   The elastic search query.
   *
   * @return array
   *   Returns array of entity uuids.
   *
   * @throws \Exception
   */
  private function getEntityUuids(array $query): array {
    $uuids = [];

    $matched_data = $this->client->startScroll(self::SCROLL_TIME_WINDOW, self::SCROLL_SIZE, $query);
    $is_final_page = $this->isFinalPage($matched_data);

    $uuids = array_merge($uuids, $this->extractEntityUuids($matched_data));

    $previous_scroll_id = $scroll_id = $matched_data[self::SCROLL_IDENTIFIER] ?? [];
    try {
      while (!$is_final_page) {
        $matched_data = $this->client->continueScroll($scroll_id, self::SCROLL_TIME_WINDOW);
        $uuids = array_merge($uuids, $this->extractEntityUuids($matched_data));

        $scroll_id = $matched_data[self::SCROLL_IDENTIFIER];
        if (!empty($scroll_id)) {
          $previous_scroll_id = $scroll_id;
        }
        $is_final_page = $this->isFinalPage($matched_data);
      }
    } finally {
      if (!empty($previous_scroll_id)) {
        $this->client->cancelScroll($previous_scroll_id);
      }
    }

    return $uuids;
  }

  /**
   * Adds entity uuids to import queue.
   *
   * @param array $uuids
   *   Array of entity uuids.
   */
  private function addToImportQueue(array $uuids): void {
    $settings = $this->client->getSettings();
    $this->clearCacheAndNullifyHashes();
    $chunks = array_chunk($uuids, 50);
    foreach ($chunks as $chunk) {
      $payload = [
        'status' => 'successful',
        'crud' => 'update',
        'assets' => $chunk,
        'initiator' => 'some-initiator',
      ];

      $request = new Request([], [], [], [], [], [], json_encode($payload));
      $key = new Key($settings->getApiKey(), $settings->getSecretKey());
      $event = new HandleWebhookEvent($request, $payload, $key, $this->client);
      $this->dispatcher->dispatch($event, AcquiaContentHubEvents::HANDLE_WEBHOOK);
    }
  }

  /**
   * Enqueues only queued entities.
   *
   * @param array $queued_entities
   *   Array of queued entities.
   */
  protected function enqueueOnlyQueuedEntities(array $queued_entities) {
    $uuids = [];

    foreach ($queued_entities as $queued_entity) {
      $uuids[$queued_entity['entity_uuid']] = [
        'uuid' => $queued_entity['entity_uuid'],
        // Since type does not matter, hard coding the value.
        'type' => 'drupal8_content_entity',
      ];
    }
    $this->addToImportQueue($uuids);
  }

  /**
   * {@inheritDoc}
   */
  protected function enqueueEntities(array $tracked_entities, ?string $entity_type, ?string $bundle): void {
    $uuids = [];
    foreach ($tracked_entities as $tracked_entity) {
      if (!$this->validEntity($tracked_entity, $entity_type, $bundle)) {
        continue;
      }

      $uuids[$tracked_entity['entity_uuid']] = [
        'uuid' => $tracked_entity['entity_uuid'],
        // Since type does not matter, hard coding the value.
        'type' => 'drupal8_content_entity',
      ];
    }
    $this->addToImportQueue($uuids);
    $this->logger->info('Processed {count} entities for import.', [
      'count' => count($uuids),
    ]);
  }

}
