<?php

namespace Drupal\acquia_contenthub_publisher;

use Drupal\acquia_contenthub\AcquiaContentHubEntityTrackerInterface;
use Drupal\acquia_contenthub\AcquiaContentHubStatusMetricsTrait;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Queue\DatabaseQueue;
use Drupal\node\NodeInterface;

/**
 * The publisher tracker table class.
 */
class PublisherTracker implements AcquiaContentHubEntityTrackerInterface {

  use AcquiaContentHubStatusMetricsTrait;

  const EXPORTED = 'exported';

  const CONFIRMED = 'confirmed';

  const READY_TO_QUEUE = 'ready_to_queue';

  /**
   * The name of the tracking table.
   */
  const EXPORT_TRACKING_TABLE = 'acquia_contenthub_publisher_export_tracking';

  /**
   * PublisherTracker constructor.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

  /**
   * Gets the tracking entity for a given uuid.
   *
   * @param string $uuid
   *   The entity uuid.
   *
   * @return \Drupal\Core\Database\StatementInterface|int|null
   *   Database statement
   */
  public function get(string $uuid) {
    $query = $this->database->select(self::EXPORT_TRACKING_TABLE, 't')
      ->fields('t', ['entity_uuid']);
    $query->condition('entity_uuid', $uuid);

    return $query->execute()->fetchObject();
  }

  /**
   * Gets the tracking record for a given uuid.
   *
   * @param string $uuid
   *   The entity uuid.
   *
   * @return \Drupal\Core\Database\StatementInterface|int|null
   *   Database statement
   */
  public function getRecord(string $uuid) {
    $query = $this->database->select(self::EXPORT_TRACKING_TABLE, 't')
      ->fields('t');
    $query->condition('entity_uuid', $uuid);
    return $query->execute()->fetchObject();
  }

  /**
   * Gets the Queue ID for a given uuid.
   *
   * @param string $uuid
   *   The entity uuid.
   * @param bool $tracker_only
   *   TRUE to check only the tracker, FALSE (default) to check the queue table.
   *
   * @return int|null|bool
   *   The Queue ID or FALSE.
   */
  public function getQueueId(string $uuid, bool $tracker_only = FALSE) {
    $query = $this->database->select(self::EXPORT_TRACKING_TABLE, 't')
      ->fields('t', ['queue_id']);
    $query->condition('entity_uuid', $uuid);
    $queue_id = $query->execute()->fetchField();
    if ($tracker_only || empty($queue_id)) {
      return $queue_id;
    }

    // Is the existing item in the Drupal "queue"?
    return $this->database
      ->query('SELECT item_id FROM {' . DatabaseQueue::TABLE_NAME . '} WHERE item_id = :queue_id', [':queue_id' => $queue_id])
      ->fetchField();
  }

  /**
   * Add tracking for an entity in a self::EXPORTED state.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to add tracking.
   * @param string $hash
   *   A sha1 hash of the data attribute for change management.
   *
   * @throws \Exception
   */
  public function track(EntityInterface $entity, string $hash) {
    $this->insertOrUpdate($entity, self::EXPORTED, $hash);
  }

  /**
   * Add tracking for entities in a self::QUEUED state.
   *
   * @param array $entities_data
   *   The entities data to be tracked.
   *
   * @code
   *    $entities_data = [
   *      'some-uuid' => [
   *        'entity' => 'some-entity',
   *        'hash' => 'some-hash',
   *        'queue_id' => 'some-queue-id',
   *      ],
   *   ];
   * @endcode
   *
   * @throws \Exception
   */
  public function queueMultiple(array $entities_data): void {
    if (empty($entities_data)) {
      throw new \Exception("Can not insert/update empty values in export tracking table.");
    }
    $entities_data_chunk = array_chunk($entities_data, 100, TRUE);
    foreach ($entities_data_chunk as $chunk) {
      $this->insertOrUpdateMultiple($chunk, self::QUEUED);
    }
  }

  /**
   * Add tracking for entities in a self::READY_TO_QUEUE state.
   *
   * @param array $entities_data
   *   The entities data to be tracked.
   *
   * @code
   *    $entities_data = [
   *      'some-uuid' => [
   *        'entity' => 'some-entity',
   *        'hash' => 'some-hash',
   *        'queue_id' => 'some-queue-id',
   *      ],
   *   ];
   * @endcode
   *
   * @throws \Exception
   */
  public function readyToQueueMultiple(array $entities_data): void {
    if (empty($entities_data)) {
      throw new \Exception("Can not insert/update empty values in export tracking table.");
    }
    $entities_data_chunk = array_chunk($entities_data, 100, TRUE);
    foreach ($entities_data_chunk as $chunk) {
      $this->insertOrUpdateMultiple($chunk, self::READY_TO_QUEUE);
    }
  }

