<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing

namespace MailPoet\Subscribers;

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


use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Html2Text;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Shortcodes;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;

class ConfirmationEmailMailer {

  const MAX_CONFIRMATION_EMAILS = 3;
  const ADMIN_CONFIRMATION_RESEND_INTERVAL_DAYS = 7;
  protected const WC_CONFIRMATION_UNAVAILABLE = 'unavailable';
  protected const WC_CONFIRMATION_SENT = 'sent';
  protected const WC_CONFIRMATION_FAILED = 'failed';

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

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

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

  /** @var MetaInfo */
  private $mailerMetaInfo;

  /** @var SubscribersRepository */
  private $subscribersRepository;

  /** @var SubscriptionUrlFactory */
  private $subscriptionUrlFactory;

  /** @var ConfirmationEmailCustomizer */
  private $confirmationEmailCustomizer;

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

  /** @var LoggerFactory */
  private $loggerFactory;

  /** @var array Cache for confirmation emails sent within a request */
  private $sentEmails = [];

  public function __construct(
    MailerFactory $mailerFactory,
    SettingsController $settings,
    SubscribersRepository $subscribersRepository,
    SubscriptionUrlFactory $subscriptionUrlFactory,
    ConfirmationEmailCustomizer $confirmationEmailCustomizer,
    NewslettersRepository $newslettersRepository,
    ?WPFunctions $wp = null
  ) {
    $this->mailerFactory = $mailerFactory;
    $this->wp = $wp ?? new WPFunctions();
    $this->settings = $settings;
    $this->mailerMetaInfo = new MetaInfo;
    $this->subscriptionUrlFactory = $subscriptionUrlFactory;
    $this->subscribersRepository = $subscribersRepository;
    $this->confirmationEmailCustomizer = $confirmationEmailCustomizer;
    $this->newslettersRepository = $newslettersRepository;
    $this->loggerFactory = LoggerFactory::getInstance();
  }

  /**
   * Use this method if you want to make sure the confirmation email
   * is not sent multiple times within a single request
   * e.g. if sending confirmation emails from hooks
   * @param SubscriberEntity $subscriber The subscriber to send the confirmation email to.
   * @param int|null $confirmationEmailId Optional ID of a specific confirmation email newsletter to use.
   * @param int|null $confirmationPageId Optional ID of a specific page to use for the confirmation link.
   * @throws \Exception if unable to send the email.
   */
  public function sendConfirmationEmailOnce(SubscriberEntity $subscriber, ?int $confirmationEmailId = null, ?int $confirmationPageId = null, bool $isPublicFormSend = false): bool {
    if (isset($this->sentEmails[$subscriber->getId()])) {
      return true;
    }
    return $this->sendConfirmationEmail($subscriber, $confirmationEmailId, $confirmationPageId, $isPublicFormSend);
  }

  public function clearSentEmailsCache(): void {
    $this->sentEmails = [];
  }

  /**
   * Send confirmation email using WooCommerce email system.
   *
   * @return string
   */
  protected function sendWCConfirmationEmail(SubscriberEntity $subscriber, ?int $confirmationPageId = null): string {
    try {
      if (!function_exists('WC')) {
        return self::WC_CONFIRMATION_UNAVAILABLE;
      }

      $wc = WC();
      if (!$wc || !method_exists($wc, 'mailer')) {
        return self::WC_CONFIRMATION_UNAVAILABLE;
      }

      $mailer = $wc->mailer();
      $emails = $mailer->get_emails();

      if (!isset($emails['mailpoet_marketing_confirmation'])) {
        return self::WC_CONFIRMATION_UNAVAILABLE;
      }

      /** @var \MailPoet\WooCommerce\Emails\MarketingConfirmation $email */
      $email = $emails['mailpoet_marketing_confirmation'];

      $subscriber_email = $subscriber->getEmail();
      $activation_link = $this->subscriptionUrlFactory->getConfirmationUrl($subscriber, $confirmationPageId);
      $subscriber_firstname = $subscriber->getFirstName() ?: '';

      if (!$email->trigger($subscriber_email, $activation_link, $subscriber_firstname)) {
        return self::WC_CONFIRMATION_FAILED;
      }

      return self::WC_CONFIRMATION_SENT;

    } catch (\Exception $e) {
      $this->loggerFactory->getLogger(LoggerFactory::TOPIC_SENDING)->error(
        'MailPoet WC Marketing Confirmation Email Error: ' . $e->getMessage(),
        ['error' => $e, 'subscriber_id' => $subscriber->getId()]
      );
      return self::WC_CONFIRMATION_FAILED;
    }
  }

