<?php declare(strict_types = 1);

namespace MailPoet\Automation\Integrations\MailPoet\Actions;

if (!defined('ABSPATH')) exit;


use MailPoet\Automation\Engine\Control\StepRunController;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Exceptions\NotFoundException;
use MailPoet\Automation\Engine\Integration\Action;
use MailPoet\Automation\Engine\Integration\ValidationException;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\InvalidStateException;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Scheduler\LatestNewsletterScheduler;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
use Throwable;

class SendLatestNewsletterAction implements Action {
  public const KEY = 'mailpoet:send-latest-newsletter';

  private const POLL_INTERVALS = [
    5 * MINUTE_IN_SECONDS,
    10 * MINUTE_IN_SECONDS,
    45 * MINUTE_IN_SECONDS,
    4 * HOUR_IN_SECONDS,
    19 * HOUR_IN_SECONDS,
    4 * DAY_IN_SECONDS,
    25 * DAY_IN_SECONDS,
  ];

  private const OPTIN_RETRY_INTERVALS = [
    1 * MINUTE_IN_SECONDS,
    5 * MINUTE_IN_SECONDS,
    20 * MINUTE_IN_SECONDS,
    1 * HOUR_IN_SECONDS,
    12 * HOUR_IN_SECONDS,
    1 * DAY_IN_SECONDS,
  ];

  private const WAIT_OPTIN = 'wait_optin';
  private const OPTIN_RETRIES = 'optin_retries';
  private const OUTCOME = 'outcome';
  private const NEWSLETTER_ID = 'newsletter_id';
  private const OUTCOME_POLLING = 'polling';
  private const OUTCOME_SENT = 'sent';
  private const OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER = 'skipped-ineligible-subscriber';

  private SubscribersRepository $subscribersRepository;

  private SubscriberSegmentRepository $subscriberSegmentRepository;

  private NewslettersRepository $newslettersRepository;

  private SegmentsRepository $segmentsRepository;

  private LatestNewsletterScheduler $latestNewsletterScheduler;

  public function __construct(
    SubscribersRepository $subscribersRepository,
    SubscriberSegmentRepository $subscriberSegmentRepository,
    NewslettersRepository $newslettersRepository,
    SegmentsRepository $segmentsRepository,
    LatestNewsletterScheduler $latestNewsletterScheduler
  ) {
    $this->subscribersRepository = $subscribersRepository;
    $this->subscriberSegmentRepository = $subscriberSegmentRepository;
    $this->newslettersRepository = $newslettersRepository;
    $this->segmentsRepository = $segmentsRepository;
    $this->latestNewsletterScheduler = $latestNewsletterScheduler;
  }

  public function getKey(): string {
    return self::KEY;
  }

  public function getName(): string {
    // translators: automation action title
    return __('Send latest newsletter', 'mailpoet');
  }

  public function getArgsSchema(): ObjectSchema {
    return Builder::object([])
      ->disableAdditionalProperties()
      ->maxProperties(0);
  }

  public function getSubjectKeys(): array {
    return [
      SubscriberSubject::KEY,
      SegmentSubject::KEY,
    ];
  }

  public function validate(StepValidationArgs $args): void {
    if ($args->getStep()->getArgs() !== []) {
      throw ValidationException::create()
        ->withMessage(__('This action does not accept configuration.', 'mailpoet'))
        ->withError('general', __('Send latest newsletter does not accept any configuration fields.', 'mailpoet'));
    }

    try {
      $args->getSingleSubject(SubscriberSubject::KEY);
      $args->getSingleSubject(SegmentSubject::KEY);
    } catch (NotFoundException $e) {
      throw ValidationException::create()
        ->withMessage(__('This action needs a trigger list.', 'mailpoet'))
        ->withError('general', __('This action needs a trigger list, such as "Someone subscribes".', 'mailpoet'));
    }
  }