  /**
   * {@inheritDoc}
   */
  public function delete(string $field_name, string $field_value): void {
    $query = $this->database->delete(self::EXPORT_TRACKING_TABLE);
    $query->condition($field_name, $field_value);
    $query->execute();
  }

  /**
   * Determines if an entity will be inserted or updated with a status.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to add tracking to.
   * @param string $status
   *   The status of the tracking.
   * @param string $hash
   *   A sha1 hash of the data attribute for change management.
   * @param string $queue_id
   *   Queue Id.
   *
   * @return \Drupal\Core\Database\StatementInterface|int|null
   *   Database statement.
   *
   * @throws \Exception
   */
  protected function insertOrUpdate(EntityInterface $entity, string $status, string $hash = '', string $queue_id = '') {
    if ($entity instanceof EntityChangedInterface) {
      $modified = date('c', $entity->getChangedTime());
    }
    else {
      $modified = date('c');
    }

    // If we've previously tracked this thing, set its created date.
    if ($entity instanceof NodeInterface) {
      $created = date('c', $entity->getCreatedTime());
    }
    // Otherwise just mirror the modified date.
    else {
      $created = $modified;
    }

    // Must check because of created field.
    $results = $this->get($entity->uuid());
    // If entity in the table update fields but not all of them.
    if ($results) {
      $values = [
        'status' => $status,
        'modified' => $modified,
        'entity_uuid' => $entity->uuid(),
        'label' => $entity->label() ?: $entity->id(),
      ];
      if ($hash) {
        $values['hash'] = $hash;
      }
      if ($queue_id) {
        $values['queue_id'] = $queue_id;
      }
    }
    else {
      $values = [
        'entity_type' => $entity->getEntityTypeId(),
        'entity_id' => $entity->id(),
        'entity_uuid' => $entity->uuid(),
        'status' => $status,
        'created' => $created,
        'modified' => $modified,
        'hash' => $hash,
        'queue_id' => $queue_id,
        'label' => $entity->label() ?: $entity->id(),
      ];
    }

    return $this->database->merge(self::EXPORT_TRACKING_TABLE)
      ->key('entity_uuid', $entity->uuid())
      ->fields($values)
      ->execute();
  }

  /**
   * Determines if an entity will be inserted or updated with a status.
   *
   * @param array $entities_data
   *   The entities data to track.
   * @param string $status
   *   The status of the tracking.
   *
   * @code
   *    $entities_data = [
   *      'some-uuid' => [
   *        'entity' => 'some-entity',
   *        'hash' => 'some-hash',
   *        'queue_id' => 'some-queue-id',
   *      ],
   *   ];
   * @endcode
   *
   * @throws \Exception
   */
  protected function insertOrUpdateMultiple(array $entities_data, string $status): void {
    $uuids = array_keys($entities_data);
    $select_query = $this->database->select(self::EXPORT_TRACKING_TABLE, 't')->fields('t', [
      'entity_uuid',
      'hash',
      'queue_id',
    ]);
    $select_query->condition('entity_uuid', $uuids, 'IN');
    $existing_hash = $select_query->execute()->fetchAllKeyed(0, 1);
    $existing_queue_id = $select_query->execute()->fetchAllKeyed(0, 2);
    $existing_uuids = array_keys($existing_hash);

    $new_uuids = array_diff($uuids, $existing_uuids);

    if (!empty($new_uuids)) {
      $insert_query = $this->database->insert(self::EXPORT_TRACKING_TABLE);
      $insert_query->fields([
        'entity_type',
        'entity_id',
        'entity_uuid',
        'status',
        'created',
        'modified',
        'hash',
        'queue_id',
        'label',
      ]);
      foreach ($new_uuids as $uuid) {
        $row = [
          'entity_type' => $entities_data[$uuid]['entity']->getEntityTypeId(),
          'entity_id' => $entities_data[$uuid]['entity']->id(),
          'entity_uuid' => $entities_data[$uuid]['entity']->uuid(),
          'status' => $status,
          'created' => $this->getCreatedTime($entities_data[$uuid]['entity']),
          'modified' => $this->getModifiedTime($entities_data[$uuid]['entity']),
          'hash' => $entities_data[$uuid]['hash'],
          'queue_id' => $entities_data[$uuid]['queue_id'],
          'label' => $entities_data[$uuid]['entity']->label() ?: $entities_data[$uuid]['entity']->id(),
        ];
        $insert_query->values($row);
      }
      $insert_query->execute();
    }

    if (!empty($existing_uuids)) {
      $update_query = $this->database->upsert(self::EXPORT_TRACKING_TABLE);
      $update_query->key('entity_uuid');
      $update_query->fields([
        'status',
        'modified',
        'entity_uuid',
        'label',
        'hash',
        'queue_id',
      ]);
      foreach ($existing_uuids as $uuid) {
        $row = [
          'status' => $status,
          'modified' => $this->getModifiedTime($entities_data[$uuid]['entity']),
          'entity_uuid' => $uuid,
          'label' => $entities_data[$uuid]['entity']->label() ?: $entities_data[$uuid]['entity']->id(),
        ];
        $row['hash'] = $entities_data[$uuid]['hash'] ?? $existing_hash[$uuid];
        $row['queue_id'] = $entities_data[$uuid]['queue_id'] ?? $existing_queue_id[$uuid];

        $update_query->values($row);
      }
      $update_query->execute();
    }
  }

