<?php
/**
 * Email Dispatcher class.
 *
 * @since   5.6.0
 *
 * @package TEC\Tickets\Emails
 */

namespace TEC\Tickets\Emails;

/**
 * Class Dispatcher.
 *
 * @since   5.6.0
 *
 * @package TEC\Tickets\Emails
 */
class Dispatcher {

	/**
	 * Whether this dispatcher was used.
	 *
	 * @since 5.6.0
	 *
	 * @var bool
	 */
	protected bool $used = false;

	/**
	 * Stores the Email used in this Dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @var Email_Abstract|null
	 */
	protected ?Email_Abstract $email = null;

	/**
	 * Stores the all the headers used on the email.
	 *
	 * @since 5.6.0
	 *
	 * @var array<string,string>
	 */
	protected array $headers = [];

	/**
	 * Stores the all the attachments used on the email.
	 *
	 * @since 5.6.0
	 *
	 * @var array<string>
	 */
	protected array $attachments = [];

	/**
	 * Stores the contents to be dispatched.
	 *
	 * @since 5.6.0
	 *
	 * @var ?string
	 */
	protected ?string $content = null;

	/**
	 * Stores where we will dispatch to.
	 *
	 * @since 5.6.0
	 *
	 * @var ?string
	 */
	protected ?string $to = null;

	/**
	 * Stores the subject to be dispatched.
	 *
	 * @since 5.6.0
	 *
	 * @var ?string
	 */
	protected ?string $subject = null;

	/**
	 * Sets the Email instance that will be used for this Dispatcher, intentionally this is protected method.
	 * You can only generate a new Dispatcher from the Factory Method.
	 *
	 * @since 5.6.0
	 *
	 * @param Email_Abstract $email
	 */
	protected function set_email( Email_Abstract $email ): void {
		$this->email = $email;
	}

	/**
	 * Gets the email instance for this Dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @return Email_Abstract|null
	 */
	public function get_email(): ?Email_Abstract {
		return $this->email;
	}

	/**
	 * Prepares the current dispatcher for sending an email.
	 *
	 * @since 5.6.0
	 *
	 * @param Email_Abstract $email Email Instance used to prepare the dispatcher with.
	 */
	protected function prepare_dispatcher( Email_Abstract $email ): void {
		// Enforce text/html content type header.
		$this->add_header( 'Content-Type', 'text/html; charset=utf-8' );
		$this->set_email( $email );

		$from_email = $email->get_from_email();
		$from_name  = $email->get_from_name();

		// Add From name/email to headers if no headers set yet, and we have a valid From email address.
		if ( ! empty( $from_name ) && ! empty( $from_email ) && is_email( $from_email ) ) {
			$from_email = sanitize_email( $from_email );

			$this->add_header( 'From', sprintf(
				'%1$s <%2$s>',
				stripcslashes( $from_name ),
				$from_email
			) );

			$this->add_header( 'Reply-To', $from_email );
		}

		$this->set_to( $email->get_recipient() );
		$this->set_subject( $email->get_subject() );
		$this->set_content( $email->get_content() );
	}

	/**
	 * From a given email type instance generate a new Dispatcher and prepare it for usage.
	 *
	 * @since 5.6.0
	 *
	 * @param Email_Abstract $email
	 *
	 * @return Dispatcher
	 */
	public static function from_email( Email_Abstract $email ): Dispatcher {
		// Generate a new dispatcher every time.
		$dispatcher = tribe( static::class );

		// Prepare the dispatcher to send an email.
		$dispatcher->prepare_dispatcher( $email );

		/**
		 * Allows modifications of the Email Dispatcher to all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param Dispatcher     $dispatcher Which dispatcher instance will be used for the email sent.
		 * @param Email_Abstract $email      Which instance of the email that will be attached to this dispatcher.
		 */
		$dispatcher = apply_filters( 'tec_tickets_emails_dispatcher', $dispatcher, $email );

		$email_slug = $email->slug;

		/**
		 * Allows modifications of the Email Dispatcher specific to this Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param Dispatcher     $dispatcher Which dispatcher instance will be used for the email sent.
		 * @param Email_Abstract $email      Which instance of the email that will be attached to this dispatcher.
		 */
		return apply_filters( "tec_tickets_emails_{$email_slug}_dispatcher", $dispatcher, $email );
	}