  public function buildEmailData(string $subject, string $html, string $text): array {
    return [
      'subject' => $subject,
      'body' => [
        'html' => $html,
        'text' => $text,
      ],
    ];
  }

  public function getMailBody(array $signupConfirmation, SubscriberEntity $subscriber, array $segmentNames, ?int $confirmationPageId = null): array {
    $body = nl2br($signupConfirmation['body']);

    // replace list of segments shortcode
    $body = str_replace(
      '[lists_to_confirm]',
      '<strong>' . join(', ', $segmentNames) . '</strong>',
      $body
    );

    // replace activation link
    $body = Helpers::replaceLinkTags(
      $body,
      $this->subscriptionUrlFactory->getConfirmationUrl($subscriber, $confirmationPageId),
      ['target' => '_blank'],
      'activation_link'
    );

    $subject = Shortcodes::process($signupConfirmation['subject'], null, null, $subscriber, null);

    $body = Shortcodes::process($body, null, null, $subscriber, null);

    //create a text version. @ is important here, Html2Text throws warnings
    $text = @Html2Text::convert(
      (mb_detect_encoding($body, 'UTF-8', true)) ? $body : mb_convert_encoding($body, 'UTF-8', mb_list_encodings()),
      true
    );

    return $this->buildEmailData($subject, $body, $text);
  }

  public function getMailBodyWithCustomizer(SubscriberEntity $subscriber, array $segmentNames, ?NewsletterEntity $newsletter = null, ?int $confirmationPageId = null): array {
    if ($newsletter === null) {
      $newsletter = $this->confirmationEmailCustomizer->getNewsletter();
    }

    $renderedNewsletter = $this->confirmationEmailCustomizer->render($newsletter);

    $stringBody = Helpers::joinObject($renderedNewsletter);

    // replace list of segments shortcode
    $body = (string)str_replace(
      '[lists_to_confirm]',
      join(', ', $segmentNames),
      $stringBody
    );

    // replace activation link
    $body = (string)str_replace(
      [
        'http://[activation_link]', // See MAILPOET-5253
        '[activation_link]',
      ],
      $this->subscriptionUrlFactory->getConfirmationUrl($subscriber, $confirmationPageId),
      $body
    );

    [
      $html,
      $text,
      $subject,
    ] = Helpers::splitObject(Shortcodes::process($body, null, $newsletter, $subscriber, null));

    // Fallback to newsletter subject if extracted subject is empty
    if (empty($subject)) {
      $subject = $newsletter->getSubject();
    }
    // Final fallback to default subject if still empty
    if (empty($subject)) {
      $subject = $this->settings->get('signup_confirmation.subject', __('Confirm your subscription', 'mailpoet'));
    }

    return $this->buildEmailData($subject, $html, $text);
  }

