<?php

namespace Drupal\acquia_contenthub_subscriber;

use Drupal\acquia_contenthub\AcquiaContentHubEntityTrackerInterface;
use Drupal\acquia_contenthub\AcquiaContentHubStatusMetricsTrait;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;

/**
 * Subscriber Tracker database table methods.
 */
class SubscriberTracker implements AcquiaContentHubEntityTrackerInterface {

  use AcquiaContentHubStatusMetricsTrait;

  /**
   * The status used to indicate that a tracked item has been imported.
   */
  const IMPORTED = 'imported';

  /**
   * The status used to indicate an entity should no longer receive updates.
   */
  const AUTO_UPDATE_DISABLED = 'auto_update_disabled';

  /**
   * The name of the tracking table.
   */
  const IMPORT_TRACKING_TABLE = 'acquia_contenthub_subscriber_import_tracking';

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

  /**
   * Checks if a particular entity uuid is tracked.
   *
   * @param mixed $uuids
   *   The uuid of an entity.
   *
   * @return bool
   *   Whether the entity is tracked in the subscriber tables.
   */
  public function isTracked($uuids) {
    if (!is_array($uuids)) {
      $uuids = [$uuids];
    }
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't');
    $query->fields('t', ['entity_type', 'entity_id', 'entity_uuid']);
    $query->condition('entity_uuid', $uuids, 'IN');
    $results = $query->execute()->fetchAll();
    if (array_diff($uuids, array_column($results, 'entity_uuid'))) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Gets a list of uuids that are not yet tracked.
   *
   * @param string[] $uuids
   *   List of UUID strings.
   *
   * @return array
   *   The list of UUIDs of untracked entities.
   */
  public function getUntracked(array $uuids) {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t');
    $query->condition('t.entity_uuid', $uuids, 'IN');
    $query->condition('t.entity_id', NULL, 'IS NOT NULL');
    $results = $query->execute();

    $uuids = array_combine($uuids, $uuids);

    foreach ($results as $result) {
      // @todo this should be compared against a known hash.
      if ($result->hash) {
        unset($uuids[$result->entity_uuid]);
      }
    }

    return array_values($uuids);
  }

  /**
   * Add tracking for an entity in a self::IMPORTED 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.
   * @param string|null $remote_uuid
   *   A remote uuid if relevant.
   *
   * @throws \Exception
   */
  public function track(EntityInterface $entity, string $hash, ?string $remote_uuid = NULL) {
    $values = [
      'entity_uuid' => $remote_uuid ?? $entity->uuid(),
      'entity_type' => $entity->getEntityTypeId(),
      'entity_id' => $entity->id(),
      'last_imported' => date('c'),
    ];
    $this->insertOrUpdate($values, self::IMPORTED, $hash);
  }

  /**
   * Add tracking for an entity in a self::QUEUED state.
   *
   * @param string $uuid
   *   The entity uuid to enqueue.
   *
   * @throws \Exception
   */
  public function queue(string $uuid): void {
    $values = [
      'entity_uuid' => $uuid,
    ];
    $this->insertOrUpdate($values, self::QUEUED);
  }