	/**
	 * Includes a new header to be dispatched.
	 *
	 * @since 5.6.0
	 *
	 * @param string $key
	 * @param string $value
	 */
	public function add_header( string $key, string $value ): void {
		$this->headers[ $key ] = $value;
	}

	/**
	 * Includes a new attachment to be dispatched.
	 *
	 * @since 5.6.0
	 *
	 * @param string $path
	 */
	public function add_attachment( string $path ): void {
		$this->attachments[] = $path;
	}

	/**
	 * Get dispatcher headers.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function get_headers(): array {
		/**
		 * Allow filtering the Dispatcher Email "Headers" for all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param array      $headers The headers.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$headers = apply_filters( 'tec_tickets_emails_dispatcher_headers', $this->headers, $this );

		$email_slug = $this->get_email()->slug;

		/**
		 * Allow filtering the Dispatcher Email "Headers" for a specific Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param array      $headers The headers.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$headers = apply_filters( "tec_tickets_emails_dispatcher_{$email_slug}_headers", $headers, $this );

		// Enforce as an array
		return (array) $headers;
	}

	/**
	 * Get the formatted headers for this dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function get_headers_formatted(): array {
		$original_headers = $this->get_headers();
		$headers          = [];

		foreach ( $original_headers as $key => $value ) {
			$headers[] = "{$key}: {$value}";
		}

		return $headers;
	}

	/**
	 * Get email attachments.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function get_attachments(): array {
		/**
		 * Allow filtering the Dispatcher Email "Attachments" for all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param array      $attachments The attachments.
		 * @param Dispatcher $this        The Dispatcher object for this specific email.
		 */
		$attachments = apply_filters( 'tec_tickets_emails_dispatcher_attachments', $this->attachments, $this );

		$email_slug = $this->get_email()->slug;

		/**
		 * Allow filtering the Dispatcher Email "Attachments" for a specific Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param array      $attachments The attachments.
		 * @param Dispatcher $this        The Dispatcher object for this specific email.
		 */
		$attachments = apply_filters( "tec_tickets_emails_dispatcher_{$email_slug}_attachments", $attachments, $this );