  /**
   * Provides created time of an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return false|string
   *   Created date.
   */
  protected function getCreatedTime(EntityInterface $entity) {
    if ($entity instanceof NodeInterface) {
      return date('c', $entity->getCreatedTime());
    }
    return $this->getModifiedTime($entity);
  }

  /**
   * Provides modified time of an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return false|string
   *   Modified time.
   */
  protected function getModifiedTime(EntityInterface $entity) {
    if ($entity instanceof EntityChangedInterface) {
      return date('c', $entity->getChangedTime());
    }
    return date('c');
  }

  /**
   * Updates the Label of given entity in Export Tracker table.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to be updated.
   */
  public function updateEntityLabel(EntityInterface $entity): void {
    if (!$entity->label() || !$entity->uuid()) {
      return;
    }
    $this->database->update(self::EXPORT_TRACKING_TABLE)
      ->fields(['label' => $entity->label()])
      ->condition('entity_uuid', $entity->uuid(), '=')
      ->execute();
  }

  /**
   * Set the queue item of a particular record by its uuid.
   *
   * @param string $uuid
   *   The uuid of an entity.
   * @param string $queue_id
   *   Queue ID.
   *
   * @throws \Exception
   */
  public function setQueueItemByUuid(string $uuid, string $queue_id) {
    if (!$this->isTracked($uuid)) {
      return;
    }
    $query = $this->database->update(self::EXPORT_TRACKING_TABLE);
    $query->fields(['queue_id' => $queue_id]);
    $query->condition('entity_uuid', $uuid);
    $query->execute();
  }

  /**
   * Checks if a particular entity uuid is tracked.
   *
   * @param string $uuid
   *   The uuid of an entity.
   *
   * @return bool
   *   Whether the entity is tracked in the subscriber table.
   */
  public function isTracked(string $uuid) {
    $query = $this->database->select(self::EXPORT_TRACKING_TABLE, 't');
    $query->fields('t', ['entity_type', 'entity_id']);
    $query->condition('entity_uuid', $uuid);

    return (bool) $query->execute()->fetchObject();
  }

  /**
   * Nullify queue_id when entities lose their queued state.
   *
   * @param string $uuid
   *   The uuid of an entity.
   */
  public function nullifyQueueId(string $uuid) {
    $query = $this->database->update(self::EXPORT_TRACKING_TABLE);
    $query->fields(['queue_id' => '']);
    $query->condition('entity_uuid', $uuid);
    $query->execute();
  }

  /**
   * {@inheritDoc}
   */
  public function listTrackedEntities($status, string $entity_type_id = ''): array {
    if (!is_array($status)) {
      $status = [$status];
    }

    $query = $this->database
      ->select(self::EXPORT_TRACKING_TABLE, 'ci')
      ->fields('ci')
      ->condition('status', $status, 'IN');

    if (!empty($entity_type_id)) {
      $query = $query->condition('entity_type', $entity_type_id);
    }

    return $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
  }

  /**
   * {@inheritDoc}
   */
  public function nullifyHashes(array $statuses = [], array $entity_types = [], array $uuids = []): void {
    $query = $this->database->update(PublisherTracker::EXPORT_TRACKING_TABLE);
    $query->fields(['hash' => '']);
    if (!empty($statuses)) {
      $query->condition('status', $statuses, 'IN');
    }
    if (!empty($entity_types)) {
      $query->condition('entity_type', $entity_types, 'IN');
    }
    if (!empty($uuids)) {
      $query->condition('entity_uuid', $uuids, 'IN');
    }
    $query->execute();
  }

  /**
   * Function to return status in an array.
   *
   * @return array
   *   Status array.
   */
  public static function getStatus(): array {
    return [
      self::READY_TO_QUEUE => self::READY_TO_QUEUE,
      self::QUEUED => self::QUEUED,
      self::EXPORTED => self::EXPORTED,
      self::CONFIRMED => self::CONFIRMED,
    ];
  }

}
