<?php
/**
 * Abstract mailer class.
 */

declare(strict_types=1);
namespace FeData\FastEvents\Mail;

defined( 'ABSPATH' ) || exit;

use Exception;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Storage\Options;
use FeData\FastEvents\Storage\PluginSettings;
use FeData\FastEvents\Utilities\TimeUtil;
use JsonException;
use stdClass;
use WP_Error;

/**
 * Abstract mailer class.
 */
abstract class AbstractMailer implements MailerInterface {

	protected const CONNECTION_TIMEOUT = 15;

	/**
	 * Retry scheme.
	 *
	 * @since 1.0
	 * @var array
	 */
	private array $retry_scheme = array();

	/**
	 * Retry position.
	 *
	 * @since 1.0
	 * @var int
	 */
	private int $retry_position = -1;

	/**
	 * Creation date of the email
	 *
	 * @since 2.5.0
	 * @var string
	 */
	protected string $date_created;

	/**
	 * Provider email id
	 *
	 * @since 2.5.0
	 * @var string
	 */
	protected string $email_id;

	/**
	 * Provider id
	 *
	 * @since 2.5.0
	 * @var string
	 */
	protected string $provider_id;

	/**
	 * AbstractMailer constructor.
	 *
	 * @since 1.0
	 *
	 * @param Options        $options  The options.
	 * @param PluginSettings $settings The settings.
	 * @param LoggerFactory  $logger   The logger.
	 */
	public function __construct(
		protected readonly Options $options,
		protected readonly PluginSettings $settings,
		protected readonly LoggerFactory $logger
	) {}

	/**
	 * Send HTML Email
	 *
	 * @since 1.0
	 *
	 * @param string $to          Emailadress.
	 * @param string $subject     Email subject.
	 * @param string $message     HTML formatted message.
	 * @throws Exception          If mail can't be sent.
	 */
	public function send_html_message( string $to, string $subject, string $message ): void {
		$this->date_created = current_time( 'mysql' );

		// Mimic an order.
		$order                  = new stdClass();
		$order->id              = '0';
		$order->event_id        = '0';
		$order->uid             = 'R4FETmO6RJtFJ1hysKikMiZLM2D1OC9C3ONSvXUt';
		$order->sender_name     = '';
		$order->sender_email    = '';
		$order->payment_status  = 'Not available';
		$order->name            = 'Not available';
		$order->email           = $to;
		$order->created_at      = wp_date( 'Y-m-d H:i:s' );
		$order->email_sent      = 1;
		$order->email_bcc       = '';
		$order->email_body      = $message;
		$order->email_subject   = $subject;
		$order->confirm_subject = '';
		$order->redirect        = '';
		$order->input_fields    = '';

		// If queues are enabled, use the action-scheduler.
		if ( $this->settings->email_queue() ) {
			$msg_id = $this->options->save_message( $order );
			$this->queue_email( $order, $msg_id );
		} else {
			// Use the email provider to send the email.
			$this->provider_send( $order, '', $subject, $message, self::FAST_EVENTS_NORMAL_EMAIL );
		}
	}

	/**
	 * Queue order email in the action-scheduler.
	 *
	 * @since 3.0.0
	 *
	 * @param stdClass $order Order information.
	 * @param int      $type  0=Normal mail, 1=Confirmation email.
	 * @throws Exception      If mail can't be sent.
	 */
	protected function queue_email( stdClass $order, int $type = self::FAST_EVENTS_NORMAL_EMAIL ): void {
		as_enqueue_async_action(
			'fast_events_action_send_mail',
			array(
				'order_id'       => (int) $order->id,
				'event_id'       => (int) $order->event_id,
				'to'             => $order->email,
				'em_type'        => $type,
				'created'        => $order->created_at,
				'retry_scheme'   => array_map( 'intval', explode( ',', $this->settings->email_retry_scheme() ) ),
				'retry_position' => -1,
			),
			'fast-events-send-mail'
		);
	}
	/**
	 * Send order email
	 *
	 * @since 3.0.0
	 *
	 * @param stdClass $order         Order information.
	 * @param int      $type          0=Normal mail, 1=Confirmation email.
	 * @param bool     $bypass_queue  true = bypass the queue even if email queues are enabled.
	 *                                Used if the method is called from the action-scheduler.
	 *                                If we didn't do that, it would be queued again and result in an endless loop.
	 * @throws Exception      If mail can't be sent.
	 */
	public function send_email( stdClass $order, int $type = self::FAST_EVENTS_NORMAL_EMAIL, bool $bypass_queue = false ): void {
		// If email queues are enabled, use the action-scheduler.
		if ( $this->settings->email_queue() && ! $bypass_queue ) {
			$this->queue_email( $order, $type );
		} else {
			$this->prepare_email( $order, $type );
		}
	}