		return (array) $attachments;
	}

	/**
	 * Get email recipient.
	 *
	 * @since 5.6.0
	 *
	 * @return ?string The email recipient.
	 */
	public function get_to(): ?string {
		/**
		 * Allow filtering the Dispatcher Email "To" for all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $to   The email recipient.
		 * @param Dispatcher $this The Dispatcher object for this specific email.
		 */
		$to = apply_filters( 'tec_tickets_emails_dispatcher_to', $this->to, $this );

		$email_slug = $this->get_email()->slug;

		/**
		 * Allow filtering the Dispatcher Email "To" for a specific Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $to   The email recipient.
		 * @param Dispatcher $this The Dispatcher object for this specific email.
		 */
		$to = apply_filters( "tec_tickets_emails_dispatcher_{$email_slug}_to", $to, $this );

		return null === $to ? $to : (string) $to;
	}

	/**
	 * Sets where we will dispatch to.
	 *
	 * @since 5.6.0
	 *
	 * @param string $value
	 */
	public function set_to( string $value ): void {
		$this->to = $value;
	}

	/**
	 * Get the subject of the email.
	 *
	 * @since 5.6.0
	 *
	 * @return ?string
	 */
	public function get_subject(): ?string {
		/**
		 * Allow filtering the Dispatcher Email "Subject" for all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $subject The email subject.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$subject = apply_filters( 'tec_tickets_emails_dispatcher_subject', $this->subject, $this );

		$email_slug = $this->get_email()->slug;

		/**
		 * Allow filtering the Dispatcher Email "Subject" for a specific Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $subject The email subject.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$subject = apply_filters( "tec_tickets_emails_dispatcher_{$email_slug}_subject", $subject, $this );

		return null === $subject ? $subject : (string) $subject;
	}

	/**
	 * Sets the subject for the dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @param string $value
	 */
	public function set_subject( string $value ): void {
		$this->subject = $value;
	}

	/**
	 * Get the content of the dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @return ?string
	 */
	public function get_content(): ?string {
		/**
		 * Allow filtering the Dispatcher Email "Content" for all Email Types.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $content The email subject.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$content = apply_filters( 'tec_tickets_emails_dispatcher_content', $this->content, $this );

		$email_slug = $this->get_email()->slug;

		/**
		 * Allow filtering the Dispatcher Email "Content" for a specific Email Type.
		 *
		 * Filtering the dispatcher hooks will allow you to modify the values being used for this instance of the
		 * Dispatcher.
		 *
		 * Each new email send will generate a new Dispatcher to avoid sending the same email multiple times.
		 *
		 * @since 5.6.0
		 *
		 * @param string     $content The email subject.
		 * @param Dispatcher $this    The Dispatcher object for this specific email.
		 */
		$content = apply_filters( "tec_tickets_emails_dispatcher_{$email_slug}_content", $content, $this );

		return null === $content ? $content : (string) $content;
	}

	/**
	 * Sets the content for the dispatcher.
	 *
	 * @since 5.6.0
	 *
	 * @param string $value
	 */
	public function set_content( string $value ): void {
		$this->content = $value;
	}

	/**
	 * Determines if at the current state the dispatcher can send an email.
	 *
	 * @since 5.6.0
	 *
	 * @return bool
	 */
	protected function can_send(): bool {
		if ( $this->was_used() ) {
			return false;
		}

		$email = $this->get_email();

		// We cannot send if there is no email instance attached to this dispatcher.
		if ( ! $email ) {
			return false;
		}

		$subject = $this->get_subject();
		if ( null === $subject ) {
			return false;
		}

		$to = $this->get_to();
		if ( null === $to ) {
			return false;
		}

		$content = $this->get_content();
		if ( null === $content ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if this dispatcher was used.
	 *
	 * @since 5.6.0
	 *
	 * @return bool
	 */
	protected function was_used(): bool {
		return $this->used;
	}

	/**
	 * Please don't use this method unless you know why you are marking the dispatcher as not used.
	 *
	 * Once a dispatcher was used it should not be re-used, since there is no way to determine for certain that
	 * the wp_mail was actually sent at this moment.
	 *
	 * If you need to make another email using a similar dispatcher please make use of the Email associated with
	 * this dispatcher by `Dispatcher::from_email( $dispatcher->get_email() )->send()` which will generate a new dispatcher
	 * and attempt to send the email.
	 *
	 * @since 5.6.0
	 */
	public function dangerously_mark_as_not_used(): void {
		$this->used = false;
	}

	/**
	 * Send an email.
	 *
	 * @since 5.6.0
	 * @since 5.8.3 Decodes the subject before sending the email.
	 * @since 5.9.1 Removes slashes from the subject before sending the email.
	 *
	 * @return bool Whether the email was sent successfully.
	 */
	public function send(): bool {
		if ( ! $this->can_send() ) {
			return false;
		}

		// Handle any encoded characters or slashes in the subject.
		$subject = wp_unslash( wp_specialchars_decode( $this->get_subject() ) );

		$sent = (bool) wp_mail(
			$this->get_to(),
			$subject,
			$this->get_content(),
			$this->get_headers_formatted(),
			$this->get_attachments()
		);

		$this->used = true;

		return $sent;
	}
}
