<?php

namespace Drupal\languagewire_translation_provider\Service;

use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface;
use Drupal\languagewire_translation_provider\Client\ClientInterface;
use Drupal\languagewire_translation_provider\Client\Project as PlatformProject;
use Drupal\languagewire_translation_provider\Client\ProjectStatus as PlatformProjectStatus;
use Drupal\languagewire_translation_provider\Client\RecoverableException;
use Drupal\languagewire_translation_provider\Content\VariablePlaceholderPrefixingConventionInterface;
use Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface;
use Drupal\languagewire_translation_provider\Domain\DocumentStatus;
use Drupal\languagewire_translation_provider\Domain\Project;
use Drupal\languagewire_translation_provider\Domain\ProjectStatus;
use Psr\Log\LoggerInterface;

/**
 * Project Monitor.
 */
class ProjectMonitor {

  /**
   * Project repository.
   *
   * @var \Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface
   */
  protected ProjectRepositoryInterface $projectRepository;

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

  /**
   * Variable placeholder prefixing convention.
   *
   * @var \Drupal\languagewire_translation_provider\Content\VariablePlaceholderPrefixingConventionInterface
   */
  private VariablePlaceholderPrefixingConventionInterface $prefixingConvention;

  /**
   * Constructs a new ProjectMonitor object.
   *
   * @param \Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface $projectRepository
   *   System.
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger.
   * @param \Drupal\languagewire_translation_provider\Content\VariablePlaceholderPrefixingConventionInterface $prefixingConvention
   *   Variable placeholder prefixing convention.
   */
  public function __construct(
    ProjectRepositoryInterface $projectRepository,
    LoggerInterface $logger,
    VariablePlaceholderPrefixingConventionInterface $prefixingConvention
  ) {
    $this->projectRepository = $projectRepository;
    $this->logger = $logger;
    $this->prefixingConvention = $prefixingConvention;
  }

  /**
   * Monitor project.
   */
  public function monitorProject(Project $project, ClientInterface $client): void {
    $job = $project->getTmgmtJob();
    $platformProject = $this->getPlatformProject($project, $job, $client);

    if ($platformProject === NULL) {
      return;
    }

    if ($platformProject->status()->equals(PlatformProjectStatus::underCreation())) {
      $this->logger->warning("Project {$project->getCorrelationId()} (TMGMT job id {$job->jobId()}) is in the 'Under creation' state in the platform.");
      return;
    }

    $isPlatformUrlUpdated = ((string) $project->getPlatformUrl()) !== $platformProject->platformUrl();

    $project->updateWith($platformProject);
    $this->addPlatformUrlMessageToJob($project, $job, $isPlatformUrlUpdated);

    $this->verifyAssignments($project, $platformProject);

    if ($platformProject->status()->equals(PlatformProjectStatus::finished())) {
      $project->setStatus(ProjectStatus::importing());
      $this->logger->info("Project " . $project->getPlatformId() . " has finished in the platform.");
    } elseif ($project->isAllCancelled() || $platformProject->status()->equals(PlatformProjectStatus::cancelled())) {
      $project->setStatus(ProjectStatus::cancelled());
      $this->logger->info("Project " . $project->getPlatformId() . " was cancelled in the platform.");

      if ($this->areAllProjectsCancelled($job, [$project->getId()])) {
        $this->cancelJob($job);
      }
    }
  }

  /**
   * Get platform project.
   */
  private function getPlatformProject(Project $project, TmgmtJobInterface $job, ClientInterface $client): ?PlatformProject {
    try {
      return $client->getPlatformProject($project);
    }
    catch (RecoverableException $recoverableException) {
      $this->logger->warning("$recoverableException");
    }
    catch (\Exception $exception) {
      $this->logException($exception, $job);
      $job->addMessage("Project monitoring has failed. Failure reason: {$exception->getMessage()}.");
      $project->setStatus(ProjectStatus::error());
    }

    return NULL;
  }