	/**
	 * Send order email
	 *
	 * @since 1.0
	 * @since 1.2.2 Added '{%YEAR%}' substitution.
	 * @since 2.0.3 Improved comment for a type of email.
	 * @since 2.1.0 Added {%PERSONALISE%} keyword.
	 * @since 2.5.0 Set email_log values.
	 *
	 * @param stdClass $order Order information.
	 * @param int      $type  0=Normal mail, 1=Confirmation email.
	 * @throws Exception      If mail can't be sent.
	 */
	protected function prepare_email( stdClass $order, int $type = self::FAST_EVENTS_NORMAL_EMAIL ): void {
		$this->date_created = current_time( 'mysql' );

		// Create the HTML for the fixed fields.
		$flds = '<table style="border-collapse: collapse;">' .
				'<tr><td style="padding-right:15px;">' . esc_html__( 'Name', 'fast-events' ) . ':</td><td><b>' . $order->name . '</b></td></tr>' .
				'<tr><td style="padding-right:15px;">' . esc_html__( 'Email', 'fast-events' ) . ':</td><td><b>' . $order->email . '</b></td></tr>';

		// Add the available optional fields.
		$fields = json_decode( $order->input_fields );
		if ( ! empty( $fields->fields ) ) {
			foreach ( $fields->fields as $item ) {
				$flds .= "<tr><td style='padding-right:15px;'>$item->name:</td><td><b>$item->value</b></td></tr>";
			}
		}
		$flds .= '</table>';

		/*
		 * What type of email are we dealing with?
		 * This can be a normal email (=0) or any other random number which is used for a free format email.
		 * The latter is substituted in the subject and the body, and if the setting 'email_retries' is on.
		 * The message is saved in the options table, so the action scheduler can pick up its contents
		 * and retry sending it again.
		 */
		$bcc = '';
		if ( self::FAST_EVENTS_CONFIRM_EMAIL !== $type ) {

			// Is there a BCC?
			if ( ! empty( $order->email_bcc ) ) {
				$bcc = $order->email_bcc;
			}
			$subject_type = $order->email_subject;
			$body_type    = $order->email_body;
		} else {

			// Is there a confirmation BCC?
			if ( ! empty( $order->confirm_bcc ) ) {
				$bcc = $order->confirm_bcc;
			}
			$subject_type = $order->confirm_subject;
			$body_type    = $order->confirm_body;
		}

		// Replace all the search strings in the body.
		$pers_url  = $order->redirect . '?redirect_uid=' . $order->uid;
		$order_url = rest_url( 'fast-events/v1/tickets/' . $order->uid );
		$html      = '<div><a style="background-color:#7892c2;border-radius:10px;border:1px solid #4e6096;' .
					'display:inline-block;color:#ffffff;font-size:16px;padding:12px;text-decoration:none;" href="' .
					$order_url . '">' . esc_html__( 'Download tickets', 'fast-events' ) . '</a></div><div style="margin-bottom:20px;">' .
					'<a style="font-size:80%;color:#808080;" href="' . $order_url . '">' . $order_url . '</a></div>';
		$pers_html = '<div><a style="background-color:#7892c2;border-radius:10px;border:1px solid #4e6096;' .
					'display:inline-block;color:#ffffff;font-size:16px;padding:12px;text-decoration:none;" href="' .
					$pers_url . '">' . esc_html__( 'Personalise tickets', 'fast-events' ) . '</a></div><div style="margin-bottom:20px;">' .
					'<a style="font-size:80%;color:#808080;" href="' . $pers_url . '">' . $pers_url . '</a></div>';
		$body      = str_replace(
			array( '{%NAME%}', '{%EMAIL%}', '{%PERSONALISE%}', '{%TICKETS%}', '{%DOWNLOAD-TICKETS%}', '{%INVOICE%}', '{%CONFIRM%}', '{%FIELDS%}', '{%YEAR%}' ),
			array(
				$order->name,
				$order->email,
				$pers_html,
				$order_url,
				$html,
				rest_url( 'fast-events/v1/invoice/' . $order->uid ),
				rest_url( 'fast-events/v1/payment/email/confirm/' . $order->uid ),
				$flds,
				gmdate( 'Y' ),
			),
			urldecode( $body_type )
		);

		// Create the full HTML structure.
		$tmp_body = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' .
					'<html xmlns="http://www.w3.org/1999/xhtml" lang="' . get_bloginfo( 'language' ) . '">' .
					'<head>' .
					'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' .
					'<title>' . $subject_type . '</title>' .
					'<meta name="viewport" content="width=device-width, initial-scale=1.0"/>' .
					'</head>' .
					'<body style="width:100%;max-width:600px;">' .
					$body .
					'</body></html>';

		// Use the email provider to send the email.
		$this->provider_send( $order, $bcc, $subject_type, $tmp_body, $type );
	}
	/**
	 * Abstract function the provider must implement
	 *
	 * @since 2.4.0
	 * @since 2.5.0 Set provider id.
	 *
	 * @param stdClass $order   Order information.
	 * @param string   $bcc     Emailaddress of BBC receiver.
	 * @param string   $subject Subject of the email.
	 * @param string   $msg     HTML mail message.
	 * @param int      $em_type 0=Normal mail, 1=Confirmation email.
	 * @throws Exception If mail can't be sent.
	 */
	abstract protected function provider_send( stdClass $order, string $bcc, string $subject, string $msg, int $em_type ): void;


