<?php

declare(strict_types=1);

namespace Drupal\languagewire_translation_provider\Service;

use Drupal\languagewire_translation_provider\Adapter\SystemInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface;
use Drupal\languagewire_translation_provider\Client\ClientInterface;
use Drupal\languagewire_translation_provider\Client\RecoverableException;
use Drupal\languagewire_translation_provider\Content\HtmlPreview\HtmlPreviewTemporaryArtifactsCleaner;
use Drupal\languagewire_translation_provider\Content\SanitizerInterface;
use Drupal\languagewire_translation_provider\Domain\Document;
use Drupal\languagewire_translation_provider\Domain\DocumentStatus;
use Drupal\languagewire_translation_provider\Domain\Project;
use Drupal\languagewire_translation_provider\Domain\ProjectStatus;
use Drupal\languagewire_translation_provider\Platform\Document\DocumentBase;
use Drupal\languagewire_translation_provider\Platform\Document\InvalidDocument;
use Drupal\languagewire_translation_provider\Platform\Document\UploadableDocument;
use Drupal\languagewire_translation_provider\Service\ProjectContentUploader\DocumentSelectorFactory;
use Drupal\languagewire_translation_provider\Settings\OrderType;
use Psr\Log\LoggerInterface;

/**
 * Project content uploader.
 * */
class ProjectContentUploader {

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

  /**
   * Sanitizer.
   *
   * @var \Drupal\languagewire_translation_provider\Content\SanitizerInterface
   */
  private SanitizerInterface $sanitizer;

  /**
   * Logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private LoggerInterface $logger;


  /**
   * Html Preview Service.
   *
   * @var HtmlPreviewService
   */
  private HtmlPreviewService $htmlPreviewService;

  /**
   * Html Preview Temporary Artifacts Cleaner.
   *
   * @var \Drupal\languagewire_translation_provider\Content\HtmlPreview\HtmlPreviewTemporaryArtifactsCleaner
   */
  private HtmlPreviewTemporaryArtifactsCleaner $htmlPreviewTemporaryArtifactsCleaner;

  /**
   * Document selector factory.
   *
   * @var \Drupal\languagewire_translation_provider\Service\ProjectContentUploader\DocumentSelectorFactory
   */
  private DocumentSelectorFactory $documentSelectorFactory;

  /**
   * Constructs a new ProjectContentUploader object.
   *
   * @param \Drupal\languagewire_translation_provider\Adapter\SystemInterface $system
   *   System.
   * @param \Drupal\languagewire_translation_provider\Content\SanitizerInterface $sanitizer
   *   Sanitizer.
   * @param \Drupal\languagewire_translation_provider\Service\ProjectContentUploader\DocumentSelectorFactory $documentSelectorFactory
   *   Document selector factory.
   * @param HtmlPreviewService $htmlPreviewService
   *   Html preview service.
   * @param \Drupal\languagewire_translation_provider\Content\HtmlPreview\HtmlPreviewTemporaryArtifactsCleaner $htmlPreviewTemporaryArtifactsCleaner
   *   Html preview temporary artifacts cleaner.
   */
  public function __construct(SystemInterface $system, SanitizerInterface $sanitizer, DocumentSelectorFactory $documentSelectorFactory, HtmlPreviewService $htmlPreviewService, HtmlPreviewTemporaryArtifactsCleaner $htmlPreviewTemporaryArtifactsCleaner) {
    $this->system = $system;
    $this->sanitizer = $sanitizer;
    $this->logger = $system->logger();
    $this->documentSelectorFactory = $documentSelectorFactory;
    $this->htmlPreviewService = $htmlPreviewService;
    $this->htmlPreviewTemporaryArtifactsCleaner = $htmlPreviewTemporaryArtifactsCleaner;
  }