  /**
   * @param SubscriberEntity $subscriber The subscriber to send the confirmation email to.
   * @param int|null $confirmationEmailId Optional ID of a specific confirmation email newsletter to use.
   * @param int|null $confirmationPageId Optional ID of a specific page to use for the confirmation link.
   * @throws \Exception if unable to send the email.
   */
  public function sendConfirmationEmail(SubscriberEntity $subscriber, ?int $confirmationEmailId = null, ?int $confirmationPageId = null, bool $isPublicFormSend = false) {
    $signupConfirmation = $this->settings->get('signup_confirmation');
    if ((bool)$signupConfirmation['enabled'] === false) {
      return false;
    }

    if ($isPublicFormSend) {
      return $this->subscribersRepository->sendPublicConfirmationEmailWithCap(
        $subscriber,
        self::MAX_CONFIRMATION_EMAILS,
        function() use ($subscriber, $signupConfirmation, $confirmationEmailId, $confirmationPageId): bool {
          $sent = $this->sendConfirmationEmailMessage($subscriber, $signupConfirmation, $confirmationEmailId, $confirmationPageId);
          if ($sent) {
            $this->sentEmails[$subscriber->getId()] = true;
          }
          return $sent;
        }
      );
    }

    if (
      !$this->wp->isUserLoggedIn()
      && $subscriber->getConfirmationsCount() >= self::MAX_CONFIRMATION_EMAILS
    ) {
      return false;
    }

    $sent = $this->sendConfirmationEmailMessage($subscriber, $signupConfirmation, $confirmationEmailId, $confirmationPageId);
    if (!$sent) {
      return false;
    }

    $this->recordSuccessfulConfirmationSend($subscriber);
    $this->sentEmails[$subscriber->getId()] = true;

    return true;
  }

  /**
   * @return array{status: 'sent'|'skipped'|'send_failed', reason?: string}
   * @throws \Exception if unable to send the email.
   */
  public function sendAdminConfirmationEmail(SubscriberEntity $subscriber, ?\DateTimeInterface $oldestLifecycleDate = null): array {
    $signupConfirmation = $this->settings->get('signup_confirmation');
    if ((bool)$signupConfirmation['enabled'] === false) {
      return ['status' => 'skipped', 'reason' => 'confirmation_disabled'];
    }

    $claim = $this->subscribersRepository->claimAdminConfirmationEmailResend(
      $subscriber,
      self::MAX_CONFIRMATION_EMAILS,
      Carbon::now()->subDays(self::ADMIN_CONFIRMATION_RESEND_INTERVAL_DAYS)->millisecond(0),
      $oldestLifecycleDate
    );
    if (!$claim['claimed']) {
      return ['status' => 'skipped', 'reason' => $claim['reason'] ?? 'not_found'];
    }

    try {
      $sent = $this->sendConfirmationEmailMessage($subscriber, $signupConfirmation);
    } catch (\Throwable $throwable) {
      $this->releaseAdminConfirmationEmailClaim($subscriber, $claim);
      throw $throwable;
    }

    if (!$sent) {
      $this->releaseAdminConfirmationEmailClaim($subscriber, $claim);
      return ['status' => 'send_failed', 'reason' => 'sending_method'];
    }

    $this->completeAdminConfirmationEmailClaim($subscriber, $claim);
    $this->sentEmails[$subscriber->getId()] = true;
    return ['status' => 'sent'];
  }

  /** @param array{claim_time?: string, previous_last_confirmation_email_sent_at?: string|null, previous_count_confirmations?: int} $claim */
  private function releaseAdminConfirmationEmailClaim(SubscriberEntity $subscriber, array $claim): void {
    if (!isset($claim['claim_time'], $claim['previous_count_confirmations'])) {
      return;
    }
    $this->subscribersRepository->releaseAdminConfirmationEmailResendClaim(
      $subscriber,
      (string)$claim['claim_time'],
      $claim['previous_last_confirmation_email_sent_at'] ?? null,
      (int)$claim['previous_count_confirmations']
    );
  }

  /** @param array{claim_time?: string, previous_last_confirmation_email_sent_at?: string|null, previous_count_confirmations?: int} $claim */
  private function completeAdminConfirmationEmailClaim(SubscriberEntity $subscriber, array $claim): void {
    if (!isset($claim['claim_time'], $claim['previous_count_confirmations'])) {
      return;
    }
    $this->subscribersRepository->completeAdminConfirmationEmailResendClaim(
      $subscriber,
      (string)$claim['claim_time'],
      $claim['previous_last_confirmation_email_sent_at'] ?? null,
      (int)$claim['previous_count_confirmations']
    );
  }

