<?php

namespace Drupal\languagewire_translation_provider\Plugin\tmgmt\Translator;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\languagewire_translation_provider\Adapter\SystemInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJob;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtTranslator;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtTranslatorInterface;
use Drupal\languagewire_translation_provider\api\Core\Exception\Error;
use Drupal\languagewire_translation_provider\api\Managers\LanguageManager;
use Drupal\languagewire_translation_provider\api\Models\LanguageModel;
use Drupal\languagewire_translation_provider\api\ProjectAPILibraryFactory;
use Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface;
use Drupal\languagewire_translation_provider\Domain\Document;
use Drupal\languagewire_translation_provider\Domain\DocumentStatus;
use Drupal\languagewire_translation_provider\Domain\ProjectManager;
use Drupal\languagewire_translation_provider\Domain\ProjectStatus;
use Drupal\languagewire_translation_provider\Platform\Document\UploadableDocument;
use Drupal\languagewire_translation_provider\Service\ProjectStateHandler;
use Drupal\languagewire_translation_provider\Settings\OrderType;
use Drupal\tmgmt\ContinuousTranslatorInterface;
use Drupal\tmgmt\JobInterface;
use Drupal\tmgmt\TMGMTException;
use Drupal\tmgmt\Translator\AvailableResult;
use Drupal\tmgmt\Translator\TranslatableResult;
use Drupal\tmgmt\Translator\TranslatorResult;
use Drupal\tmgmt\TranslatorInterface;
use Drupal\tmgmt\TranslatorPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * LanguageWire TMGMT Translator.
 *
 * @TranslatorPlugin(
 *   id = "languagewire",
 *   label = @Translation("LanguageWire Translator"),
 *   description = @Translation("LanguageWire translation service."),
 *   ui = "Drupal\languagewire_translation_provider\Ui\LanguageWireTranslatorUi",
 *   logo = "icons/languagewire-orange-black.svg"
 * ).
 */
class LanguageWireTranslator extends TranslatorPluginBase implements ContinuousTranslatorInterface, ContainerFactoryPluginInterface {
  use StringTranslationTrait;

  /**
   * Job translation error.
   */
  private const JOB_TRANSLATION_ERROR = 'Creation of project was not possible. Try again in a few minutes.';

  /**
   * LanguageWire system.
   *
   * @var \Drupal\languagewire_translation_provider\Adapter\SystemInterface
   */
  private SystemInterface $system;

  /**
   * LanguageWire project repository.
   *
   * @var \Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface
   */
  private ProjectRepositoryInterface $projectRepository;

  /**
   * LanguageWire project state handler.
   *
   * @var \Drupal\languagewire_translation_provider\Service\ProjectStateHandler
   */
  private ProjectStateHandler $projectStateHandler;

