<?php declare(strict_types = 1);

namespace MailPoet\Newsletter;

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


use MailPoet\Cron\ActionScheduler\Actions\DaemonTrigger;
use MailPoet\Cron\CronTrigger;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\UnexpectedValueException;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use MailPoetVendor\Doctrine\ORM\EntityManager;

class NewsletterResendController {
  const MIN_RESEND_DELAY_HOURS = 24;
  const MAX_RESEND_DELAY_HOURS = 72;

  /** @var NewsletterSaveController */
  private $newsletterSaveController;

  /** @var NewsletterDeleteController */
  private $newsletterDeleteController;

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

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

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

  /** @var SendingQueuesRepository */
  private $sendingQueuesRepository;

  /** @var WPFunctions */
  private $wp;

  /** @var DaemonTrigger */
  private $daemonTrigger;

  /** @var SettingsController */
  private $settings;

  /** @var SubscribersFeature */
  private $subscribersFeature;

  /** @var MailerFactory */
  private $mailerFactory;

  /** @var TrackingConfig */
  private $trackingConfig;

  public function __construct(
    NewsletterSaveController $newsletterSaveController,
    NewsletterDeleteController $newsletterDeleteController,
    NewslettersRepository $newslettersRepository,
    EntityManager $entityManager,
    ScheduledTasksRepository $scheduledTasksRepository,
    SendingQueuesRepository $sendingQueuesRepository,
    WPFunctions $wp,
    DaemonTrigger $daemonTrigger,
    SettingsController $settings,
    SubscribersFeature $subscribersFeature,
    MailerFactory $mailerFactory,
    TrackingConfig $trackingConfig
  ) {
    $this->newsletterSaveController = $newsletterSaveController;
    $this->newsletterDeleteController = $newsletterDeleteController;
    $this->newslettersRepository = $newslettersRepository;
    $this->entityManager = $entityManager;
    $this->scheduledTasksRepository = $scheduledTasksRepository;
    $this->sendingQueuesRepository = $sendingQueuesRepository;
    $this->wp = $wp;
    $this->daemonTrigger = $daemonTrigger;
    $this->settings = $settings;
    $this->subscribersFeature = $subscribersFeature;
    $this->mailerFactory = $mailerFactory;
    $this->trackingConfig = $trackingConfig;
  }

  /**
   * Extensibility hook for premium/plan restrictions.
   * Override or decorate this method to add per-site daily caps,
   * plan-based limits, or other restrictions.
   */
  public function canResend(NewsletterEntity $newsletter): void {
    if (!$this->trackingConfig->isEmailTrackingEnabled()) {
      throw UnexpectedValueException::create()
        ->withMessage(__('Resend to non-openers requires email tracking to be enabled.', 'mailpoet'));
    }

    if ($newsletter->getType() !== NewsletterEntity::TYPE_STANDARD) {
      throw UnexpectedValueException::create()
        ->withMessage(__('Only standard newsletters can be resent.', 'mailpoet'));
    }

    if ($newsletter->getStatus() !== NewsletterEntity::STATUS_SENT) {
      throw UnexpectedValueException::create()
        ->withMessage(__('Only sent newsletters can be resent.', 'mailpoet'));
    }

    $queue = $newsletter->getLatestQueue();
    if ($queue && $queue->getCountToProcess() > 0) {
      throw UnexpectedValueException::create()
        ->withMessage(__('This email is still being sent. Wait until sending is complete.', 'mailpoet'));
    }

    if ($newsletter->getParent() !== null) {
      throw UnexpectedValueException::create()
        ->withMessage(__('A resent email cannot be resent again.', 'mailpoet'));
    }

    if (!$newsletter->getChildren()->isEmpty()) {
      throw UnexpectedValueException::create()
        ->withMessage(__('This email has already been resent.', 'mailpoet'));
    }

    $sentAt = $newsletter->getSentAt();
    if (!$sentAt) {
      throw UnexpectedValueException::create()
        ->withMessage(__('This email has no sent date and cannot be resent.', 'mailpoet'));
    }

    $hoursSinceSent = Carbon::now()->diffInHours(Carbon::createFromTimestamp($sentAt->getTimestamp()), true);
    if ($hoursSinceSent < self::MIN_RESEND_DELAY_HOURS) {
      throw UnexpectedValueException::create()
        ->withMessage(__('You can resend this email at least 1 day after it was sent.', 'mailpoet'));
    }
    if ($hoursSinceSent > self::MAX_RESEND_DELAY_HOURS) {
      throw UnexpectedValueException::create()
        ->withMessage(__('You can only resend this email within 3 days of sending it.', 'mailpoet'));
    }

    if ($this->subscribersFeature->check()) {
      throw UnexpectedValueException::create()
        ->withMessage(__('Subscribers limit reached.', 'mailpoet'));
    }

    try {
      $this->mailerFactory->getDefaultMailer();
    } catch (\Exception $e) {
      throw UnexpectedValueException::create()
        ->withMessage($e->getMessage());
    }
  }

