<?php

namespace Drupal\straker_translate;

use Drupal\Component\Gettext\PoItem;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\language\Entity\ConfigurableLanguage;
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;

/**
 * Service for managing Straker Translate interface translations.
 */
class StrakerTranslateInterfaceTranslationService implements StrakerTranslateInterfaceTranslationServiceInterface {

  use StringTranslationTrait;

  /**
   * The Straker Translate interface.
   *
   * @var \Drupal\straker_translate\StrakerTranslateInterface
   */
  protected $straker_translate;

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

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

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

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The theme handler.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

  /**
   * Constructs a new StrakerTranslateContentTranslationService 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\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection object.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler.
   */
  public function __construct(StrakerTranslateInterface $straker_translate, LanguageLocaleMapperInterface $language_locale_mapper, StrakerTranslateConfigurationServiceInterface $straker_translate_configuration, LanguageManagerInterface $language_manager, Connection $connection, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
    $this->straker_translate = $straker_translate;
    $this->languageLocaleMapper = $language_locale_mapper;
    $this->straker_translateConfiguration = $straker_translate_configuration;
    $this->languageManager = $language_manager;
    $this->connection = $connection;
    $this->moduleHandler = $module_handler;
    $this->themeHandler = $theme_handler;
  }

  /**
   * Returns the component Straker Translate metadata.
   *
   * @param string $component
   *   The component.
   *
   * @return array
   *   The metadata.
   */
  protected function getMetadata($component) {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    $component_metadata = [];
    if ($translations_metadata) {
      if (isset($translations_metadata[$component])) {
        $component_metadata = $translations_metadata[$component];
      }
    }
    return $component_metadata;
  }

  /**
   *
   */
  protected function saveMetadata($component, $metadata) {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    if (!$translations_metadata) {
      $translations_metadata = [];
    }
    $translations_metadata[$component] = $metadata;
    $state->set('straker_translate.interface_translations_metadata', $translations_metadata);
  }