  /**
   * @throws \Exception if unable to send the email.
   */
  private function sendConfirmationEmailMessage(SubscriberEntity $subscriber, array $signupConfirmation, ?int $confirmationEmailId = null, ?int $confirmationPageId = null): bool {
    $authorizationEmailsValidation = $this->settings->get(AuthorizedEmailsController::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING);
    $unauthorizedSenderEmail = isset($authorizationEmailsValidation['invalid_sender_address']);
    if (Bridge::isMPSendingServiceEnabled() && $unauthorizedSenderEmail) {
      return false;
    }

    // Try to send using WooCommerce email first. Only available in Garden environment.
    // Skip WC path when a per-list confirmation email is set, since WC doesn't support custom templates.
    if ($confirmationEmailId === null) {
      $wcConfirmationEmailResult = $this->sendWCConfirmationEmail($subscriber, $confirmationPageId);
      if ($wcConfirmationEmailResult === self::WC_CONFIRMATION_SENT) {
        return true;
      }
    }

    $segments = $subscriber->getSegments()->toArray();
    $segmentNames = array_map(function(SegmentEntity $segment) {
      return $segment->getName();
    }, $segments);

    $email = $this->getConfirmationEmailBody($signupConfirmation, $subscriber, $segmentNames, $confirmationEmailId, $confirmationPageId);

    // send email
    $extraParams = [
      'meta' => $this->mailerMetaInfo->getConfirmationMetaInfo($subscriber),
    ];

    // Don't attempt to send confirmation email when sending is paused
    $confirmationEmailErrorMessage = __('There was an error when sending a confirmation email for your subscription. Please contact the website owner.', 'mailpoet');
    if (MailerLog::isSendingPaused()) {
      throw new \Exception($confirmationEmailErrorMessage);
    }

    try {
      $defaultMailer = $this->mailerFactory->getDefaultMailer();
      $result = $defaultMailer->send($email, $subscriber, $extraParams);
    } catch (\Exception $e) {
      MailerLog::processTransactionalEmailError(MailerError::OPERATION_CONNECT, $e->getMessage(), $e->getCode());
      throw new \Exception($confirmationEmailErrorMessage);
    }

    if ($result['response'] === false) {
      if ($result['error'] instanceof MailerError && $result['error']->getLevel() === MailerError::LEVEL_HARD) {
        MailerLog::processTransactionalEmailError($result['error']->getOperation(), (string)$result['error']->getMessage());
      }
      throw new \Exception($confirmationEmailErrorMessage);
    };

    // E-mail was successfully sent we need to update the MailerLog
    MailerLog::incrementSentCount();

    return true;
  }

  private function recordSuccessfulConfirmationSend(SubscriberEntity $subscriber): void {
    if ($this->wp->isUserLoggedIn()) {
      return;
    }
    $subscriber->setConfirmationsCount($subscriber->getConfirmationsCount() + 1);
    $this->subscribersRepository->persist($subscriber);
    $this->subscribersRepository->flush();
  }

  /**
   * Determines which confirmation email body to use based on settings and optional override.
   */
  private function getConfirmationEmailBody(array $signupConfirmation, SubscriberEntity $subscriber, array $segmentNames, ?int $confirmationEmailId = null, ?int $confirmationPageId = null): array {
    // If a specific confirmation email ID is provided, try to use it
    if ($confirmationEmailId !== null) {
      $newsletter = $this->newslettersRepository->findOneById($confirmationEmailId);
      if ($newsletter !== null && $newsletter->getType() === NewsletterEntity::TYPE_CONFIRMATION_EMAIL_CUSTOMIZER) {
        return $this->getMailBodyWithCustomizer($subscriber, $segmentNames, $newsletter, $confirmationPageId);
      }
    }

    // Fall back to global settings
    $IsConfirmationEmailCustomizerEnabled = (bool)$this->settings->get(ConfirmationEmailCustomizer::SETTING_ENABLE_EMAIL_CUSTOMIZER, false);

    return $IsConfirmationEmailCustomizerEnabled ?
      $this->getMailBodyWithCustomizer($subscriber, $segmentNames, null, $confirmationPageId) :
      $this->getMailBody($signupConfirmation, $subscriber, $segmentNames, $confirmationPageId);
  }
}