  public function resendToNonOpeners(NewsletterEntity $newsletter, string $subject): NewsletterEntity {
    $this->canResend($newsletter);

    $subject = trim($subject);
    if ($subject === '') {
      throw UnexpectedValueException::create()
        ->withMessage(__('Subject line is required.', 'mailpoet'));
    }

    $originalSubject = trim($newsletter->getSubject());
    if (strcasecmp($subject, $originalSubject) === 0) {
      throw UnexpectedValueException::create()
        ->withMessage(__('The subject line must be different from the original email.', 'mailpoet'));
    }

    $nonOpenerIds = $this->getNonOpenerIds($newsletter);
    if (empty($nonOpenerIds)) {
      throw UnexpectedValueException::create()
        ->withMessage(__('All recipients have already opened this email.', 'mailpoet'));
    }

    $duplicate = $this->newsletterSaveController->duplicate($newsletter);
    $duplicate->setParent($newsletter);
    $duplicate->setSubject($subject);

    $wpPostId = $duplicate->getWpPostId();
    if ($wpPostId) {
      $this->wp->wpUpdatePost([
        'ID' => $wpPostId,
        'post_title' => $subject,
      ]);
    }

    $this->newslettersRepository->flush();

    try {
      $scheduledTask = new ScheduledTaskEntity();
      $scheduledTask->setType(SendingQueueWorker::TASK_TYPE);
      $scheduledTask->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
      $scheduledTask->setStatus(null);
      $this->scheduledTasksRepository->persist($scheduledTask);
      $this->scheduledTasksRepository->flush();

      $sendingQueue = new SendingQueueEntity();
      $sendingQueue->setNewsletter($duplicate);
      $sendingQueue->setTask($scheduledTask);
      $this->sendingQueuesRepository->persist($sendingQueue);
      $this->sendingQueuesRepository->flush();
      $this->newslettersRepository->refresh($duplicate);

      $insertedCount = $this->addSubscribersToTask($scheduledTask, $nonOpenerIds);

      if ($insertedCount === 0) {
        throw UnexpectedValueException::create()
          ->withMessage(__('No eligible recipients found. All non-openers have since unsubscribed, bounced, or left the original segments.', 'mailpoet'));
      }

      $this->sendingQueuesRepository->updateCounts($sendingQueue);
      $duplicate->setStatus(NewsletterEntity::STATUS_SENDING);
      $this->newslettersRepository->flush();
    } catch (\Throwable $e) {
      $this->cleanupDuplicate($duplicate, $scheduledTask ?? null, $sendingQueue ?? null);
      throw $e;
    }

    if ($this->settings->get('cron_trigger.method') === CronTrigger::METHOD_ACTION_SCHEDULER) {
      $this->daemonTrigger->process();
    }

    return $duplicate;
  }

  private function cleanupDuplicate(
    NewsletterEntity $duplicate,
    ?ScheduledTaskEntity $task,
    ?SendingQueueEntity $queue
  ): void {
    $duplicateId = $duplicate->getId();
    if ($duplicateId) {
      $this->newsletterDeleteController->bulkDelete([$duplicateId]);
      return;
    }
    if ($queue) {
      $this->entityManager->remove($queue);
    }
    if ($task) {
      $this->entityManager->remove($task);
    }
    $this->entityManager->flush();
  }

  /** @return int[] */
  private function getNonOpenerIds(NewsletterEntity $newsletter): array {
    $statisticsNewsletterTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
    $statisticsOpenTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();

    $connection = $this->entityManager->getConnection();

    $result = $connection->executeQuery(
      "SELECT DISTINCT sn.subscriber_id
       FROM $statisticsNewsletterTable sn
       LEFT JOIN $statisticsOpenTable so
         ON so.newsletter_id = sn.newsletter_id
         AND so.subscriber_id = sn.subscriber_id
       WHERE sn.newsletter_id = ?
         AND so.id IS NULL",
      [
        $newsletter->getId(),
      ],
      [
        ParameterType::INTEGER,
      ]
    );

    /** @var list<string|int> $column */
    $column = $result->fetchFirstColumn();
    return array_map('intval', $column);
  }

  /** @param int[] $subscriberIds */
  private function addSubscribersToTask(ScheduledTaskEntity $task, array $subscriberIds): int {
    $scheduledTaskSubscriberTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
    $subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();

    $connection = $this->entityManager->getConnection();

    $result = $connection->executeQuery(
      "INSERT IGNORE INTO $scheduledTaskSubscriberTable
       (task_id, subscriber_id, processed)
       SELECT DISTINCT ? as task_id, subscribers.`id` as subscriber_id, ? as processed
       FROM $subscriberTable subscribers
       WHERE subscribers.`deleted_at` IS NULL
       AND subscribers.`status` = ?
       AND subscribers.`id` IN (?)",
      [
        $task->getId(),
        ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED,
        SubscriberEntity::STATUS_SUBSCRIBED,
        $subscriberIds,
      ],
      [
        ParameterType::INTEGER,
        ParameterType::INTEGER,
        ParameterType::STRING,
        ArrayParameterType::INTEGER,
      ]
    );

    return $result->rowCount();
  }
}