	/**
	 * Email is delivered; check if this was a free format email.
	 *
	 * @since 1.0
	 * @since 2.0.3 No longer depending on retries.
	 * @since 2.5.0 Create email_log row for normal and confirmation emails.
	 *
	 * @param stdClass $order   Order information.
	 * @param int      $em_type 0=Normal mail, 1=Confirmation email, >100=Free format email.
	 */
	protected function email_delivered( stdClass $order, int $em_type ): void {
		global $wpdb;

		if ( 100 <= $em_type ) {
			$this->options->delete_message( $em_type );
			return;
		}
		$wpdb->insert(
			$wpdb->prefix . 'fe_event_email_log',
			array(
				'event_id'          => $order->event_id,
				'order_id'          => $order->id,
				'date_created'      => $this->date_created,
				'date_modified'     => $this->date_created,
				'provider_id'       => $this->provider_id,
				'email_type'        => self::FAST_EVENTS_NORMAL_EMAIL === $em_type ? 'normal' : 'confirm',
				'provider_email_id' => $this->email_id,
				'sender_email'      => $this->get_sender_email( $order ),
				'receiver_email'    => $order->email,
				'email_subject'     => self::FAST_EVENTS_NORMAL_EMAIL === $em_type ? $order->email_subject : $order->confirm_subject,
				'email_events'      => wp_json_encode( array() ),
			)
		);
	}