  /**
   * Constructs a new LanguageWireTranslator object.
   *
   * @param array $configuration
   *   Configuration.
   * @param string $pluginId
   *   Plugin ID.
   * @param mixed $pluginDefinition
   *   Plugin Definition.
   * @param \Drupal\languagewire_translation_provider\Adapter\SystemInterface $system
   *   LanguageWire system.
   * @param \Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface $projectRepository
   *   LanguageWire project repository.
   * @param \Drupal\languagewire_translation_provider\Service\ProjectStateHandler $projectStateHandler
   *   LanguageWire project state handler.
   * @param \Drupal\languagewire_translation_provider\Domain\ProjectManager $projectManager
   *   LanguageWire project manager.
   */
  public function __construct(
    array $configuration,
    string $pluginId,
    array $pluginDefinition,
    SystemInterface $system,
    ProjectRepositoryInterface $projectRepository,
    ProjectStateHandler $projectStateHandler,
    private ProjectManager $projectManager
  ) {
    parent::__construct($configuration, $pluginId, $pluginDefinition);
    $this->system = $system;
    $this->projectRepository = $projectRepository;
    $this->projectStateHandler = $projectStateHandler;
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition): self {
    $projectManager = new ProjectManager(
      $container->get('languagewire.document_repository') ?? throw new \Exception('Document repository is not available.')
    );

    return new static(
      $configuration,
      $pluginId,
      $pluginDefinition,
      $container->get('languagewire.drupal.system') ?? throw new \Exception('Drupal system is not available.'),
      $container->get('languagewire.project_repository') ?? throw new \Exception('Project repository is not available.'),
      $container->get('languagewire.project_state_handler') ?? throw new \Exception('Project state handler is not available.'),
      $projectManager
    );
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function requestTranslation(JobInterface $tmgmtJob): void {
    $job = new TmgmtJob($tmgmtJob);

    if (!$this->hasContentToTranslate($job)) {
      $job->submit((string) $this->t('No content to translate was selected.'));
      $tmgmtJob->aborted();
      return;
    }

    $bundlingID = NULL;
    try {
      $isQuoteOrder = $job->getOrderType() === OrderType::QUOTATION_ORDER;

      $chunkItems = $this->projectManager->splitJobIntoItemsChucks($job);

      if ($job->settings()->isBundling()) {
        $bundlingID = $this->generateBundleId($job);
      }

      $projectCount = count($chunkItems);
      array_walk($chunkItems, function ($items, $itemPosition) use ($isQuoteOrder, $job, $projectCount, $bundlingID) {
        $project = $this->projectManager->createProjectFromJob($job, $bundlingID);
        $savedProject = $this->projectRepository->save($project);
        $this->projectManager->saveItemsToProject($savedProject, $items);

        $message = "LanguageWire project draft (%d of %d) has been created with status \"%s\" and correlation ID \"%s\"
                        <br> All the progress will be done automatically in the background.";

        if ($isQuoteOrder) {
          $message = "LanguageWire quotation order draft (%d of %d) has been created with status \"%s\" and correlation ID \"%s\"
                        <br> All the progress will be done automatically in the background.";
        }

        $job->addMessage((string) $this->t(
          sprintf(
            $message,
            $itemPosition + 1,
            $projectCount,
            $project->getStatus(),
            $project->getCorrelationId()
          )
        ));
      });

      if ($isQuoteOrder) {
        $job->submit((string) $this->t('Project was submitted successfully as a quotation order.'));
      }
      else {
        $job->submit((string) $this->t('Project was submitted successfully.'));
      }
    }
    catch (\Exception) {
      $job->addMessage(self::JOB_TRANSLATION_ERROR);
      return;
    }

    $job->makeActive();
  }

  /**
   * {@inheritDoc}
   */
  public function requestJobItemsTranslation(array $jobItems) {
    // @todo Implement requestJobItemsTranslation() method.
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function abortTranslation(JobInterface $tmgmtJob): bool {

    $job = new TmgmtJob($tmgmtJob);

    $projectStatuses = $this->projectRepository->getProjectStatusesByTmgmtJobId($job->jobId());

    if (
      $this->canAbortJob($tmgmtJob, $projectStatuses)
      && $this->projectStateHandler->tryCancellingJobProjects($job)
    ) {
      $tmgmtJob->aborted();

      return TRUE;
    }

    $currentStatus = implode(', ', array_unique($projectStatuses));
    $validCancellationStatus = implode(', ', ProjectStatus::CANCELLABLE_STATUSES);

    \Drupal::messenger()->addError(
      $this->t("Project with status (\"$currentStatus\") cannot be canceled.
            Valid statuses are (\"$validCancellationStatus\").
            Please contact a LanguageWire representative if you wish to cancel your translation project.")
    );

    return FALSE;
  }

  /**
   * {@inheritDoc}
   */
  public function getSupportedRemoteLanguages(TranslatorInterface $_): array {
    $tmgmtTranslator = new TmgmtTranslator($_);
    $clientID = $tmgmtTranslator->getClientID();
    $clientSecret = $tmgmtTranslator->getClientSecret();

    try {
      $languageManager = new LanguageManager(
        ProjectAPILibraryFactory::createNewSettingsCredentialsInstance($clientID, $clientSecret)
      );
    }
    catch (Error) {
      return [];
    }

    $availableLanguages = $languageManager->getAvailableLanguages();

    if (!$availableLanguages) {
      return [];
    }

    $languages = [];

    foreach ($availableLanguages as $languageCode => $label) {
      $languages[$languageCode] = "$languageCode - $label";
    }

    return $languages;
  }

  /**
   * {@inheritDoc}
   */
  public function getSupportedTargetLanguages(TranslatorInterface $translator, $sourceLanguage): array {
    $languages = $this->getSupportedRemoteLanguages($translator);
    unset($languages[$sourceLanguage]);

    foreach ($languages as $languageCode => $label) {
      if (!str_contains($sourceLanguage, $languageCode)) {
        unset($languages[$languageCode]);
      }
    }

    return $languages;
  }

  /**
   * {@inheritDoc}
   */
  public function checkAvailable(TranslatorInterface $tmgmtTranslator): AvailableResult {
    $translator = new TmgmtTranslator($tmgmtTranslator);
    return class_exists(LanguageModel::class) && $translator->isConfigured()
            ? AvailableResult::yes()
            : AvailableResult::no($this->getNotAvailableReason($translator));
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Drupal\tmgmt\TMGMTException
   */
  public function checkTranslatable(TranslatorInterface $tmgmtTranslator, JobInterface $tmgmtJob): TranslatorResult {
    $translator = new TmgmtTranslator($tmgmtTranslator);
    $pluginConfiguration = $translator->getPluginConfiguration($this->system);
    $job = new TmgmtJob($tmgmtJob);
    $jobSettings = $job->settings();

    if (!$pluginConfiguration->isFullyInitialized()) {
      return TranslatableResult::no($this->t("This provider hasn't been fully initialized yet. Please check if API Key is correct."));
    }

    try {
      if (count($job->items()) > $job->translator()->getMaxDocumentsPerProject()) {
        return TranslatableResult::no($this->t('The number of items exceeds the maximum allowed by the service.'));
      }
    } catch (TMGMTException) {
      // Do nothing.
    }

    if ($jobSettings->getService()) {
      $serviceRepository = $this->system->serviceRepository();
      $selectedService = $serviceRepository->getById($jobSettings->getService(), $translator);

      if (!$selectedService->isLanguageSupported($job->sourceLanguage())) {
        return TranslatableResult::no($this->t(
              'Source language "@lang" is not supported by @service service.',
              ['@lang' => $tmgmtJob->getSourceLanguage()->getName(), '@service' => $selectedService->name()]
          ));
      }

      if (!$selectedService->isLanguageSupported($job->targetLanguage())) {
        return TranslatableResult::no($this->t(
              'Target language "@lang" is not supported by @service service.',
              ['@lang' => $tmgmtJob->getTargetLanguage()->getName(), '@service' => $selectedService->name()]
          ));
      }
    }

    $isAvailable = $this->checkAvailable($tmgmtTranslator);

    return $isAvailable->getSuccess()
            ? TranslatableResult::yes()
            : TranslatableResult::no($isAvailable->getReason());
  }

  /**
   * Check if there is content to translate.
   *
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJob $job
   *   Job to check.
   * @param bool $addMessage
   *   If message should be added.
   *
   * @return bool
   *   If there is content to translate.
   *
   * @throws \Drupal\tmgmt\TMGMTException
   */
  private function hasContentToTranslate(TmgmtJob $job, bool $addMessage = TRUE): bool {
    $items = $job->items();

    $system = $this->system;
    $settings = $job->settings();
    $sourceLanguage = $job->sourceLanguage();

    $allEmpty = TRUE;
    foreach ($items as $item) {
      $document = new Document(0, $item, DocumentStatus::uploadingContent());
      $uploadableContent = new UploadableDocument($document, $settings, $sourceLanguage, $system);

      $uploadableContent->updateDomainDocument($addMessage);

      if ($uploadableContent->getDocument()->getStatus()->value() !== DocumentStatus::empty()->value()) {
        $allEmpty = FALSE;
        break;
      }
    }

    return !$allEmpty;
  }

  /**
   * Check if job can be aborted.
   *
   * @param \Drupal\tmgmt\JobInterface $tmgmtJob
   *   Job to check.
   * @param \Drupal\languagewire_translation_provider\Domain\ProjectStatus[] $projectStatuses
   *   All project current status.
   */
  private function canAbortJob(JobInterface $tmgmtJob, array $projectStatuses): bool {
    return $tmgmtJob->isActive() && $this->areAllProjectStatusesCancellable($projectStatuses);
  }

  /**
   * Check if status are cancellable.
   *
   * @param \Drupal\languagewire_translation_provider\Domain\ProjectStatus[] $projectStatuses
   *   All project current status.
   */
  private function areAllProjectStatusesCancellable(array $projectStatuses): bool {
    return array_reduce($projectStatuses, function (bool $carry, ProjectStatus $item) {
      return $carry && $item->isCancellable();
    }, TRUE);
  }

  /**
   * Get not available reason.
   */
  private function getNotAvailableReason(TmgmtTranslatorInterface $translator): TranslatableMarkup {
    if (!class_exists(LanguageModel::class)) {
      $message = 'LanguageWire Client Library is not loaded.'
                . ' Make sure that all of the LanguageWire TMGMT plugin dependencies are correctly installed.'
                . PHP_EOL
                . 'Please read the installation guide to get more information.'
                . ' Contact LanguageWire support if problem persists.';
      return $this->t(nl2br($message));
    }

    $messages = [];

    if (!$translator->hasCredentials()) {
      $messages[] = 'Credentials must be provided for the LanguageWire plugin to work.';
    }

    if (!$translator->hasServices()) {
      $messages[] = 'At least one service must be selected.';
    }

    if (!$translator->hasWorkAreas()) {
      $messages[] = 'At least one work area must be selected.';
    }

    if (!$translator->hasTranslationMemories()) {
      $messages[] = 'At least one translation memory must be selected.';
    }

    if (!$translator->hasInvoicingAccounts()) {
      $messages[] = 'At least one invoicing account must be selected.';
    }

    return $this->t(nl2br(implode(PHP_EOL, $messages)));
  }

  /**
   * Generate bundle ID.
   *
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJob $tmgmtJob
   *   TMGMT job.
   *
   * @return string
   *   Bundle ID.
   */
  private function generateBundleId(TmgmtJob $tmgmtJob): string {
    return md5(serialize($tmgmtJob->settings()) . date('Y-m-d H:i'));
  }

}
