<?php

namespace Drupal\straker_translate;

use Drupal\config_translation\ConfigEntityMapper;
use Drupal\config_translation\ConfigFieldMapper;
use Drupal\config_translation\ConfigMapperManagerInterface;
use Drupal\config_translation\ConfigNamesMapper;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\straker_translate\Entity\StrakerTranslateConfigMetadata;
use Drupal\straker_translate\Exception\StrakerTranslateApiException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentArchivedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentLockedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentNotFoundException;
use Drupal\straker_translate\Exception\StrakerTranslatePaymentRequiredException;
use Drupal\straker_translate\Exception\StrakerTranslateProcessedWordsLimitException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentAlreadyUploaded;

/**
 * Service for managing Straker Translate configuration translations.
 */
class StrakerTranslateConfigTranslationService implements StrakerTranslateConfigTranslationServiceInterface {

  /**
   * @var \Drupal\straker_translate\StrakerTranslateInterface
   */
  protected $straker_translate;

  /**
   * The Straker Translate configuration service.
   *
   * @var \Drupal\straker_translate\StrakerTranslateConfigurationServiceInterface
   */
  protected $straker_translateConfiguration;

  /**
   * The language-locale mapper.
   *
   * @var \Drupal\straker_translate\LanguageLocaleMapperInterface
   */
  protected $languageLocaleMapper;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The language manager.
   *
   * * @var \Drupal\language\ConfigurableLanguageManager //\Drupal\Core\Language\LanguageManager.
   */
  protected $languageManager;

  /**
   * The configuration mapper manager.
   *
   * * @var \Drupal\config_translation\ConfigMapperManagerInterface.
   */
  protected $configMapperManager;

  /**
   * A array of configuration mapper instances.
   *
   * @var \Drupal\config_translation\ConfigMapperInterface[]
   */
  protected $mappers;

  /**
   * Constructs a new StrakerTranslateConfigTranslationService object.
   *
   * @param \Drupal\straker_translate\StrakerTranslateInterface $straker_translate
   *   An straker_translate object.
   * @param \Drupal\straker_translate\LanguageLocaleMapperInterface $language_locale_mapper
   *   The language-locale mapper.
   * @param \Drupal\straker_translate\StrakerTranslateConfigurationServiceInterface $straker_translate_configuration
   *   The Straker Translate configuration service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   An entity manager object.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
   *   The configuration mapper manager.
   */
  public function __construct(StrakerTranslateInterface $straker_translate, LanguageLocaleMapperInterface $language_locale_mapper, StrakerTranslateConfigurationServiceInterface $straker_translate_configuration, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ConfigMapperManagerInterface $mapper_manager) {
    $this->straker_translate = $straker_translate;
    $this->languageLocaleMapper = $language_locale_mapper;
    $this->straker_translateConfiguration = $straker_translate_configuration;
    $this->entityTypeManager = $entity_type_manager;
    $this->languageManager = $language_manager;
    $this->configMapperManager = $mapper_manager;
    $this->mappers = $mapper_manager->getMappers();
  }

  /**
   * {@inheritDoc}
   */
  public function getEnabledConfigTypes() {
    $enabled_types = [];
    foreach ($this->mappers as $mapper) {
      if ($mapper instanceof ConfigEntityMapper) {
        $enabled = $this->isEnabled($mapper->getPluginId());
        if ($enabled) {
          $enabled_types[] = $mapper->getPluginId();
        }
      }
    }
    return $enabled_types;
  }

  /**
   * {@inheritDoc}
   */
  public function isEnabled($plugin_id) {
    $config = \Drupal::config('straker_translate.settings');
    $key = 'translate.config.' . $plugin_id . '.enabled';
    $result = !!$config->get($key);
    return $result;
  }

  /**
   * {@inheritDoc}
   */
  public function setEnabled($plugin_id, $enabled = TRUE) {
    $config = \Drupal::configFactory()->getEditable('straker_translate.settings');
    $key = 'translate.config.' . $plugin_id . '.enabled';
    $config->set($key, $enabled)->save();
  }

  /**
   * {@inheritDoc}
   */
  public function getConfigTranslatableProperties(ConfigNamesMapper $mapper) {
    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
    $typed_config = \Drupal::service('config.typed');

    $properties = [];
    foreach ($mapper->getConfigNames() as $name) {
      $schema = $typed_config->get($name);
      $properties[$name] = $this->getTranslatableProperties($schema, NULL);
    }
    return $properties;
  }