  /**
   * Determines if an entity will be inserted or updated with a status.
   *
   * @param array $values
   *   The array of values to insert.
   * @param string $status
   *   The status of the tracking.
   * @param string $hash
   *   The hash string.
   *
   * @return \Drupal\Core\Database\StatementInterface|int|null
   *   Database statement.
   *
   * @throws \Exception
   */
  protected function insertOrUpdate(array $values, $status, $hash = "") {
    if (empty($values['entity_uuid'])) {
      throw new \Exception("Cannot track a subscription without an entity uuid.");
    }
    $values['status'] = $status;
    if ($hash) {
      $values['hash'] = $hash;
    }
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['first_imported']);
    $query->condition('entity_uuid', $values['entity_uuid']);
    $results = $query->execute()->fetchObject();
    // If we've previously tracked this thing, set its created date.
    if ($results) {
      $query = $this->database->update(self::IMPORT_TRACKING_TABLE)
        ->fields($values);
      $query->condition('entity_uuid', $values['entity_uuid']);
      return $query->execute();
    }
    $values['first_imported'] = date('c');
    return $this->database->insert(self::IMPORT_TRACKING_TABLE)
      ->fields($values)
      ->execute();
  }

  /**
   * Add tracking for multiple entities in a self::QUEUED state.
   *
   * @param array $values
   *   The fields value to be updated. The following format should apply:
   *
   * @code
   *    $values = [
   *      ['entity_uuid' => 'some-uuid','hash' => 'some-hash'],
   *      ['entity_uuid' => 'some-uuid','hash' => 'some-hash'],
   *   ];
   * @endcode
   *
   * @throws \Exception
   */
  public function queueMultiple(array $values): void {
    if (empty($values)) {
      throw new \Exception("Can not insert/update empty values");
    }
    $values_chunk = array_chunk($values, 100);
    foreach ($values_chunk as $chunk) {
      $this->insertOrUpdateMultiple($chunk, self::QUEUED);
    }
  }

  /**
   * Determines if entities will be inserted or updated with a status.
   *
   * @param array $values
   *   The fields value to be updated.
   * @param string $status
   *   The status of the tracking.
   *
   * @code
   *     $values = [
   *       ['entity_uuid' => 'some-uuid','hash' => 'some-hash'],
   *       ['entity_uuid' => 'some-uuid','hash' => 'some-hash'],
   *    ];
   * @endcode
   *
   * @throws \Exception
   */
  protected function insertOrUpdateMultiple(array $values, string $status): void {
    $uuids = array_column($values, 'entity_uuid');

    $select_query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')->fields('t', ['entity_uuid']);
    $select_query->condition('entity_uuid', $uuids, 'IN');
    $existing_uuids = $select_query->execute()->fetchAll(\PDO::FETCH_COLUMN);

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

    if (!empty($new_uuids)) {
      $insert_query = $this->database->insert(self::IMPORT_TRACKING_TABLE);
      $insert_query->fields(['status', 'hash', 'entity_uuid', 'first_imported']);
      foreach ($new_uuids as $uuid) {
        $row['entity_uuid'] = $uuid;
        $row['status'] = $status;
        $key = array_search($uuid, array_column($values, 'entity_uuid'));
        $row['hash'] = $values[$key]['hash'];
        $row['first_imported'] = date('c');
        $insert_query->values($row);
      }
      $insert_query->execute();
    }

    if (!empty($existing_uuids)) {
      $update_query = $this->database->update(self::IMPORT_TRACKING_TABLE);
      foreach ($existing_uuids as $uuid) {
        $row['entity_uuid'] = $uuid;
        $row['status'] = $status;
        $key = array_search($uuid, array_column($values, 'entity_uuid'));
        $row['hash'] = $values[$key]['hash'];
        $update_query->fields($row);
        $update_query->condition('entity_uuid', $uuid);
      }
      $update_query->execute();
    }

  }

  /**
   * Get a local entity by its remote uuid if hashes match.
   *
   * @param string $uuid
   *   The remote uuid of the entity to load.
   * @param string|null $hash
   *   The expected hash of the entity (Currently unused).
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The loaded entity if found
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function getEntityByRemoteIdAndHash($uuid, $hash = NULL) {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['entity_type', 'entity_id']);
    $query->condition('entity_uuid', $uuid);

    if (NULL !== $hash) {
      $query->condition('hash', $hash);
    }

    $result = $query->execute()->fetchObject();

    if ($result && $result->entity_type && $result->entity_id) {
      return \Drupal::entityTypeManager()
        ->getStorage($result->entity_type)
        ->load($result->entity_id);
    }
    return NULL;
  }

  /**
   * Provides local entities by its remote uuid if hashes match.
   *
   * @param array $entities
   *   Array of remote uuid and expected hash of the entities.
   *
   * @return array
   *   Array of loaded entity if found.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function getEntitiesByRemoteIdAndHash(array $entities): array {
    $matching_entities = [];
    $chunks = array_chunk($entities, 100, TRUE);
    foreach ($chunks as $values) {
      $with_hash = [];
      $only_uuid = [];
      foreach ($values as $uuid => $hash) {
        if (!$hash) {
          $only_uuid[] = $uuid;
          continue;
        }
        $with_hash[$uuid] = $hash;
      }
      if (empty($with_hash) && empty($only_uuid)) {
        continue;
      }

      $results = $this->getMatchingEntitiesFromImportTrackingTable($with_hash, $only_uuid);
      if (empty($results)) {
        continue;
      }

      $entities_by_type = [];
      foreach ($results as $obj) {
        $entities_by_type[$obj->entity_type][$obj->entity_uuid] = $obj->entity_id;
      }
      $matching_entities = array_merge($matching_entities, $this->loadMultipleEntitiesByType($entities_by_type));
    }
    return $matching_entities;
  }

  /**
   * Loads multiple entities with uuid and entity type.
   *
   * @param array $entities_by_type
   *   Array of uuids and entity type.
   *
   * @return array
   *   Array of entities.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  private function loadMultipleEntitiesByType(array $entities_by_type): array {
    $loaded_entities = [];
    foreach ($entities_by_type as $type => $ids) {
      $res = \Drupal::entityTypeManager()->getStorage($type)->loadMultiple($ids);
      if (!empty($res)) {
        $data = [];
        foreach ($ids as $remote_uuid => $id) {
          if (array_key_exists($id, $res)) {
            $data[$remote_uuid] = $res[$id];
          }
        }
        $loaded_entities = array_merge($data, $loaded_entities);
      }
    }
    return $loaded_entities;
  }

  /**
   * Provides matching entities from import tracking table.
   *
   * @param array $with_hash
   *   Entities uuid with hash value.
   * @param array $only_uuid
   *   Entity uuids.
   *
   * @return array
   *   Matching entities from import tracking table.
   */
  private function getMatchingEntitiesFromImportTrackingTable(array $with_hash, array $only_uuid): array {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['entity_type', 'entity_id', 'entity_uuid']);
    $or = $query->orConditionGroup();
    if (!empty($with_hash)) {
      $and = $query->andConditionGroup();
      $and->condition('entity_uuid', array_keys($with_hash), 'IN');
      $and->condition('hash', array_values($with_hash), 'IN');
      $or->condition($and);
    }

    if (!empty($only_uuid)) {
      $or->condition('entity_uuid', $only_uuid, 'IN');
    }

    return $query->condition($or)->execute()->fetchAll();
  }

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

    $query->execute();
  }

  /**
   * Delete rows based on given uuids array.
   *
   * @param array $uuids
   *   The dynamic uuids array for which tracking should be removed.
   */
  public function deleteMultipleByUuids(array $uuids) : void {
    $query = $this->database->delete(self::IMPORT_TRACKING_TABLE);
    $uuid_chunk = array_chunk($uuids, 100);
    foreach ($uuid_chunk as $chunk) {
      $query->condition('entity_uuid', $chunk, 'IN');
      $query->execute();
    }
  }

  /**
   * Get the current status of an entity by type/id.
   *
   * @param string $type
   *   The type of an entity.
   * @param string $id
   *   The id of an entity.
   *
   * @return string|void
   *   The status string.
   */
  public function getStatusByTypeId(string $type, string $id) {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['status']);
    $query->condition('entity_type', $type);
    $query->condition('entity_id', $id);
    $result = $query->execute()->fetchObject();
    if ($result && $result->status) {
      return $result->status;
    }
  }

  /**
   * Get the current status of a particular uuid.
   *
   * @param string $uuid
   *   The uuid of an entity.
   *
   * @return string|void
   *   The status string.
   */
  public function getStatusByUuid(string $uuid) {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['status']);
    $query->condition('entity_uuid', $uuid);
    $result = $query->execute()->fetchObject();
    if ($result && $result->status) {
      return $result->status;
    }
  }

  /**
   * Get the current statuses for multiple UUIDs.
   *
   * @param array $uuids
   *   Array of entity UUIDs.
   *
   * @return array
   *   Array keyed by UUID with status values.
   */
  public function getStatusesByUuids(array $uuids): array {
    if (empty($uuids)) {
      return [];
    }

    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['entity_uuid', 'status'])
      ->condition('entity_uuid', $uuids, 'IN');

    $results = $query->execute()->fetchAll();

    $statuses = [];
    foreach ($results as $result) {
      $statuses[$result->entity_uuid] = $result->status;
    }

    return $statuses;
  }

  /**
   * Set the status of a particular item by its uuid.
   *
   * @param string $uuid
   *   The uuid of an entity.
   * @param string $status
   *   The status to set.
   *
   * @throws \Exception
   */
  public function setStatusByUuid(string $uuid, string $status) {
    $this->validateStatuses($status);

    if (!$this->isTracked($uuid)) {
      return;
    }
    $query = $this->database->update(self::IMPORT_TRACKING_TABLE);
    $query->fields(['status' => $status]);
    $query->condition('entity_uuid', $uuid);
    $query->execute();
  }

  /**
   * Sets the status for the provided uuids.
   *
   * @param array $uuids
   *   Set of uuids.
   * @param string $status
   *   Status to set.
   *
   * @throws \Exception
   */
  public function setStatusByUuids(array $uuids, string $status): void {
    $this->validateStatuses($status);
    $tracked_uuids = [];
    foreach ($uuids as $uuid) {
      if ($this->isTracked($uuid)) {
        $tracked_uuids[] = $uuid;
      }
    }
    $chunks = array_chunk($tracked_uuids, 100);
    foreach ($chunks as $values) {
      $query = $this->database->update(self::IMPORT_TRACKING_TABLE);
      $query->fields(['status' => $status]);
      $query->condition('entity_uuid', $values, 'IN');
      $query->execute();
    }
  }

  /**
   * Validates the status.
   *
   * @param string $status
   *   Status tobe validated.
   *
   * @throws \Exception
   */
  protected function validateStatuses(string $status): void {
    $acceptable_statuses = [
      $this::AUTO_UPDATE_DISABLED,
      $this::IMPORTED,
      $this::QUEUED,
    ];
    if (!in_array($status, $acceptable_statuses)) {
      throw new \Exception(sprintf("The '%s' status is not valid. Please pass one of the following options: '%s'", $status, implode("', '", $acceptable_statuses)));
    }
  }

  /**
   * Set the queue item of a particular record by its uuid.
   *
   * @param array $uuids
   *   The uuids of entities.
   * @param string $queue_id
   *   The status to set.
   *
   * @throws \Exception
   */
  public function setQueueItemByUuids(array $uuids, string $queue_id) {
    if (!$this->isTracked($uuids)) {
      return;
    }
    $query = $this->database->update(self::IMPORT_TRACKING_TABLE);
    $query->fields(['queue_id' => $queue_id]);
    $query->condition('entity_uuid', $uuids, 'IN');
    $query->execute();
  }

  /**
   * Set the status of a particular item by its entity type and id.
   *
   * @param string $type
   *   The type of an entity.
   * @param string $id
   *   The id of an entity.
   * @param string $status
   *   The status to set.
   *
   * @throws \Exception
   */
  public function setStatusByTypeId(string $type, string $id, string $status) {
    $query = $this->database->select(self::IMPORT_TRACKING_TABLE, 't')
      ->fields('t', ['entity_uuid']);
    $query->condition('entity_type', $type);
    $query->condition('entity_id', $id);
    $result = $query->execute()->fetchObject();
    if (!(bool) $result) {
      return;
    }
    $this->setStatusByUuid($result->entity_uuid, $status);
  }

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

    $query = $this->database
      ->select(self::IMPORT_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);
  }

  /**
   * Delete all the rows in the tracking table.
   */
  public function deleteAll(): void {
    $query = $this->database->delete(self::IMPORT_TRACKING_TABLE);
    $query->execute();
  }

  /**
   * {@inheritDoc}
   */
  public function nullifyHashes(array $statuses = [], array $entity_types = [], array $uuids = []): void {
    $query = $this->database->update(SubscriberTracker::IMPORT_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();
  }

}