  public function run(StepRunArgs $args, StepRunController $controller): void {
    [$subscriber, $segmentId] = $this->getSubscriberAndSegment($args);
    $state = null;

    if ($args->isFirstRun()) {
      if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
        $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
        return;
      }

      if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED) {
        $controller->getRunLog()->saveLogData([self::WAIT_OPTIN => 1]);
        $this->rerunLater($args->getRunNumber(), $controller, true);
        return;
      }

      if (!$this->scheduleLatestNewsletter($args, $controller, $subscriber, $segmentId)) {
        return;
      }
    } else {
      $state = $this->getRunLogData($controller);
      if (($state[self::WAIT_OPTIN] ?? 0) === 1) {
        if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED) {
          $this->rerunLater($args->getRunNumber(), $controller, true);
          return;
        }

        if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
          $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
          return;
        }

        $optinRetryCount = max(0, $args->getRunNumber() - 1);
        $controller->getRunLog()->saveLogData([
          self::WAIT_OPTIN => 0,
          self::OPTIN_RETRIES => $optinRetryCount,
        ]);
        $state[self::OPTIN_RETRIES] = $optinRetryCount;
        if (!$this->scheduleLatestNewsletter($args, $controller, $subscriber, $segmentId)) {
          return;
        }
      }

      $success = $this->checkSendingStatus($args, $controller, $subscriber, $segmentId);
      if ($success) {
        return;
      }
    }

    $runNumber = $args->getRunNumber();
    $state = $state ?? $this->getRunLogData($controller);
    $optinRetryCount = $state[self::OPTIN_RETRIES] ?? 0;
    $runNumber -= $optinRetryCount;
    $this->rerunLater($runNumber, $controller, false);
  }

  private function scheduleLatestNewsletter(StepRunArgs $args, StepRunController $controller, SubscriberEntity $subscriber, int $segmentId): bool {
    try {
      $result = $this->latestNewsletterScheduler->schedule($subscriber, $segmentId, $this->getAutomationMeta($args));
    } catch (Throwable $e) {
      throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
    }

    $newsletter = $result['newsletter'];
    $newsletterId = $newsletter instanceof NewsletterEntity ? $newsletter->getId() : null;
    $this->saveOutcome($controller, $result['outcome'], $newsletterId);

    if ($result['outcome'] === LatestNewsletterScheduler::OUTCOME_SCHEDULED) {
      return true;
    }
    return false;
  }

  private function checkSendingStatus(StepRunArgs $args, StepRunController $controller, SubscriberEntity $subscriber, int $segmentId): bool {
    $state = $this->getRunLogData($controller);
    $newsletterId = (int)($state[self::NEWSLETTER_ID] ?? 0);
    if (!$newsletterId) {
      return true;
    }

    $newsletter = $this->latestNewsletterScheduler->getScheduledTaskSubscriber(
      $this->getNewsletter($newsletterId),
      $subscriber,
      $args->getAutomationRun()
    );
    if (!$newsletter) {
      if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
        $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER, $newsletterId);
        return true;
      }
      throw InvalidStateException::create()->withMessage(__('Email failed to schedule.', 'mailpoet'));
    }

    if ($newsletter->getFailed() === ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED) {
      throw InvalidStateException::create()->withMessage(
        // translators: %s is the error message.
        sprintf(__('Email failed to send. Error: %s', 'mailpoet'), $newsletter->getError() ?: 'Unknown error')
      );
    }

    $wasSent = $newsletter->getProcessed() === ScheduledTaskSubscriberEntity::STATUS_PROCESSED;
    if ($wasSent) {
      $this->saveOutcome($controller, self::OUTCOME_SENT, $newsletterId);
      return true;
    }

    if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
      $this->latestNewsletterScheduler->saveErrorAndPause(
        $newsletter,
        __('Subscriber is no longer eligible for this email.', 'mailpoet')
      );
      $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER, $newsletterId);
      return true;
    }

    $optinRetryCount = (int)($state[self::OPTIN_RETRIES] ?? 0);
    $pollRunNumber = max(1, $args->getRunNumber() - $optinRetryCount);
    $isLastRun = $pollRunNumber >= 1 + count(self::POLL_INTERVALS);
    if (!$isLastRun) {
      $this->saveOutcome($controller, self::OUTCOME_POLLING, $newsletterId);
      return false;
    }

    $error = __('Email sending process timed out.', 'mailpoet');
    $this->latestNewsletterScheduler->saveErrorAndPause($newsletter, $error);
    throw InvalidStateException::create()->withMessage($error);
  }

  private function getNewsletter(int $newsletterId): NewsletterEntity {
    $newsletter = $this->newslettersRepository->findOneById($newsletterId);
    if (!$newsletter instanceof NewsletterEntity) {
      throw InvalidStateException::create()->withMessage(__('Email failed to schedule.', 'mailpoet'));
    }
    return $newsletter;
  }

  /**
   * @return array{0: SubscriberEntity, 1: int}
   */
  private function getSubscriberAndSegment(StepRunArgs $args): array {
    try {
      $subscriberId = $args->getSinglePayloadByClass(SubscriberPayload::class)->getId();
      $segmentId = $args->getSinglePayloadByClass(SegmentPayload::class)->getId();
    } catch (Throwable $e) {
      throw InvalidStateException::create()->withMessage(__('This action needs a trigger list, such as "Someone subscribes".', 'mailpoet'));
    }

    $subscriber = $this->subscribersRepository->findOneById($subscriberId);
    if (!$subscriber) {
      throw InvalidStateException::create()->withMessage(__('Subscriber was not found.', 'mailpoet'));
    }
    if (!$this->segmentsRepository->findOneById($segmentId)) {
      throw InvalidStateException::create()->withMessage(__('Cannot send the email because the list was not found.', 'mailpoet'));
    }

    return [$subscriber, $segmentId];
  }

  private function isSubscriberIneligible(SubscriberEntity $subscriber, int $segmentId): bool {
    if (
      $subscriber->getDeletedAt() !== null
      || in_array($subscriber->getStatus(), [SubscriberEntity::STATUS_BOUNCED, SubscriberEntity::STATUS_UNSUBSCRIBED], true)
    ) {
      return true;
    }

    if ($subscriber->getStatus() !== SubscriberEntity::STATUS_SUBSCRIBED) {
      return false;
    }

    return !$this->subscriberSegmentRepository->findOneBy([
      'subscriber' => $subscriber->getId(),
      'segment' => $segmentId,
      'status' => SubscriberEntity::STATUS_SUBSCRIBED,
    ]);
  }

  private function rerunLater(int $runNumber, StepRunController $controller, bool $waitingForOptIn): void {
    if ($waitingForOptIn) {
      if ($runNumber > count(self::OPTIN_RETRY_INTERVALS)) {
        $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
        return;
      }
      $controller->scheduleProgress(time() + self::OPTIN_RETRY_INTERVALS[$runNumber - 1]);
      return;
    }

    $nextInterval = self::POLL_INTERVALS[$runNumber - 1] ?? 0;
    $controller->scheduleProgress(time() + $nextInterval);
  }

  private function getRunLogData(StepRunController $controller): array {
    $runLog = $controller->getRunLog()->getLog();
    return $runLog->getData();
  }

  /** @return array{id: mixed, run_id: mixed, step_id: mixed, run_number: mixed} */
  private function getAutomationMeta(StepRunArgs $args): array {
    return [
      'id' => $args->getAutomation()->getId(),
      'run_id' => $args->getAutomationRun()->getId(),
      'step_id' => $args->getStep()->getId(),
      'run_number' => $args->getRunNumber(),
    ];
  }

  private function saveOutcome(StepRunController $controller, string $outcome, ?int $newsletterId = null): void {
    $data = [self::OUTCOME => $outcome];
    if ($newsletterId) {
      $data[self::NEWSLETTER_ID] = $newsletterId;
    }
    $controller->getRunLog()->saveLogData($data);
  }

  public function onDuplicate(Step $step): Step {
    return $step;
  }
}