  /**
   * Upload project content.
   *
   * @param \Drupal\languagewire_translation_provider\Domain\Project $project
   *   Project.
   * @param \Drupal\languagewire_translation_provider\Domain\Document[] $projectDocuments
   *   Project documents.
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface $job
   *   Job.
   * @param \Drupal\languagewire_translation_provider\Client\ClientInterface $client
   *   Client.
   * @param bool $shouldStart
   *   Should start project.
   *
   * @throws \Drupal\languagewire_translation_provider\Client\RecoverableException
   */
  public function uploadProjectContent(Project $project, array $projectDocuments, TmgmtJobInterface $job, ClientInterface $client, bool $shouldStart = TRUE): void {
    $allDocumentsToUpload = $this->documentsToUpload($projectDocuments, $job);
    $documentsToUpload = $this->documentSelectorFactory
      ->createFromTranslator($job->translator())
      ->selectDocuments($allDocumentsToUpload);

    if (empty($documentsToUpload)) {
      // @todo Check whether this can lead to project duplication and project garbage in DB.
      // We set the project status to 'unrecoverable error'
      // because this is a state which is not recoverable
      // by re-importing since the project is never
      // created in the platform in the first place.
      $project->setStatus(ProjectStatus::unrecoverableError());
      $job->addMessage('Nothing to upload to LanguageWire. Please make sure that at least one content field is included in the "Content Settings" section.');
      $job->makeUnprocessed();
      $job->save();
      return;
    }

    $this->sanitize($documentsToUpload);

    try {
      $uploadedDocuments = $client->uploadDocuments($project, $documentsToUpload);
    }
    catch (RecoverableException $exception) {
      // @todo Add circuit breaker
      $this->logger->warning("$exception");
      return;
    }
    catch (\Exception $exception) {
      // @todo Check whether this can lead to project duplication and project garbage in DB.
      $this->logger->error("$exception");
      $project->setStatus(ProjectStatus::error());
      $job->addMessage($exception->getMessage());
      $job->save();
      return;
    }

    $invalidDocuments = array_values(array_filter($uploadedDocuments, function (DocumentBase $document): bool {
      return $document instanceof InvalidDocument;
    }));

    if (count($invalidDocuments) > 0) {
      $project->setStatus(ProjectStatus::error());
      $job->makeUnprocessed();
      $job->save();
      return;
    }

    $pendingDocumentCount = count($allDocumentsToUpload) - count($documentsToUpload);
    if ($pendingDocumentCount == 0) {
      $htmlPreviews = $this->createHtmlPreviews($project, $project->getDocuments(), $job, $client);

      if (count($htmlPreviews) === 0) {
        $job->addMessage('HTML Previews were not created.');
      }

      if ($shouldStart) {
        $correlationID = $project->getCorrelationId();

        if ($job->getOrderType() === OrderType::QUOTATION_ORDER) {
          $message = "All items of quote order '$correlationID' have been uploaded to the LanguageWire platform.";
          $client->startQuoteProject($project) ?? throw new RecoverableException('Unable to start quote project');
        }
        else {
          $message = "All items of project '$correlationID' have been uploaded to the LanguageWire platform.";
          $client->startProject($project) ?? throw new RecoverableException('Unable to start project');
        }

        $job->addMessage($message);
      }

      $project->setStatus(ProjectStatus::inProgress());
    }
    else {
      $this->logger->info("There are $pendingDocumentCount pending documents for project '{$project->getCorrelationId()}' which will be uploaded in the next cron job run.");
    }
  }

  /**
   * Documents to upload.
   *
   * @param \Drupal\languagewire_translation_provider\Domain\Document[] $projectDocuments
   *   Project documents.
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface $job
   *   Job.
   *
   * @return \Drupal\languagewire_translation_provider\Platform\Document\UploadableDocument[]
   *   Documents to upload.
   */
  private function documentsToUpload(array $projectDocuments, TmgmtJobInterface $job): array {
    $pendingDocuments = array_values(array_filter($projectDocuments, function (Document $projectDocument): bool {
      return $projectDocument->getStatus()->equals(DocumentStatus::uploadingContent());
    }));

    $uploadableDocuments = array_map(function (Document $projectDocument) use ($job): UploadableDocument {
      return new UploadableDocument($projectDocument, $job->settings(), $job->sourceLanguage(), $this->system);
    }, $pendingDocuments);

    return array_filter($uploadableDocuments, function (UploadableDocument $document): bool {
      return $document->isDomainDocumentInStatus(DocumentStatus::uploadingContent());
    });
  }

  /**
   * Create Html previews.
   *
   * @param \Drupal\languagewire_translation_provider\Domain\Project $project
   *   Project.
   * @param \Drupal\languagewire_translation_provider\Domain\Document[] $documents
   *   Project documents.
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface $job
   *   Job.
   * @param \Drupal\languagewire_translation_provider\Client\ClientInterface $client
   *   Client.
   */
  private function createHtmlPreviews(project $project, array $documents, TmgmtJobInterface $job, ClientInterface $client): array {
    $translator = $job->translator();

    if (!$translator->isHtmlPreviewEnabled()) {
      return [];
    }

    $htmlPreviews = [];

    try {
      $htmlPreviews = $this->htmlPreviewService->createHtmlPreviews($documents, $job->jobId());
      $uploadHtmlPreviews = $client->uploadHtmlPreviews($project, $htmlPreviews);
    }
    catch (\Exception $e) {
      $job->addMessage("Failed to upload HTML Previews. Project uploading will continue without them.");
      $this->logHtmlPreviewUploadException($e, $job);

      return [];
    } finally {
      if ($htmlPreviews > 0) {
        $this->htmlPreviewTemporaryArtifactsCleaner->removePreviewArchives($htmlPreviews);
      }
    }

    return $uploadHtmlPreviews;
  }

  /**
   * Log Html preview upload exception.
   *
   * @param \Exception $exception
   *   Html preview exception.
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface $job
   *   Tmgmt job.
   */
  private function logHtmlPreviewUploadException(\Exception $exception, TmgmtJobInterface $job): void {
    $errorMessage = "Error occurred when trying to upload HTML Previews for job {$job->title()} (ID {$job->jobId()}) in the LanguageWire platform. \n Error: $exception";
    $this->logger->error($errorMessage);
  }

  /**
   * Sanitize.
   *
   * @param \Drupal\languagewire_translation_provider\Platform\Document\UploadableDocument[] $documents
   *   Documents.
   */
  private function sanitize(array &$documents): void {
    array_walk($documents, function (UploadableDocument $document): void {
      $document->sanitizeContent($this->sanitizer);
    });
  }

}
