<?php declare(strict_types = 1);

namespace MailPoet\Newsletter\Scheduler;

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


use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use MailPoetVendor\Doctrine\ORM\EntityManager;

class ReEngagementScheduler {

  /** @var NewslettersRepository */
  private $newslettersRepository;

  /** @var ScheduledTasksRepository */
  private $scheduledTasksRepository;

  /** @var EntityManager */
  private $entityManager;

  public function __construct(
    NewslettersRepository $newslettersRepository,
    ScheduledTasksRepository $scheduledTasksRepository,
    EntityManager $entityManager
  ) {
    $this->newslettersRepository = $newslettersRepository;
    $this->scheduledTasksRepository = $scheduledTasksRepository;
    $this->entityManager = $entityManager;
  }

  /**
   * Schedules sending tasks for re-engagement emails
   * @return ScheduledTaskEntity[]
   */
  public function scheduleAll(): array {
    $scheduled = [];
    $emails = $this->newslettersRepository->findActiveByTypes([NewsletterEntity::TYPE_RE_ENGAGEMENT]);
    if (!$emails) {
      return $scheduled;
    }
    foreach ($emails as $email) {
      $scheduled[] = $this->scheduleForEmail($email);
    }
    return array_filter($scheduled);
  }

  private function scheduleForEmail(NewsletterEntity $newsletter): ?ScheduledTaskEntity {
    $scheduledOrRunning = $this->scheduledTasksRepository->findByScheduledAndRunningForNewsletter($newsletter);
    if ($scheduledOrRunning) {
      return null;
    }
    $intervalUnit = $newsletter->getOptionValue(NewsletterOptionFieldEntity::NAME_AFTER_TIME_TYPE);
    $intervalValue = (int)$newsletter->getOptionValue(NewsletterOptionFieldEntity::NAME_AFTER_TIME_NUMBER);
    if (!$intervalValue || !in_array($intervalUnit, ['weeks', 'months'], true)) {
      return null;
    }
    if (!$newsletter->getNewsletterSegments()->count()) {
      return null;
    }

    $scheduledTask = $this->scheduleTask();
    $enqueuedCount = 0;
    foreach ($newsletter->getSegmentIds() as $segmentId) {
      $enqueuedCount += $this->enqueueSubscribersForSegment((int)$newsletter->getId(), $segmentId, $scheduledTask, $intervalUnit, $intervalValue);
    }

    if ($enqueuedCount) {
      $this->createSendingQueue($newsletter, $scheduledTask, $enqueuedCount);
      return $scheduledTask;
    } else {
      // Nothing to send
      $this->scheduledTasksRepository->remove($scheduledTask);
      $this->scheduledTasksRepository->flush();
      return null;
    }
  }

  private function scheduleTask(): ScheduledTaskEntity {
    // Scheduled task
    $scheduledTask = new ScheduledTaskEntity();
    $scheduledTask->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
    $scheduledTask->setScheduledAt(Carbon::now()->millisecond(0));
    $scheduledTask->setType(SendingQueue::TASK_TYPE);
    $scheduledTask->setPriority(SendingQueueEntity::PRIORITY_MEDIUM);
    $this->scheduledTasksRepository->persist($scheduledTask);
    $this->scheduledTasksRepository->flush();
    return $scheduledTask;
  }

  private function createSendingQueue(NewsletterEntity $newsletter, ScheduledTaskEntity $scheduledTask, int $countToProcess): SendingQueueEntity {
    // Sending queue
    $sendingQueue = new SendingQueueEntity();
    $sendingQueue->setTask($scheduledTask);
    $sendingQueue->setNewsletter($newsletter);
    $sendingQueue->setCountToProcess($countToProcess);
    $sendingQueue->setCountTotal($countToProcess);
    $this->entityManager->persist($sendingQueue);
    $this->entityManager->flush();
    return $sendingQueue;
  }

  /**
   * Finds subscribers that should receive re-engagement email and saves scheduled tasks subscribers
   * @return int Count of enqueued subscribers
   */
  private function enqueueSubscribersForSegment(int $newsletterId, int $segmentId, ScheduledTaskEntity $scheduledTask, string $intervalUnit, int $intervalValue): int {
    // Parameters for scheduled task subscribers query
    $thresholdDate = Carbon::now()->millisecond(0);
    if ($intervalUnit === 'months') {
      $thresholdDate->subMonths($intervalValue);
    } else {
      $thresholdDate->subWeeks($intervalValue);
    }
    $thresholdDateSql = $thresholdDate->toDateTimeString();
    // When checking engagement, we ignore emails that subscribers received in the last 24 hours so that we leave them some time to engage.
    // This is prevention for sending re-engagement emails to subscribers who have received a single email very recently.
    $upperThresholdDate = Carbon::now()->millisecond(0);
    $upperThresholdDate->subDay();
    $upperThresholdDate = $upperThresholdDate->toDateTimeString();
    $taskId = $scheduledTask->getId();
    $subscribedStatus = SubscriberEntity::STATUS_SUBSCRIBED;
    $newsletterStatsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
    $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
    $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $nowSql = Carbon::now()->millisecond(0)->toDateTimeString();

    $query = "INSERT IGNORE INTO $scheduledTaskSubscribersTable
      (subscriber_id, task_id,  processed, created_at)
      SELECT DISTINCT ns.subscriber_id as subscriber_id, :taskId as task_id, 0 as processed, :now as created_at
      FROM $newsletterStatsTable as ns
      JOIN $subscribersTable s ON
        ns.subscriber_id = s.id
        AND s.deleted_at is NULL
        AND s.status = :subscribed
        AND GREATEST(COALESCE(s.created_at, '0'), COALESCE(s.last_subscribed_at, '0'), COALESCE(s.last_engagement_at, '0')) < :thresholdDate
      JOIN $subscriberSegmentTable as ss ON ns.subscriber_id = ss.subscriber_id
        AND ss.segment_id = :segmentId
        AND ss.status = :subscribed
      WHERE ns.sent_at > :thresholdDate
        AND ns.sent_at < :upperThresholdDate
        AND ns.subscriber_id NOT IN (
          SELECT DISTINCT subscriber_id as id FROM $newsletterStatsTable WHERE newsletter_id = :newsletterId AND sent_at > :thresholdDate
        );
    ";

    $statement = $this->entityManager->getConnection()->prepare($query);
    $statement->bindValue('now', $nowSql, ParameterType::STRING);
    $statement->bindValue('taskId', $taskId, ParameterType::INTEGER);
    $statement->bindValue('subscribed', $subscribedStatus, ParameterType::STRING);
    $statement->bindValue('thresholdDate', $thresholdDateSql, ParameterType::STRING);
    $statement->bindValue('upperThresholdDate', $upperThresholdDate, ParameterType::STRING);
    $statement->bindValue('newsletterId', $newsletterId, ParameterType::INTEGER);
    $statement->bindValue('segmentId', $segmentId, ParameterType::INTEGER);

    $result = $statement->executeQuery();
    return $result->rowCount();
  }
}