  /**
   * {@inheritdoc}
   */
  public function checkSourceStatus($component) {
    $document_id = $this->getDocumentId($component);
    if ($document_id) {
      // Document has successfully imported.
      try {
        $response = $this->straker_translate->getDocumentStatus($document_id);
      }
      catch (StrakerTranslateDocumentLockedException $exception) {
        $this->setDocumentId($component, $exception->getNewDocumentId());
        throw $exception;
      }
      catch (StrakerTranslateDocumentNotFoundException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateDocumentArchivedException $exception) {
        $this->setDocumentId($component, NULL);
        $this->deleteMetadata($component);
        throw $exception;
      }
      catch (StrakerTranslatePaymentRequiredException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateApiException $exception) {
        throw $exception;
      }
      if ($response["data"]["status"] == 'COMPLETED') {
        $this->setSourceStatus($component, StrakerTranslate::STATUS_CURRENT);
        $this->setTranslatedTargetStatus($component, $response);
        return TRUE;
      }
      if ($response["data"]["status"] == 'PENDING_TOKEN_PAYMENT' || $response["data"]["status"] == 'IN_PROGRESS') {
        $this->setSourceStatus($component, StrakerTranslate::STATUS_PROCESSING);
        $this->setTargetStatuses($component, StrakerTranslate::STATUS_PENDING);
      }
      elseif ($response["data"]["status"] == 'FAILED' || $response["data"]["status"] == 'UNSUCCESSFUL') {
        $this->setSourceStatus($component, StrakerTranslate::STATUS_ERROR);
        $this->setTargetStatuses($component, StrakerTranslate::STATUS_ERROR);
        throw new StrakerTranslateApiException("The document status is failed. Please check in Straker");
      }
      else {
        $this->setSourceStatus($component, StrakerTranslate::STATUS_PROCESSING);
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceStatus($component) {
    $source_language = 'en';
    return $this->getTargetStatus($component, $source_language);
  }

  /**
   * {@inheritdoc}
   */
  public function setSourceStatus($component, $status): string {
    $source_language = 'en';
    return $this->setTargetStatus($component, $source_language, $status);
  }

  /**
   * Clear the target statuses.
   *
   * @param string $component
   *   The component.
   */
  protected function clearTargetStatuses($component) {
    // Clear the target statuses. As we save the source status with the target,
    // we need to keep that one.
    $source_status = $this->getSourceStatus($component);

    $metadata = $this->getMetadata($component);
    if (!empty($metadata) && isset($metadata['translation_status'])) {
      unset($metadata['translation_status']);
      $this->saveMetadata($component, $metadata);
    }
    $this->setTargetStatus($component, 'en', $source_status);
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetStatus($component, $langcode) {
    $status = StrakerTranslate::STATUS_UNTRACKED;
    $statuses = $this->getTargetStatuses($component);
    if (isset($statuses[$langcode])) {
      $status = $statuses[$langcode];
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetStatuses($component) {
    $metadata = $this->getMetadata($component);
    return $metadata['translation_status'] ?? [];
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetFileId($component, $langcode) {
    $target_file_id = '';
    $metadata = $this->getMetadata($component);
    $translation_files = $metadata['translation_files'] ?? [];
    if (isset($translation_files[$langcode])) {
      $target_file_id = $translation_files[$langcode];
    }
    return $target_file_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatus($component, $langcode, $status) {
    $metadata = $this->getMetadata($component);
    $metadata['translation_status'][$langcode] = $status;
    $this->saveMetadata($component, $metadata);
    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetFileId($component, $langcode, $target_file_id) {
    $metadata = $this->getMetadata($component);
    $metadata['translation_files'][$langcode] = $target_file_id;
    $this->saveMetadata($component, $metadata);
    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function setTranslatedTargetStatus($component, $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($component, $drupal_language->id(), StrakerTranslate::STATUS_READY);
          // Set the file ID too.
          $target_file_id = $target_file['target_file_uuid'];
          $this->setTargetFileId($component, $drupal_language->id(), $target_file_id);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatuses($component, $status) {
    $target_languages = $this->languageManager->getLanguages();
    $source_langcode = 'en';

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $source_langcode && $current_status = $this->getTargetStatus($component, $langcode)) {
        $this->setTargetStatus($component, $langcode, $status);
      }
    }
    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function markTranslationsAsDirty($component): void {
    $target_languages = $this->languageManager->getLanguages();
    $source_langcode = 'en';

    // 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 != $source_langcode && $current_status = $this->getTargetStatus($component, $langcode)) {
        if (in_array($current_status, $to_change)) {
          $this->setTargetStatus($component, $langcode, StrakerTranslate::STATUS_PENDING);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentId($component) {
    $doc_id = NULL;
    $metadata = $this->getMetadata($component);
    if (!empty($metadata) && isset($metadata['document_id'])) {
      $doc_id = $metadata['document_id'];
    }
    return $doc_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setDocumentId($component, $doc_id) {
    $metadata = $this->getMetadata($component);
    $metadata['document_id'] = $doc_id;
    $this->saveMetadata($component, $metadata);
    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceLocale($component) {
    $source_language = 'en';
    return $this->languageLocaleMapper->getLocaleForLangcode($source_language);
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceData($component) {
    $data = [];
    $potx_strings = $this->extractPotxStrings($component);
    if (!empty($potx_strings)) {
      foreach ($potx_strings as $potx_string => $potx_string_meta) {
        // The key in the JSON download cannot have the null byte used by plurals.
        $translationStringKey = str_replace("\0", "<PLURAL>", $potx_string);
        // Plural strings have a null byte delimited format. We need to separate the
        // segments ourselves and nest those in.
        $explodedStrings = explode("\0", $potx_string);
        $translationData = [];
        foreach ($explodedStrings as $index => $explodedString) {
          $translationData[$explodedString] = $explodedString;
        }
        foreach ($potx_string_meta as $context => $potx_string_meta_locations) {
          $translationStringKeyWithContext = $translationStringKey . '<CONTEXT>' . $context;
          $data[$translationStringKeyWithContext] = $translationData;
          $data[$translationStringKeyWithContext]['_context'] = $context;
        }
      }
    }
    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function updateEntityHash($component) {
    $source_data = json_encode($this->getSourceData($component));
    $metadata = $this->getMetadata($component);
    if (!empty($metadata)) {
      $metadata['straker_translate_hash'] = md5($source_data);
      $this->saveMetadata($component, $metadata);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hasEntityChanged($component) {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function uploadDocument($component) {
    $source_data = $this->getSourceData($component);
    $document_name = 'Interface translation: ' . $component;

    // Allow other modules to alter the data before is uploaded.
    \Drupal::moduleHandler()->invokeAll('straker_translate_interface_translation_document_upload', [&$source_data, &$component]);

    try {
      $filename = str_replace('/', '_', $component) . '_interface_translation.json';
      // Languages to skip.
      $skip_langcodes = ['en'];

      $target_languages = $this->languageManager->getLanguages();

      // Filter out source and disabled languages.
      $target_languages = array_filter($target_languages, function (LanguageInterface $language) use ($skip_langcodes) {
        if (in_array($language->getId(), $skip_langcodes, TRUE)) {
          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 interface translation component %component. Document upload skipped.', ['%component' => $component]);
        return FALSE;
      }
      $target_langcodes = array_keys($target_languages);
      $target_locales = array_map([$this->languageLocaleMapper, 'getLocaleForLangcode'], $target_langcodes);
      $target_locale_uuid = implode(',', $target_locales);
      $token_confirmation = 'false';
      $document_id = $this->straker_translate->uploadDocument($document_name, $source_data, $target_locale_uuid, $token_confirmation, $filename, NULL);
    }
    catch (StrakerTranslatePaymentRequiredException $exception) {
      $this->setSourceStatus($component, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateProcessedWordsLimitException $exception) {
      $this->setSourceStatus($component, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateApiException $exception) {
      $this->setSourceStatus($component, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    if ($document_id) {
      $this->setDocumentId($component, $document_id);
      $this->setSourceStatus($component, StrakerTranslate::STATUS_READY);
      $this->setTargetStatuses($component, StrakerTranslate::STATUS_PENDING);
      $this->setLastUploaded($component, \Drupal::time()->getRequestTime());
      return $document_id;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function downloadDocument($component, $langcode) {
    if ($target_file_id = $this->getTargetFileId($component, $langcode)) {
      $source_status = $this->getSourceStatus($component);
      $target_status = $this->getTargetStatus($component, $langcode);
      $data = [];
      try {
        $data = $this->straker_translate->downloadDocument($target_file_id, $langcode);
      }
      catch (StrakerTranslateApiException $exception) {
        \Drupal::logger('straker_translate')->error('Error happened downloading %document_id %locale: %message', [
          '%document_id' => $document_id,
          '%langcode' => $langcode,
          '%message' => $exception->getMessage(),
        ]);
        $this->setTargetStatus($component, $langcode, StrakerTranslate::STATUS_ERROR);
        throw $exception;
      }

      if ($data) {
        // Check the real status, because it may still need review or anything.
        $transaction = $this->connection->startTransaction();
        try {
          $saved = $this->saveTargetData($component, $langcode, $data);
          if ($saved) {
            // If the status was "Importing", and the target was added
            // successfully, we can ensure that the content is current now.
            if ($source_status == StrakerTranslate::STATUS_IMPORTING) {
              $this->setSourceStatus($component, StrakerTranslate::STATUS_CURRENT);
            }
            if ($source_status == StrakerTranslate::STATUS_EDITED) {
              $this->setTargetStatus($component, $langcode, StrakerTranslate::STATUS_EDITED);
            }
            $this->setTargetStatus($component, $langcode, StrakerTranslate::STATUS_CURRENT);
          }
        }
        catch (\Exception $exception) {
          $transaction->rollBack();
          \Drupal::logger('straker_translate')->error('Error happened (unknown) saving %document_id %locale: %message', ['%document_id' => $document_id, '%locale' => $locale, '%message' => $exception->getMessage()]);
          $this->setTargetStatus($component, $langcode, StrakerTranslate::STATUS_ERROR);
          return FALSE;
        }
        return TRUE;
      }
    }
    \Drupal::logger('straker_translate')->warning('Error happened trying to download (%component): no document id found.', ['%component' => $component]);
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteMetadata($component) {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    if ($translations_metadata) {
      unset($translations_metadata[$component]);
      $state->set('straker_translate.interface_translations_metadata', $translations_metadata);
    }
    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAllMetadata() {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    $state->delete('straker_translate.interface_translations_metadata');
  }

  /**
   * {@inheritdoc}
   */
  public function loadByDocumentId($document_id) {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    if ($translations_metadata) {
      foreach ($translations_metadata as $component => $componentMetadata) {
        if ($componentMetadata['document_id'] === $document_id) {
          return $component;
        }
      }
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getAllLocalDocumentIds() {
    $state = \Drupal::state();
    $translations_metadata = $state->get('straker_translate.interface_translations_metadata');
    $docIds = [];
    if ($translations_metadata) {
      foreach ($translations_metadata as $component => $componentMetadata) {
        $docIds[] = $componentMetadata['document_id'];
      }
    }
    return $docIds;
  }

  /**
   * {@inheritdoc}
   */
  public function saveTargetData($component, $langcode, $data) {
    $customized = TRUE;
    $overwrite_options['customized'] = TRUE;
    $overwrite_options['not_customized'] = FALSE;
    foreach ($data as $sourceData => $translationData) {
      // We need to take and ignore the special _context entry.
      $context = $translationData['_context'];
      unset($translationData['_context']);
      // We need to manage plurals.
      if (count($translationData) == 1) {
        $keys = array_keys($translationData);
        $source = reset($keys);
        $translation = reset($translationData);
      }
      else {
        $keys = array_keys($translationData);
        $source = implode(PoItem::DELIMITER, $keys);
        $translation = implode(PoItem::DELIMITER, $translationData);
      }
      // Look up the source string and any existing translation.
      /** @var \Drupal\locale\TranslationString[] $strings */
      $strings = \Drupal::service('locale.storage')->getTranslations([
        'language' => $langcode,
        'source' => $source,
        'context' => $context,
      ]);
      $string = reset($strings);
      if (!empty($translation)) {
        // Skip this string unless it passes a check for dangerous code.
        if (!locale_string_is_safe($translation)) {
          \Drupal::logger('straker_translate')->error('Import of string "%string" was skipped because of disallowed or malformed HTML.', ['%string' => $translation]);
        }
        elseif ($string) {
          $customized_existing = !empty($string->customized) ? 'customized' : 'not_customized';
          $string->setString($translation);
          if ($string->isNew()) {
            // No translation in this language.
            $string->language = $langcode;
            $string->setCustomized($customized);
            $string->save();
          }
          elseif ($overwrite_options[$customized_existing]) {
            // Translation exists, only overwrite if instructed.
            $string->setCustomized($customized);
            $string->save();
          }
        }
        else {
          // No such source string in the database yet.
          $string = \Drupal::service('locale.storage')->createString(['source' => $source, 'context' => $context])
            ->save();
          \Drupal::service('locale.storage')->createTranslation([
            'lid' => $string->getId(),
            'language' => $langcode,
            'translation' => $translation,
            'customized' => $customized,
          ])->save();
        }
      }
    }
    return $component;
  }

  /**
   * Extract strings by using potx module.
   *
   * @param string $component
   *   The component we want to extract the strings from.
   *
   * @return array
   *   Collection of strings in the potx format:
   *     string => [
   *       context => context_info,
   *       context => context_info,
   *    ]
   */
  protected function extractPotxStrings($component) {
    global $_potx_strings;

    $this->moduleHandler->loadInclude('potx', 'inc');
    $this->moduleHandler->loadInclude('potx', 'inc', 'potx.local');

    // Silence status messages.
    potx_status('set', POTX_STATUS_MESSAGE);
    $pathinfo = pathinfo($component);
    if (!isset($pathinfo['filename'])) {
      // The filename key is only available in PHP 5.2.0+.
      $pathinfo['filename'] = substr($pathinfo['basename'], 0, strrpos($pathinfo['basename'], '.'));
    }
    if (isset($pathinfo['extension'])) {
      // A specific module or theme file was requested (otherwise there should
      // be no extension).
      potx_local_init($pathinfo['dirname']);
      $files = _potx_explore_dir($pathinfo['dirname'] . '/', $pathinfo['filename']);
      $strip_prefix = 1 + strlen($pathinfo['dirname']);
    }
    // A directory name was requested.
    else {
      potx_local_init($component);
      $files = _potx_explore_dir($component . '/');
      $strip_prefix = 1 + strlen($component);
    }

    // Collect every string in affected files. Installer related strings are
    // discarded.
    foreach ($files as $file) {
      _potx_process_file($file, $strip_prefix);
    }
    potx_finish_processing('_potx_save_string');
    return $_potx_strings;
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUploaded($component, int $timestamp) {
    $metadata = $this->getMetadata($component);
    $metadata['uploaded_timestamp'] = $timestamp;
    $this->saveMetadata($component, $metadata);

    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUpdated($component, int $timestamp) {
    $metadata = $this->getMetadata($component);
    $metadata['updated_timestamp'] = $timestamp;
    $this->saveMetadata($component, $metadata);

    return $component;
  }

  /**
   * {@inheritdoc}
   */
  public function getLastUploaded($component) {
    $metadata = $this->getMetadata($component);
    return $metadata['uploaded_timestamp'] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getUpdatedTime($component) {
    $metadata = $this->getMetadata($component);
    return $metadata['updated_timestamp'] ?? NULL;
  }

}