  /**
   * {@inheritDoc}
   */
  public function getTranslatableProperties(TraversableTypedDataInterface $schema, $base_key = NULL): array {
    $properties = [];
    $definition = $schema->getDataDefinition();
    if (isset($definition['form_element_class'])) {
      foreach ($schema as $key => $element) {
        $element_key = isset($base_key) ? "$base_key.$key" : $key;
        $definition = $element->getDataDefinition();

        if ($element instanceof TraversableTypedDataInterface) {
          $properties = array_merge($properties, $this->getTranslatableProperties($element, $element_key));
        }
        else {
          if (isset($definition['form_element_class'])) {
            $properties[] = $element_key;
          }
        }
      }
    }
    return $properties;
  }

  /**
   *
   */
  public function getDocumentId(ConfigEntityInterface $entity) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    return $metadata->getDocumentId();
  }

  /**
   *
   */
  public function setDocumentId(ConfigEntityInterface &$entity, $document_id) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $metadata->setDocumentId($document_id)->save();
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceStatus(ConfigEntityInterface &$entity) {
    $status = StrakerTranslate::STATUS_UNTRACKED;
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $source_status = $metadata->getSourceStatus();
    if ($source_status !== NULL && isset($source_status[$entity->language()->getId()])) {
      $status = $source_status[$entity->language()->getId()];
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function setSourceStatus(ConfigEntityInterface &$entity, $status) {
    $source_language = NULL;
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_source = $metadata->getSourceStatus();
    if ($translation_source) {
      $source_language = key($translation_source);
    }
    if ($source_language == LanguageInterface::LANGCODE_NOT_SPECIFIED || $source_language == NULL) {
      $source_language = $entity->language()->getId();
    }
    $status_value = [$source_language => $status];
    $metadata->setSourceStatus($status_value)->save();
    return $this->getSourceStatus($entity);
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetStatus(ConfigEntityInterface &$entity, $langcode) {
    $status = StrakerTranslate::STATUS_UNTRACKED;
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_status = $metadata->getTargetStatus();
    if (count($translation_status) > 0 && isset($translation_status[$langcode])) {
      $status = $translation_status[$langcode];
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetStatuses(ConfigEntityInterface &$entity) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_status = $metadata->getTargetStatus();
    return $translation_status;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatus(ConfigEntityInterface &$entity, $langcode, $status, $save = TRUE) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_status = $metadata->getTargetStatus();
    $translation_status[$langcode] = $status;
    $metadata->setTargetStatus($translation_status);
    if ($save) {
      $metadata->save();
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatuses(ConfigEntityInterface &$entity, $status): void {
    $target_languages = $this->languageManager->getLanguages();
    $entity_langcode = $entity->language()->getId();

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $entity_langcode && $current_status = $this->getTargetStatus($entity, $langcode)) {
        if ($current_status == StrakerTranslate::STATUS_DISABLED || $current_status == StrakerTranslate::STATUS_CANCELLED && $status == StrakerTranslate::STATUS_READY) {
          continue;
        }
        if ($current_status == $status) {
          continue;
        }
        if ($current_status == StrakerTranslate::STATUS_PENDING && $status == StrakerTranslate::STATUS_CURRENT) {
          continue;
        }
        $this->setTargetStatus($entity, $langcode, $status);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetFileId(ConfigEntityInterface $entity, $langcode) {
    $file_id = NULL;
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_files = $metadata->getTargetFiles();
    if (count($translation_files) > 0 && isset($translation_files[$langcode])) {
      $file_id = $translation_files[$langcode];
    }
    return $file_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetFileId(ConfigEntityInterface &$entity, $langcode, $target_file_id) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $translation_files = $metadata->getTargetFiles();
    $translation_files[$langcode] = $target_file_id;
    $metadata->setTargetFiles($translation_files);
    if ($target_file_id) {
      $metadata->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigTargetFileId(ConfigNamesMapper $mapper, $langcode) {
    $status = [];
    $file_id = NULL;
    $config_names = $mapper->getConfigNames();
    if (!empty($config_names)) {
      $config_name = reset($config_names);
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $translation_files = $metadata->getTargetFiles();
      if (count($translation_files) > 0 && isset($translation_files[$langcode])) {
        $file_id = $translation_files[$langcode];
      }
    }
    return $file_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigTargetFileId(ConfigNamesMapper $mapper, $langcode, $target_file_id) {
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $translation_files = $metadata->getTargetFiles();
      $translation_files[$langcode] = $target_file_id;
      $metadata->setTargetFiles($translation_files);
      if ($target_file_id) {
        $metadata->save();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function setTranslatedTargetStatus(ConfigEntityInterface &$entity, $response) {
    if (empty($response["data"]["source_files"][0]["target_files"])) {
      return;
    }
    $languageMapper = \Drupal::service('straker_translate.language_locale_mapper');
    foreach ($response["data"]["source_files"][0]["target_files"] as $target_file) {
      if ($target_file["status"] === 'translated') {
        $drupal_language = $languageMapper->getConfigurableLanguageForLocale($target_file["language_uuid"]);
        if ($drupal_language) {
          $this->setTargetStatus($entity, $drupal_language->id(), StrakerTranslate::STATUS_READY);
          // Set the file ID too.
          $target_file_id = $target_file['target_file_uuid'];
          $this->setTargetFileId($entity, $drupal_language->id(), $target_file_id);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function setTranslatedConfigTargetStatus(ConfigNamesMapper $mapper, $response) {
    if (empty($response["data"]["source_files"][0]["target_files"])) {
      return;
    }
    $languageMapper = \Drupal::service('straker_translate.language_locale_mapper');
    foreach ($response["data"]["source_files"][0]["target_files"] as $target_file) {
      if ($target_file["status"] === 'translated') {
        $drupal_language = $languageMapper->getConfigurableLanguageForLocale($target_file["language_uuid"]);
        if ($drupal_language) {
          $this->setConfigTargetStatus($mapper, $drupal_language->id(), StrakerTranslate::STATUS_READY);
          // Set the file ID too.
          $target_file_id = $target_file['target_file_uuid'];
          $this->setConfigTargetFileId($mapper, $drupal_language->id(), $target_file_id);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hasEntityChanged(ConfigEntityInterface &$entity): bool {
    $source_data = json_encode($this->getSourceData($entity));
    $hash = md5($source_data);

    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $old_hash = $metadata->getHash();
    if (!$old_hash || strcmp($hash, $old_hash)) {
      $metadata->setHash($hash)->save();
      return TRUE;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function markTranslationsAsDirty(ConfigEntityInterface &$entity): void {
    $target_languages = $this->languageManager->getLanguages();
    $entity_langcode = $entity->language()->getId();

    // Only mark as out of date the current ones.
    $to_change = [
      StrakerTranslate::STATUS_CURRENT,
      // StrakerTranslate::STATUS_PENDING,
      // StrakerTranslate::STATUS_INTERMEDIATE,
      // StrakerTranslate::STATUS_READY,.
    ];

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $entity_langcode && $current_status = $this->getTargetStatus($entity, $langcode)) {
        if (in_array($current_status, $to_change)) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_PENDING);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceData(ConfigEntityInterface $entity) {
    if ($entity->getEntityTypeId() == 'field_config') {
      /** @var \Drupal\Core\Field\FieldConfigBase $entity */
      $id = $entity->getTargetEntityTypeId();
      /** @var \Drupal\config_translation\ConfigEntityMapper $mapper */
      $mapper = clone $this->mappers[$id . '_fields'];
      $mapper->setEntity($entity);
    }
    else {
      /** @var \Drupal\config_translation\ConfigEntityMapper $mapper */
      $mapper = clone $this->mappers[$entity->getEntityTypeId()];
      $mapper->setEntity($entity);
    }
    $data = $this->getConfigSourceData($mapper);
    // For retro-compatibility, if there is only one config name, we plain our
    // data.
    $names = $mapper->getConfigNames();
    if (count($names) == 1) {
      $data = $data[$names[0]] ?? [];
    }
    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceLocale(ConfigEntityInterface &$entity) {
    $locale = NULL;
    $source_language = $entity->language()->getId();
    if (!in_array($source_language, [LanguageInterface::LANGCODE_NOT_SPECIFIED, LanguageInterface::LANGCODE_NOT_APPLICABLE])) {
      $locale = $this->languageLocaleMapper->getLocaleForLangcode($source_language);
    }
    return $locale;
  }

  /**
   * {@inheritdoc}
   */
  public function uploadDocument(ConfigEntityInterface $entity) {
    $profile = $this->straker_translateConfiguration->getConfigEntityProfile($entity);
    // We can reupload if the document is cancelled.
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED) {
      return FALSE;
    }
    if (in_array($this->getSourceStatus($entity), [StrakerTranslate::STATUS_CURRENT, StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING])) {
      throw new StrakerTranslateDocumentAlreadyUploaded('The document has already been uploaded and is being processed or is current.', 0);
      return FALSE;
    }
    $source_data = $this->getSourceData($entity);
    if (empty($source_data)) {
      \Drupal::logger('straker_translate')->warning('No source data found for %entity:', ['%entity' => $entity->label()]);
      return FALSE;
    }
    $extended_name = $entity->id() . ' (config): ' . $entity->label();
    $profile_preference = $profile->getAppendContentTypeToTitle();
    $global_preference = $this->straker_translateConfiguration->getPreference('append_type_to_title');
    switch ($profile_preference) {
      case 'yes':
        $document_name = $extended_name;
        break;

      case 'no':
        $document_name = $entity->label();
        break;

      case 'global_setting':
        $document_name = $global_preference ? $extended_name : $entity->label();
        break;

      default:
        $document_name = $extended_name;
    }

    // Allow other modules to alter the data before is uploaded.
    \Drupal::moduleHandler()->invokeAll('straker_translate_config_entity_document_upload', [&$source_data, &$entity, &$url]);
    $encoded_data = json_encode($source_data);
    $filename = $entity->id() . '_config.json';
    $token_confirmation = 'false';
    try {
      $entity_langcode = $entity->language()->getId();
      $target_languages = $this->languageManager->getLanguages();
      // Filter out the source language and disabled languages.
      $target_languages = array_filter($target_languages, function (LanguageInterface $language) use ($entity_langcode) {
        if ($language->getId() === $entity_langcode) {
          return FALSE;
        }
        $configLanguage = ConfigurableLanguage::load($language->getId());
        return $this->straker_translateConfiguration->isLanguageEnabled($configLanguage);
      });
      // Get langcodes only.
      if (empty($target_languages)) {
        // If there are no target languages, we can return.
        \Drupal::logger('straker_translate')->warning('No target languages configured for %entity', ['%entity' => $entity->id()]);
        return FALSE;
      }
      $target_langcodes = array_keys($target_languages);
      $target_locales = array_map([$this->languageLocaleMapper, 'getLocaleForLangcode'], $target_langcodes);
      $target_locale_uuid = implode(',', $target_locales);
      $document_id = $this->straker_translate->uploadDocument($document_name, $encoded_data, $target_locale_uuid, $token_confirmation, $filename, $profile);
    }
    catch (StrakerTranslatePaymentRequiredException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateProcessedWordsLimitException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateApiException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    if ($document_id) {
      $this->setDocumentId($entity, $document_id);
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_READY);
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_PENDING);
      $this->setLastUploaded($entity, \Drupal::time()->getRequestTime());
      return $document_id;
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function checkSourceStatus(ConfigEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getConfigEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if (!in_array($this->getSourceStatus($entity), [StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING, StrakerTranslate::STATUS_ERROR])) {
      return FALSE;
    }
    $document_id = $this->getDocumentId($entity);
    if ($document_id) {
      try {
        $response = $this->straker_translate->getDocumentStatus($document_id);
      }
      catch (StrakerTranslateDocumentLockedException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateDocumentNotFoundException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateDocumentArchivedException $exception) {
        $this->deleteMetadata($entity);
        throw $exception;
      }
      catch (StrakerTranslatePaymentRequiredException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateApiException $exception) {
        throw $exception;
      }
      if ($response["data"]["status"] == 'COMPLETED') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_CURRENT);
        $this->setTranslatedTargetStatus($entity, $response);
        return TRUE;
      }
      if ($response["data"]["status"] == 'PENDING_TOKEN_PAYMENT' || $response["data"]["status"] == 'IN_PROGRESS') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_PROCESSING);
        $this->setTargetStatuses($entity, StrakerTranslate::STATUS_PENDING);
      }
      elseif ($response["data"]["status"] == 'FAILED' || $response["data"]["status"] == 'UNSUCCESSFUL') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
        $this->setTargetStatuses($entity, StrakerTranslate::STATUS_ERROR);
        throw new StrakerTranslateApiException("The document status is failed. Please check in Straker");
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function downloadDocument(ConfigEntityInterface $entity, $file_id, $langcode) {
    $profile = $this->straker_translateConfiguration->getConfigEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if ($document_id = $this->getDocumentId($entity)) {
      $data = [];
      try {
        $data = $this->straker_translate->downloadDocument($file_id, $langcode);
      }
      catch (StrakerTranslateDocumentNotFoundException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateApiException $exception) {
        \Drupal::logger('straker_translate')->error('Error happened downloading %document_id %langauge: %message', ['%document_id' => $document_id, '%langauge' => $langcode, '%message' => $exception->getMessage()]);
        $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
        return FALSE;
      }
      if ($data) {
        try {
          $this->saveTargetData($entity, $langcode, $data);
          // successfully, we can ensure that the content is current now.
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CURRENT);
        }
        catch (\Exception $exception) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
          \Drupal::logger('straker_translate')->error('Error happened (unknown) saving %document_id %langauge: %message', ['%document_id' => $document_id, '%langauge' => $langcode, '%message' => $exception->getMessage()]);
          return FALSE;
        }
        return TRUE;
      }

    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\straker_translate\Exception\StrakerTranslateDocumentNotFoundException
   */
  public function deleteMetadata(ConfigEntityInterface &$entity) {
    $doc_id = $this->getDocumentId($entity);
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    if (!$metadata->isNew()) {
      $metadata->delete();
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function saveTargetData(ConfigEntityInterface $entity, $langcode, $data) {
    // Allow other modules to alter the translation before it is saved.
    \Drupal::moduleHandler()->invokeAll('straker_translate_config_entity_translation_presave', [&$entity, $langcode, &$data]);

    if ($entity->getEntityTypeId() == 'field_config') {
      /** @var \Drupal\Core\Field\FieldConfigBase $entity */
      $id = $entity->getTargetEntityTypeId();
      /** @var \Drupal\config_translation\ConfigEntityMapper $mapper */
      $mapper = clone ($this->mappers[$id . '_fields']);
      $mapper->setEntity($entity);
    }
    else {
      /** @var \Drupal\config_translation\ConfigEntityMapper $mapper */
      $mapper = clone ($this->mappers[$entity->getEntityTypeId()]);
      $mapper->setEntity($entity);
    }
    // For retro-compatibility, if there is only one config name, we expand our
    // data.
    $names = $mapper->getConfigNames();
    if (count($names) == 1) {
      $expanded[$names[0]] = $data;
    }
    else {
      $expanded = $data;
    }
    $this->saveConfigTargetData($mapper, $langcode, $expanded);
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigDocumentId(ConfigNamesMapper $mapper) {
    $document_id = NULL;
    $metadata = NULL;
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      break;
    }
    if ($metadata) {
      $document_id = $metadata->getDocumentId();
    }
    return $document_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigDocumentId(ConfigNamesMapper $mapper, $document_id) {
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $metadata->setDocumentId($document_id);
      $metadata->save();
    }
    return $document_id;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigSourceLocale(ConfigNamesMapper $mapper) {
    $locale = NULL;
    $source_langcode = $mapper->getLangcode();
    if (!in_array($source_langcode, [LanguageInterface::LANGCODE_NOT_SPECIFIED, LanguageInterface::LANGCODE_NOT_APPLICABLE])) {
      $locale = $this->languageLocaleMapper->getLocaleForLangcode($source_langcode);
    }
    return $locale;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigSourceStatus(ConfigNamesMapper $mapper) {
    $config_names = $mapper->getConfigNames();
    $source_language = $mapper->getLangcode();
    $status = StrakerTranslate::STATUS_UNTRACKED;
    foreach ($config_names as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $source_status = $metadata->getSourceStatus();
      if (count($source_status) > 0 && isset($source_status[$source_language])) {
        $status = $source_status[$source_language];
      }
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigSourceStatus(ConfigNamesMapper $mapper, $status): void {
    $config_names = $mapper->getConfigNames();
    $source_language = $mapper->getLangcode();
    $status_value = [$source_language => $status];
    foreach ($config_names as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $metadata->setSourceStatus($status_value);
      $metadata->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigTargetStatuses(ConfigNamesMapper $mapper) {
    $status = [];
    $config_names = $mapper->getConfigNames();
    if (!empty($config_names)) {
      $config_name = reset($config_names);
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $status = $metadata->getTargetStatus();
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigTargetStatus(ConfigNamesMapper $mapper, $langcode) {
    $status = StrakerTranslate::STATUS_UNTRACKED;
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $translation_status = $metadata->getTargetStatus();
      if (count($translation_status) > 0 && isset($translation_status[$langcode])) {
        $status = $translation_status[$langcode];
      }
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigTargetStatus(ConfigNamesMapper $mapper, $langcode, $status, $save = TRUE): void {
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $target_status = $metadata->getTargetStatus();
      $target_status[$langcode] = $status;
      $metadata->setTargetStatus($target_status);
      $metadata->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigTargetStatuses(ConfigNamesMapper $mapper, $status): void {
    $target_languages = $this->languageManager->getLanguages();
    $entity_langcode = $mapper->getLangcode();

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $entity_langcode && $current_status = $this->getConfigTargetStatus($mapper, $langcode)) {
        if ($current_status == StrakerTranslate::STATUS_DISABLED || $current_status == StrakerTranslate::STATUS_CANCELLED && $status == StrakerTranslate::STATUS_READY) {
          continue;
        }
        if ($current_status == $status) {
          continue;
        }
        if ($current_status == StrakerTranslate::STATUS_PENDING && $status == StrakerTranslate::STATUS_CURRENT) {
          continue;
        }
        $this->setConfigTargetStatus($mapper, $langcode, $status);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigSourceData(ConfigNamesMapper $mapper) {
    $properties = $this->getConfigTranslatableProperties($mapper);
    $values = [];
    foreach ($properties as $config_name => $config_properties) {
      $config = \Drupal::configFactory()->getEditable($config_name);
      foreach ($config_properties as $property) {
        $values[$config_name][$property] = $config->get($property);
      }
    }
    return $values;
  }

  /**
   * {@inheritdoc}
   */
  public function uploadConfig($mapper_id, $force = FALSE) {
    $mapper = $this->mappers[$mapper_id];
    $profile = $this->straker_translateConfiguration->getConfigProfile($mapper_id, TRUE);
    // We can reupload if the document is cancelled.
    if ($profile !== NULL && $profile->id() === StrakerTranslate::PROFILE_DISABLED) {
      return FALSE;
    }
    // Skip status check if force re-upload is requested
    if (!$force && in_array($this->getConfigSourceStatus($mapper), [StrakerTranslate::STATUS_CURRENT, StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING])) {
      throw new StrakerTranslateDocumentAlreadyUploaded('The document has already been uploaded and is being processed or is current.', 0);
      return FALSE;
    }
    // Get the provide providing a default.
    $source_data = $this->getConfigSourceData($mapper);
    if (empty($source_data)) {
      \Drupal::logger('straker_translate')->warning('No source data found for %mapper:', ['%mapper' => $mapper_id]);
      return FALSE;
    }
    // Allow other modules to alter the data before is uploaded.
    $config_names = $mapper->getConfigNames();
    $config_name = reset($config_names);
    \Drupal::moduleHandler()->invokeAll('straker_translate_config_object_document_upload', [&$source_data, $config_name]);

    $source_data = json_encode($source_data);

    $extended_name = $mapper_id . ' (config): ' . $mapper->getTitle();
    $profile_preference = $profile->getAppendContentTypeToTitle();
    $global_preference = $this->straker_translateConfiguration->getPreference('append_type_to_title');
    switch ($profile_preference) {
      case 'yes':
        $document_name = $extended_name;
        break;

      case 'no':
        $document_name = (string) $mapper->getTitle();
        break;

      case 'global_setting':
        $document_name = $global_preference ? $extended_name : (string) $mapper->getTitle();
        break;

      default:
        $document_name = $extended_name;
    }

    $filename = $mapper_id . '_config.json';
    $token_confirmation = 'false';

    try {
      $entity_langcode = $mapper->getLangcode();
      $target_languages = $this->languageManager->getLanguages();

      $target_languages = array_filter($target_languages, function (LanguageInterface $language) use ($entity_langcode) {
        if ($language->getId() === $entity_langcode) {
          return FALSE;
        }
        $configLanguage = ConfigurableLanguage::load($language->getId());
        return $this->straker_translateConfiguration->isLanguageEnabled($configLanguage);
      });
      // Get langcodes only.
      if (empty($target_languages)) {
        // If there are no target languages, we can return.
        \Drupal::logger('straker_translate')->warning('No target languages configured for %mapper:', ['%mapper' => $mapper_id]);
        return FALSE;
      }
      $target_langcodes = array_keys($target_languages);
      $target_locales = array_map([$this->languageLocaleMapper, 'getLocaleForLangcode'], $target_langcodes);
      $target_locale_uuid = implode(',', $target_locales);
      $document_id = $this->straker_translate->uploadDocument($document_name, $source_data, $target_locale_uuid, $token_confirmation, $filename, $profile);
    }
    catch (StrakerTranslatePaymentRequiredException $exception) {
      $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateProcessedWordsLimitException $exception) {
      $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateApiException $exception) {
      $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    if ($document_id) {
      $this->setConfigDocumentId($mapper, $document_id);
      $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_READY);
      $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_PENDING);
      $this->setConfigLastUploaded($mapper, \Drupal::time()->getRequestTime());
      return $document_id;
    }
    if ($this->getConfigSourceStatus($mapper) == StrakerTranslate::STATUS_DISABLED) {
      $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function checkConfigSourceStatus($mapper_id) {
    $mapper = $this->mappers[$mapper_id];
    $profile = $this->straker_translateConfiguration->getConfigProfile($mapper_id);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getConfigSourceStatus($mapper) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if (!in_array($this->getConfigSourceStatus($mapper), [StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING, StrakerTranslate::STATUS_ERROR])) {
      return FALSE;
    }
    $document_id = $this->getConfigDocumentId($mapper);
    if ($document_id) {
      try {
        $response = $this->straker_translate->getDocumentStatus($document_id);
      }
      catch (StrakerTranslateApiException $exception) {
        throw $exception;
      }
      if ($response["data"]["status"] == 'COMPLETED') {
        $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_CURRENT);
        $this->setTranslatedConfigTargetStatus($mapper, $response);
        return TRUE;
      }
      if ($response["data"]["status"] == 'PENDING_TOKEN_PAYMENT' || $response["data"]["status"] == 'IN_PROGRESS') {
        $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_PROCESSING);
        $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_PENDING);
      }
      elseif ($response["data"]["status"] == 'FAILED' || $response["data"]["status"] == 'UNSUCCESSFUL') {
        $this->setConfigSourceStatus($mapper, StrakerTranslate::STATUS_ERROR);
        $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_ERROR);
        throw new StrakerTranslateApiException("The document status is failed. Please check in Straker");
      }
    }
    if ($this->getConfigSourceStatus($mapper) == StrakerTranslate::STATUS_DISABLED) {
      $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function downloadConfig($mapper_id, $file_id, $langcode) {
    $mapper = $this->mappers[$mapper_id];
    $profile = $this->straker_translateConfiguration->getConfigProfile($mapper_id);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getConfigSourceStatus($mapper) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if ($document_id = $this->getConfigDocumentId($mapper)) {
      $data = [];
      try {
        $data = $this->straker_translate->downloadDocument($file_id, $langcode);
      }
      catch (StrakerTranslateApiException $exception) {
        // @todo log issue
        $this->setConfigTargetStatus($mapper, $langcode, StrakerTranslate::STATUS_ERROR);
        return FALSE;
      }
      if ($data) {
        // Allow other modules to alter the data after it is downloaded.
        $config_names = $mapper->getConfigNames();
        $config_name = reset($config_names);
        \Drupal::moduleHandler()->invokeAll('straker_translate_config_object_translation_presave', [&$data, $config_name]);
        try {
          $this->saveConfigTargetData($mapper, $langcode, $data);
          // If the status was "READY", and the target was added
          // successfully, we can ensure that the content is current now.
          $this->setConfigTargetStatus($mapper, $langcode, StrakerTranslate::STATUS_CURRENT);
        }
        catch (\Exception $exception) {
          $this->setConfigTargetStatus($mapper, $langcode, StrakerTranslate::STATUS_ERROR);
          \Drupal::logger('straker_translate')->error('Error happened (unknown) saving %document_id %locale: %message', ['%document_id' => $document_id, '%locale' => $locale, '%message' => $exception->getMessage()]);
          return FALSE;
        }
        return TRUE;
      }
    }
    if ($this->getConfigSourceStatus($mapper) == StrakerTranslate::STATUS_DISABLED) {
      $this->setConfigTargetStatuses($mapper, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteConfigMetadata($mapper_id) {
    $mapper = $this->mappers[$mapper_id];
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      if (!$metadata->isNew()) {
        $metadata->delete();
      }
    }
  }

  /**
   *
   */
  public function saveConfigTargetData(ConfigNamesMapper $mapper, $langcode, $data) {
    $names = $mapper->getConfigNames();
    if (!empty($names)) {
      foreach ($names as $name) {

        $config_translation = $this->languageManager->getLanguageConfigOverride($langcode, $name);

        foreach ($data as $name => $properties) {
          foreach ($properties as $property => $value) {
            $config_translation->set($property, html_entity_decode($value ?? ''));
          }
          $config_translation->save();
        }
      }
    }
  }

  /**
   *
   */
  public function loadByDocumentId($document_id) {
    // We cannot use a mapping table as in content, because config can be staged.
    $entity = NULL;
    // Check config first.
    $config_mappers = array_filter($this->mappers, function ($mapper) {
      return ($mapper instanceof ConfigNamesMapper
        && !$mapper instanceof ConfigEntityMapper
        && !$mapper instanceof ConfigFieldMapper);
    });
    foreach ($config_mappers as $mapper_id => $mapper) {
      if ($this->getConfigDocumentId($mapper) === $document_id) {
        return $mapper;
      }
    }
    // If we failed, check config entities.
    foreach ($this->mappers as $mapper_id => $mapper) {
      if (!isset($config_mappers[$mapper_id])) {
        $id = NULL;
        if (substr($mapper_id, -7) == '_fields') {
          // Hack for fields, the entity is field config.
          $mapper_id = 'field_config';
        }
        $id = $this->entityTypeManager->getStorage('straker_config_metadata')->getQuery()
          ->accessCheck(FALSE)
          ->condition('document_id', $document_id)
          ->execute();
        if (!empty($id)) {
          [$mapper_id, $entity_id] = explode('.', reset($id), 2);
          return $this->entityTypeManager->getStorage($mapper_id)->load($entity_id);
        }
      }
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function markConfigTranslationsAsDirty(ConfigNamesMapper $mapper) {
    $target_languages = $this->languageManager->getLanguages();
    $source_langcode = $mapper->getLangcode();

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $source_langcode && $current_status = $this->getConfigTargetStatus($mapper, $langcode)) {
        if ($current_status == StrakerTranslate::STATUS_CURRENT) {
          $this->setConfigTargetStatus($mapper, $langcode, StrakerTranslate::STATUS_PENDING);
        }
      }
    }
    return $mapper;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigLastUploaded(ConfigNamesMapper $mapper, int $timestamp) {
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $metadata->setLastUploaded($timestamp)->save();
    }
    return $mapper;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfigLastUpdated(ConfigNamesMapper $mapper, int $timestamp) {
    foreach ($mapper->getConfigNames() as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      $metadata->setLastUpdated($timestamp)->save();
    }
    return $mapper;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigLastUpdated(ConfigNamesMapper $mapper) {
    $config_names = $mapper->getConfigNames();
    foreach ($config_names as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      return $metadata->getLastUpdated();
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigLastUploaded(ConfigNamesMapper $mapper) {
    $config_names = $mapper->getConfigNames();
    foreach ($config_names as $config_name) {
      $metadata = StrakerTranslateConfigMetadata::loadByConfigName($config_name);
      return $metadata->getLastUploaded();
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getLastUploaded(ConfigEntityInterface $entity) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    return $metadata->getLastUploaded();
  }

  /**
   * {@inheritdoc}
   */
  public function getLastUpdated(ConfigEntityInterface $entity) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    return $metadata->getLastUpdated();
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUploaded(ConfigEntityInterface $entity, int $timestamp) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $metadata->setLastUploaded($timestamp)->save();

    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUpdated(ConfigEntityInterface $entity, int $timestamp) {
    $metadata = StrakerTranslateConfigMetadata::loadByConfigName($entity->getEntityTypeId() . '.' . $entity->id());
    $metadata->setLastUpdated($timestamp)->save();

    return $entity;
  }

}
