<?php

declare(strict_types=1);

namespace Drupal\migrate_tools;

use Drupal\Core\Database\Connection;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;

/**
 * Utility functionality for use in migrate_tools.
 *
 * @internal
 *   The migrate tools utility functionality is internal.
 */
class MigrateTools {

  /**
   * Default ID list delimiter.
   */
  public const DEFAULT_ID_LIST_DELIMITER = ':';

  /**
   * Maximum number of source IDs to keep in memory before flushing to database.
   */
  protected const MAX_BUFFERED_SYNC_SOURCE_IDS_ENTRIES = 1000;

  /**
   * Sync Ids buffered in RAM before flushing them to database.
   */
  protected array $bufferedSyncIdsEntries = [];

  /**
   * Array keeping track of migrations being in the Syncing IDs phase.
   *
   * Structure: List of `string => bool` where the key is the migration ID and
   * the value is a boolean indicating whether it is currently syncing.
   */
  protected array $syncingMigrations = [];

  public function __construct(
    public Connection $connection,
  ) {}

  /**
   * Build the list of specific source IDs to import.
   *
   * @param array $options
   *   The migration executable options.
   *
   *   The ID list.
   */
  public static function buildIdList(array $options): array {
    $options += [
      'idlist' => NULL,
      'idlist-delimiter' => self::DEFAULT_ID_LIST_DELIMITER,
    ];
    $id_list = [];
    if (is_scalar($options['idlist'])) {
      $id_list = explode(',', (string) $options['idlist']);
      array_walk($id_list, function (&$value) use ($options): void {
        $value = str_getcsv($value, $options['idlist-delimiter'], escape: '\\');
      });
    }
    return $id_list;
  }

  /**
   * Clears all SyncSourceIds entries from the database, for given migration.
   *
   * @param string $migrationId
   *   Migration ID.
   */
  public function clearSyncSourceIds(string $migrationId): void {
    $query = $this->connection->delete('migrate_tools_sync_source_ids')
      ->condition('migration_id', $migrationId);
    $query->execute();
  }

  /**
   * Adds a SyncSourceIds entry to the database, for given migration.
   *
   * @param \Drupal\migrate\Plugin\MigrationInterface|string $migration
   *   The migration.
   * @param array $sourceIds
   *   A set of SyncSourceIds. Gets serialized to retain its structure.
   *
   * @throws \Exception
   */
  public function addToSyncSourceIds(MigrationInterface|string $migration, array $sourceIds): void {
    if (!$migration instanceof MigrationInterface) {
      // @phpstan-ignore-next-line
      $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration);
      @trigger_error('Passing a migration ID to MigrateTools::addToSyncSourceIds is deprecated in migrate_tools:6.0.6 and is removed in migrate_tools:7.0.0. Instead passing MigrationInterface will be required. See https://www.drupal.org/node/3507073', E_USER_DEPRECATED);
    }
    $sourceIds = $this->prepareSourceIdValues($sourceIds, $migration->getSourcePlugin());
    $this->bufferedSyncIdsEntries[] = [
      'migration_id' => $migration->getPluginId(),
      // Serialize source IDs before saving them to retain their structure.
      'source_ids' => serialize($sourceIds),
    ];
    if (count($this->bufferedSyncIdsEntries) >= static::MAX_BUFFERED_SYNC_SOURCE_IDS_ENTRIES) {
      $this->flushSyncSourceIdsToDatabase();
    }
  }

  /**
   * Ensure all source IDs match the expected type.
   *
   * @param array $rowSourceIds
   *   The source IDs from the current row.
   * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
   *   The migrate source.
   *
   * @return array
   *   The updated source values.
   *
   * @see migrate_tools_migrate_prepare_row()
   * @see https://www.drupal.org/node/3104268
   */
  protected function prepareSourceIdValues(array $rowSourceIds, MigrateSourceInterface $source): array {
    $sourceIds = $source->getIds();
    foreach ($rowSourceIds as $key => $value) {
      if (!array_key_exists($key, $sourceIds)) {
        continue;
      }

      // Cast the source ID as the configured type to avoid rollback
      // and recreations when an exact match is not found.
      if (in_array($sourceIds[$key]['type'], ['string', 'integer'], TRUE)) {
        $rowSourceIds[$key] = $sourceIds[$key]['type'] === 'string' ? (string) $rowSourceIds[$key] : (int) $rowSourceIds[$key];
      }
    }

    return $rowSourceIds;
  }

  /**
   * Flushes any pending SyncSourceIds to the database.
   *
   * @throws \Exception
   */
  protected function flushSyncSourceIdsToDatabase(): void {
    if (empty($this->bufferedSyncIdsEntries)) {
      // Nothing to flush, do nothing.
      return;
    }

    // Batch insert all buffered pending entries.
    $query = $this->connection->insert('migrate_tools_sync_source_ids')
      ->fields(['migration_id', 'source_ids']);
    foreach ($this->bufferedSyncIdsEntries as $entry) {
      $query->values($entry);
    }
    $query->execute();

    // Clear buffered pending entries.
    $this->bufferedSyncIdsEntries = [];
  }

  /**
   * Returns all SyncSourceIds from the database, for given migration.
   *
   * @param string $migrationId
   *   Migration ID.
   *
   * @return array
   *   Ids, structured as they were inserted.
   *
   * @throws \Exception
   */
  public function getSyncSourceIds(string $migrationId): array {
    // Ensure all data was flushed to database before retrieving all of them.
    $this->flushSyncSourceIdsToDatabase();

    // Retrieve all IDs.
    $serializedSourceIds = $this->connection->query(
        'SELECT source_ids FROM {migrate_tools_sync_source_ids} WHERE migration_id = :mid',
        [':mid' => $migrationId],
      )
      ->fetchCol();

    // Unserialize source IDs to restore their structure.
    array_walk($serializedSourceIds, static function (&$entry) {
      $entry = unserialize($entry);
    });

    return $serializedSourceIds;
  }

  /**
   * Sets the syncing state of a migration.
   *
   * @param string $migrationId
   *   Migration ID.
   * @param bool $isSyncing
   *   State to set.
   */
  public function setMigrationSyncingState(string $migrationId, bool $isSyncing): void {
    $this->syncingMigrations[$migrationId] = $isSyncing;
  }

  /**
   * Returns the syncing state of a migration.
   *
   * @param string $migrationId
   *   Migration ID.
   *
   * @return bool
   *   Whether the migration is currently syncing its IDs or not.
   */
  public function isMigrationSyncing(string $migrationId): bool {
    return $this->syncingMigrations[$migrationId] ?? FALSE;
  }

  /**
   * Returns a mapping of log levels to a human-friendly label.
   *
   * @return array
   *   An array of log level labels.
   */
  public static function getLogLevelLabelMapping() {
    return [
      MigrationInterface::MESSAGE_ERROR => t('Error'),
      MigrationInterface::MESSAGE_WARNING => t('Warning'),
      MigrationInterface::MESSAGE_NOTICE => t('Notice'),
      MigrationInterface::MESSAGE_INFORMATIONAL => t('Informational'),
    ];
  }

  /**
   * Returns a mapping of status levels to a human-friendly label.
   *
   * @return array
   *   An array of migration status labels.
   */
  public static function getStatusLevelLabelMapping() {
    return [
      MigrateIdMapInterface::STATUS_IMPORTED => t('Imported'),
      MigrateIdMapInterface::STATUS_NEEDS_UPDATE => t('Pending'),
      MigrateIdMapInterface::STATUS_IGNORED => t('Ignored'),
      MigrateIdMapInterface::STATUS_FAILED => t('Failed'),
    ];
  }

}