  /**
   * Verify assignments.
   */
  private function verifyAssignments(Project $project, PlatformProject $platformProject): void {
    $sourceDocuments = [];

    foreach ($platformProject->assignments() as $platformAssignment) {
      foreach ($platformAssignment->getPlatformDocumentIdPairs() as $sourceDocumentId => $targetDocumentId) {
        $currentDocument = $project->getDocumentByPlatformId($sourceDocumentId);

        if (in_array($sourceDocumentId, $sourceDocuments)) {
          continue;
        }

        if (!$currentDocument || $currentDocument->getStatus()->equals(DocumentStatus::cancelled())) {
          continue;
        } elseif ($platformAssignment->status()->isCancelled()) {
          $currentDocument->setStatus(DocumentStatus::cancelled());
          $project->updateDocument($currentDocument);
          $project->getTmgmtJob()->addMessage("Document with ID $sourceDocumentId was cancelled.");
          $sourceDocuments[] = $sourceDocumentId;
        }
      }
    }
  }

  /**
   * Add platform URL message to job.
   *
   * @param \Drupal\languagewire_translation_provider\Domain\Project $project
   *   Project.
   * @param \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface $job
   *   Job.
   * @param bool $isPlatformUrlUpdated
   *   Is platform URL updated.
   */
  private function addPlatformUrlMessageToJob(Project $project, TmgmtJobInterface $job, bool $isPlatformUrlUpdated): void {
    $projectId = $project->getPlatformId();

    $urlPlaceholderVar = $this->prefixingConvention->prefix('url', VariablePlaceholderPrefixingConventionInterface::TYPE_INSECURE) . $projectId;
    $projectIdPlaceholderVar = $this->prefixingConvention->prefix('projectId', VariablePlaceholderPrefixingConventionInterface::TYPE_SECURE) . $projectId;

    $projectPlatformUrlMessage = "The project ID in the LanguageWire platform is <a href=\"$urlPlaceholderVar\">$projectIdPlaceholderVar</a>."
      . " You can track it <a href=\"$urlPlaceholderVar\">here</a>.";

    if ($this->shouldUpdateJobWithPlatformUrlMessage($project, $job, $isPlatformUrlUpdated, $projectPlatformUrlMessage)) {
      $job->addMessage(
        $projectPlatformUrlMessage,
        [$urlPlaceholderVar => (string) $project->getPlatformUrl(), $projectIdPlaceholderVar => $projectId]
      );
    }
  }

  /**
   * Should update job with platform URL message.
   */
  private function shouldUpdateJobWithPlatformUrlMessage(Project $project, TmgmtJobInterface $job, bool $isPlatformUrlUpdated, string $expectedMessage): bool {
    return $isPlatformUrlUpdated && !$job->hasMessage("%$expectedMessage%", 'like');
  }

  /**
   * Cancel job.
   */
  public function cancelJob(TmgmtJobInterface $job): void {
    $job->cancel(
          'Translation project "@jobTitle" was cancelled in the LanguageWire platform.',
          ['@jobTitle' => $job->title()]
      );
  }

  /**
   * Are all projects cancelled.
   */
  private function areAllProjectsCancelled(TmgmtJobInterface $job, array $ignoreProjects = []): bool {
    $allJobProjects = $this->projectRepository->getAllByTmgmtJobId($job->jobId());

    foreach ($allJobProjects as $project) {
      if (!$project->getStatus()->equals(ProjectStatus::cancelled()) && !in_array($project->getId(), $ignoreProjects)) {
        return FALSE;
      }
    }

    return TRUE;
  }

  /**
   * Log exception.
   */
  private function logException(\Exception $exception, TmgmtJobInterface $job): void {
    $this->logger->error("Error occurred when trying to create a translation project for job {$job->title()} "
          . "(ID {$job->jobId()}) in the LanguageWire platform. \n"
          . "Error: $exception");
  }

}