	/**
	 * Check if we need to execute the next retry
	 *
	 * @since 1.0
	 * @since 1.9   Added provider_name to error
	 * @since 2.0.3 Cast to int for logging
	 * @since 2.1.0 Log cast to int
	 * @since 2.2.0 - Remove log if there are no retries.
	 *              - Remove free format email if retries are not enabled.
	 *
	 * @param stdClass $order          Order information.
	 * @param string   $error_msg      Error message.
	 * @param int      $em_type        0=Normal mail, 1=Confirmation email, >100=Free format email.
	 */
	protected function check_email_retry_scheme( stdClass $order, string $error_msg, int $em_type ): void {

		// Are retries enabled?
		if ( ! $this->settings->email_retries() ) {
			if ( 100 <= $em_type ) {
				$this->options->delete_message( $em_type );
			}
			return;
		}

		// Is this the first retry?
		if ( -1 === $this->retry_position ) {
			$this->retry_scheme = array_map( 'intval', explode( ',', $this->settings->email_retry_scheme() ) );
		}
		$cnt = count( $this->retry_scheme ) - 1;

		// Are we still not at the end of the retry scheme?
		if ( $this->retry_position < $cnt ) {
			++$this->retry_position;
			$new_retry_timestamp = TimeUtil::utc_time() + ( $this->retry_scheme[ $this->retry_position ] * MINUTE_IN_SECONDS );

			// Schedule a future action retrying to send the email.
			as_schedule_single_action(
				$new_retry_timestamp,
				'fast_events_action_send_mail',
				array(
					'order_id'       => (int) $order->id,
					'event_id'       => (int) $order->event_id,
					'to'             => $order->email,
					'em_type'        => $em_type,
					'created'        => $order->created_at,
					'retry_scheme'   => $this->retry_scheme,
					'retry_position' => $this->retry_position,
				),
				'fast-events-send-mail'
			);

			// Log the retry.
			$this->logger->info(
				'Retry #' . ( $this->retry_position + 1 ) . ', ' . $error_msg,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => (int) $order->event_id,
					'order_id' => (int) $order->id,
				)
			);
		} else {

			// Give up retry and log error.
			$this->logger->alert(
				'** delivery failed after ' . ( $this->retry_position + 1 ) . ' retries **, ' . $error_msg,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => (int) $order->event_id,
					'order_id' => (int) $order->id,
				)
			);
		}
	}


	/**
	 * Error handling for API email provider
	 *
	 * @since 1.0
	 * @since 2.0.3 - Escape error
	 *              - Retries for non 200 http-codes.
	 *
	 * @param array|WP_Error $resp          Response of wp_remote_post.
	 * @param stdClass       $order         Order information.
	 * @param int            $em_type       0=Normal mail, 1=Confirmation email.
	 * @throws Exception If mail can't be sent.
	 */
	protected function check_email_provider_api_error( array|WP_Error $resp, stdClass $order, int $em_type ): void {

		// Network error?
		if ( is_wp_error( $resp ) ) {
			$error = $this->provider_id . ' API error (Order #' . $order->id . ', ' . $order->email . '): ' . $resp->get_error_message();

			// Log error.
			$this->logger->error(
				$error,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => (int) $order->event_id,
					'order_id' => (int) $order->id,
				)
			);

			// Check for a retry scheme.
			$this->check_email_retry_scheme( $order, wp_remote_retrieve_body( $resp ), $em_type );

			throw new Exception( esc_html( $error ) );
		}

		// Get the http-code.
		$http_code = (int) wp_remote_retrieve_response_code( $resp );

		/**
		 * Action to use the results of the API call
		 *
		 * @since 1.0
		 *
		 * @param int    $http_code The HTTP resultcode of the API.
		 * @param int    $order_id  The id of the order.
		 * @param string $email     Emailaddress of recipient.
		 * @param string $result    The body of the API-call result.
		 */
		do_action( 'fast_events_email_api_result', $http_code, (int) $order->id, $order->email, wp_remote_retrieve_body( $resp ) );

		// Check if the result is not OK (!2xx) => fatal, no retry.
		if ( 200 > $http_code || 299 < $http_code ) {
			try {
				$json = json_decode( wp_remote_retrieve_body( $resp ), false, 512, JSON_THROW_ON_ERROR );
			} catch ( JsonException ) {
				$json = wp_remote_retrieve_body( $resp );
			}
			$error = $this->provider_id . ' API error (Order #' . $order->id . ', ' . $order->email . '): ' .
					wp_remote_retrieve_response_code( $resp ) . ', ' .
					wp_remote_retrieve_response_message( $resp );

			// Log error.
			$this->logger->error(
				$error,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => (int) $order->event_id,
					'order_id' => (int) $order->id,
					'data'     => $json,
				)
			);

			// Check for a retry scheme.
			$this->check_email_retry_scheme( $order, wp_remote_retrieve_body( $resp ), $em_type );

			throw new Exception( esc_html( $error ) );
		} else {
			$this->set_email_id( $resp );
			$this->email_delivered( $order, $em_type );
		}
	}

	/**
	 * Extract the email_id the provider uses.
	 *
	 * @since 2.5.0
	 *
	 * @param array|WP_Error $response API response from provider.
	 */
	abstract protected function set_email_id( array|WP_Error $response ): void;


	/**
	 * Update the email log with the new event.
	 *
	 * @since 2.5.0
	 *
	 * @param string   $email_id Provider email id.
	 * @param stdClass $event    Event properties.
	 * @param int      $event_id The unique id of the event.
	 * @param int      $order_id The unique id of the order.
	 */
	protected function update_email_log( string $email_id, stdClass $event, int $event_id, int $order_id ): void {
		global $wpdb;

		// Start the transaction.
		$wpdb->query( 'START TRANSACTION' );

		// Find the email_id.
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT id, email_events FROM {$wpdb->prefix}fe_event_email_log WHERE provider_email_id = %s FOR UPDATE", $email_id ) );
		if ( empty( $row ) ) {
			$tmp_error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );
			$this->logger->warning(
				empty( $tmp_error ) ?
					/* translators: %s is the provider email id */
					sprintf( __( 'Email-id "%1$s" not found', 'fast-events' ), $email_id ) :
					/* translators: %s is the provider email id */
					sprintf( __( 'Email-id "%1$s": ', 'fast-events' ), $email_id ) . $tmp_error,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => $event_id,
					'order_id' => $order_id,
				)
			);
			return;
		}

		// Process the event.
		$json   = json_decode( $row->email_events );
		$json[] = $event;
		$resp   = $wpdb->update(
			$wpdb->prefix . 'fe_event_email_log',
			array(
				'date_modified' => current_time( 'mysql' ),
				'email_events'  => wp_json_encode( $json ),
			),
			array( 'id' => $row->id )
		);
		if ( false === $resp || 0 === $resp ) {
			$tmp_error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );
			$this->logger->warning(
				empty( $tmp_error ) ?
					/* translators: %s is the email provider event name, and the last %s is the provider email id */
					sprintf( __( 'Could not add a "%1$s" event to email-id "%2$s"', 'fast-events' ), $event, $email_id ) :
					/* translators: %s is the email provider event name, and the last %s is the provider email id */
					sprintf( __( 'Adding "%1$s" event to email-id "%1$s": ', 'fast-events' ), $event, $email_id ) . $tmp_error,
				array(
					'type'     => 'Email-' . $this->provider_id,
					'event_id' => $event_id,
					'order_id' => $order_id,
				)
			);
			return;
		}

		// Commit the transaction.
		$wpdb->query( 'COMMIT' );
	}


	/**
	 * Get the sender name from the event or general settings.
	 *
	 * @since 1.0
	 * @since 2.1 Added 'fast_events_mail_from_name' filter
	 *
	 * @param stdClass $order The order information.
	 *
	 * @return string The sender name.
	 */
	protected function get_sender_name( stdClass $order ): string {
		return apply_filters(
			'fast_events_mail_from_name',
			empty( $order->sender_name ) ? $this->settings->sender_name() : $order->sender_name,
			(int) $order->id,
			$order->payment_status,
			(int) $order->event_id,
			$order->name,
			$order->email
		);
	}


	/**
	 * Get the sender emailaddress from the event or general settings.
	 *
	 * @since 1.0
	 * @since 2.1 Added 'fast_events_mail_from' filter
	 *
	 * @param stdClass $order The order information.
	 *
	 * @return string The sender's emailaddress.
	 */
	protected function get_sender_email( stdClass $order ): string {
		return apply_filters(
			'fast_events_mail_from',
			empty( $order->sender_email ) ? $this->settings->sender_email() : $order->sender_email,
			(int) $order->id,
			$order->payment_status,
			(int) $order->event_id,
			$order->name,
			$order->email
		);
	}

	/**
	 * Set retry position.
	 *
	 * @since 3.0.0
	 *
	 * @param int $position Retry position.
	 */
	public function set_retry_position( int $position ): void {
		$this->retry_position = $position;
	}

	/**
	 * Set retry scheme.
	 *
	 * @since 3.0.0
	 *
	 * @param array $retry_scheme Retry position.
	 */
	public function set_retry_scheme( array $retry_scheme ): void {
		$this->retry_scheme = $retry_scheme;
	}
}
