<?php

namespace Drupal\commerce\Config;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\ExtensionInstallStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Default implementation of the ConfigUpdaterInterface.
 */
class ConfigUpdater implements ConfigUpdaterInterface {

  use StringTranslationTrait;

  /**
   * The extension config storage for config/install config items.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $extensionConfigStorage;

  /**
   * The extension config storage for config/optional config items.
   *
   * @var \Drupal\Core\Config\ExtensionInstallStorage
   */
  protected $extensionOptionalConfigStorage;

  /**
   * List of current config entity types, keyed by prefix.
   *
   * @var string[]
   */
  protected $typesByPrefix = [];

  /**
   * Constructs a new ConfigUpdater object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Config\StorageInterface $activeConfigStorage
   *   The active config storage.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param string|false|null $install_profile
   *   The name of the currently active installation profile.
 */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected StorageInterface $activeConfigStorage,
    protected ConfigFactoryInterface $configFactory,
    $install_profile,
  ) {
    $this->extensionConfigStorage = new ExtensionInstallStorage($activeConfigStorage, InstallStorage::CONFIG_INSTALL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $install_profile);
    $this->extensionOptionalConfigStorage = new ExtensionInstallStorage($activeConfigStorage, InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $install_profile);

    foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
      if ($definition->entityClassImplements(ConfigEntityInterface::class)) {
        /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $definition */
        $prefix = $definition->getConfigPrefix();
        $this->typesByPrefix[$prefix] = $entity_type;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function import(array $config_names) {
    $succeeded = [];
    $failed = [];
    foreach ($config_names as $config_name) {
      $config_data = $this->loadFromExtension($config_name);
      if (!$config_data) {
        $failed[$config_name] = $this->t('@config does not exist in extension storage', ['@config' => $config_name]);
        continue;
      }
      if ($this->loadFromActive($config_name)) {
        $failed[$config_name] = $this->t('@config already exists, use revert to update', ['@config' => $config_name]);
        continue;
      }

      $config_type = $this->getConfigType($config_name);
      if ($config_type == 'system.simple') {
        $this->configFactory->getEditable($config_name)->setData($config_data)->save();
      }
      else {
        /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
        $entity_storage = $this->entityTypeManager->getStorage($config_type);
        $entity = $entity_storage->createFromStorageRecord($config_data);
        $entity->save();
      }

      $succeeded[$config_name] = $this->t('@config was successfully imported', ['@config' => $config_name]);
    }

    return new ConfigUpdateResult($succeeded, $failed);
  }

  /**
   * {@inheritdoc}
   */
  public function revert(array $config_names, $skip_modified = TRUE) {
    $succeeded = [];
    $failed = [];
    foreach ($config_names as $config_name) {
      $config_data = $this->loadFromExtension($config_name);
      if (!$config_data) {
        $failed[$config_name] = $this->t('@config does not exist in extension storage', ['@config' => $config_name]);
        continue;
      }
      $active_config_data = $this->loadFromActive($config_name);
      if (!$active_config_data) {
        $succeeded[$config_name] = $this->t('Skipped: @config does not exist in active storage', ['@config' => $config_name]);
        continue;
      }
      if ($this->isModified($active_config_data) && $skip_modified) {
        $succeeded[$config_name] = $this->t('Skipped: @config was not reverted because it was modified by the user', ['@config' => $config_name]);
        continue;
      }

      $config_type = $this->getConfigType($config_name);
      if ($config_type == 'system.simple') {
        $this->configFactory->getEditable($config_name)->setData($config_data)->save();
      }
      else {
        /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
        $entity_type = $this->entityTypeManager->getDefinition($config_type);
        $id = substr($config_name, strlen($entity_type->getConfigPrefix()) + 1);
        /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
        $entity_storage = $this->entityTypeManager->getStorage($config_type);
        /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
        $entity = $entity_storage->load($id);
        // The UUID must remain unchanged between updates.
        $uuid = $entity->uuid();
        $entity = $entity_storage->updateFromStorageRecord($entity, $config_data);
        $entity->set('uuid', $uuid);
        $entity->save();
      }

      $succeeded[$config_name] = $this->t('@config was successfully reverted', ['@config' => $config_name]);
    }

    return new ConfigUpdateResult($succeeded, $failed);
  }

  /**
   * {@inheritdoc}
   */
  public function delete(array $config_names) {
    $succeeded = [];
    $failed = [];
    foreach ($config_names as $config_name) {
      if (!$this->loadFromActive($config_name)) {
        $succeeded[$config_name] = $this->t('Skipped: @config does not exist in active storage', ['@config' => $config_name]);
        continue;
      }

      $config_type = $this->getConfigType($config_name);
      if ($config_type == 'system.simple') {
        $this->configFactory->getEditable($config_name)->delete();
      }
      else {
        /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
        $entity_type = $this->entityTypeManager->getDefinition($config_type);
        $id = substr($config_name, strlen($entity_type->getConfigPrefix()) + 1);
        /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
        $entity_storage = $this->entityTypeManager->getStorage($config_type);
        $entity = $entity_storage->load($id);
        $entity_storage->delete([$entity]);
      }

      $succeeded[$config_name] = $this->t('@config was successfully deleted', ['@config' => $config_name]);
    }

    return new ConfigUpdateResult($succeeded, $failed);
  }

  /**
   * {@inheritdoc}
   */
  public function loadFromActive($config_name) {
    return $this->activeConfigStorage->read($config_name);
  }

  /**
   * {@inheritdoc}
   */
  public function loadFromExtension($config_name) {
    $data = $this->extensionConfigStorage->read($config_name);
    if (!$data) {
      $data = $this->extensionOptionalConfigStorage->read($config_name);
    }
    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function isModified(array $config) {
    $original_hash = $config['_core']['default_config_hash'];
    // Create a new hash based on current values.
    unset($config['uuid']);
    unset($config['_core']);
    $current_hash = Crypt::hashBase64(serialize($config));

    return $original_hash !== $current_hash;
  }

  /**
   * Gets the config type for a given config object.
   *
   * @param string $config_name
   *   Name of the config object.
   *
   * @return string
   *   Name of the config type. Either 'system.simple' or an entity type ID.
   */
  protected function getConfigType($config_name) {
    foreach ($this->typesByPrefix as $prefix => $config_type) {
      if (strpos($config_name, $prefix) === 0) {
        return $config_type;
      }
    }

    return NULL;
  }

}
