<?php
/**
 * Abstract class for email notifications.
 *
 * @package Awesomemotive\Rewardswp\Abstracts
 * @since 1.0.0
 */

namespace Awesomemotive\Rewardswp\Abstracts;

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

use Awesomemotive\Rewardswp\Factories\ViewsFactory;
use Awesomemotive\Rewardswp\GlobalConfig;
use Awesomemotive\Rewardswp\Interfaces\NotificationWithMergeTagsInterface;
use Awesomemotive\Rewardswp\Services\EmailsService;
use Awesomemotive\Rewardswp\Settings\Options\Emails\FromEmailOption;
use Awesomemotive\Rewardswp\Settings\Options\Emails\FromNameOption;
use Awesomemotive\Rewardswp\Views\Notifications\BaseEmailView;
use function Awesomemotive\Rewardswp\Helpers\get_option_value;
use function Awesomemotive\Rewardswp\Helpers\replace_placeholders;
use function Awesomemotive\Rewardswp\Helpers\get_email_banner_data;
use function Awesomemotive\Rewardswp\Helpers\get_email_logo_data;
use function Awesomemotive\Rewardswp\Helpers\rewardswp_log_error;

/**
 * Class AbstractEmailNotification
 *
 * This abstract class defines the basic structure for email notifications.
 *
 * @since 1.0.0
 */
abstract class AbstractEmailNotification extends AbstractNotification implements NotificationWithMergeTagsInterface {
	/**
	 * True when email is being sent.
	 *
	 * @var bool
	 */
	public bool $sending = false;

	/**
	 * The content type to use.
	 *
	 * Possible options are: multipart/alternative (default), text/plain or text/html.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private string $content_type = 'multipart/alternative';

	/**
	 * The email subject.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected string $subject = '';

	/**
	 * The email addresses to send the email.
	 *
	 * @since 1.0.0
	 *
	 * @var array|string
	 */
	protected $recipients = [];

	/**
	 * Globals.
	 *
	 * @since 1.0.0
	 *
	 * @var GlobalConfig
	 */
	protected GlobalConfig $global_config;

	/**
	 * Views Factory.
	 *
	 * @since 1.0.0
	 *
	 * @var ViewsFactory
	 */
	protected ViewsFactory $views_factory;

	/**
	 * Emails Service.
	 *
	 * @since 1.1.0
	 *
	 * @var EmailsService|null
	 */
	protected EmailsService $emails_service;

	/**
	 * Constructor.
	 *
	 * @param GlobalConfig  $global_config Global config.
	 * @param ViewsFactory  $views_factory Views factory.
	 * @param EmailsService $emails_service Emails service.
	 *
	 * @return void
	 */
	public function __construct( GlobalConfig $global_config, ViewsFactory $views_factory, EmailsService $emails_service ) {
		$this->global_config  = $global_config;
		$this->views_factory  = $views_factory;
		$this->emails_service = $emails_service;

		// Handle multipart emails filter.
		add_filter( 'phpmailer_init', [ $this, 'filter_handle_multipart' ] );
		add_filter( 'wp_mail_from', [ $this, 'filter_wp_mail_from' ] );
		add_filter( 'wp_mail_from_name', [ $this, 'filter_wp_mail_from_name' ] );
	}

	/**
	 * Determine if the current email is accessible under the active license.
	 *
	 * @since 1.1.0
	 *
	 * @return bool
	 */
	public function has_access(): bool {
		return true;
	}

	/**
	 * Retrieve the feature slug associated with this email, when applicable.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_feature_slug(): string {
		return '';
	}

	/**
	 * Determine whether this email requires a paid license.
	 *
	 * @since 1.1.0
	 *
	 * @return bool
	 */
	public function requires_license(): bool {
		return '' !== $this->get_feature_slug();
	}

	/**
	 * Handle multipart mail.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $mailer PHPMailer object.
	 *
	 * @return mixed
	 */
	public function filter_handle_multipart( $mailer ) {
		if ( 'multipart/alternative' !== $this->content_type ) {
			return $mailer;
		}

		if ( ! $this->sending ) {
			return $mailer;
		}

		$plain_text_content = replace_placeholders(
			/**
			 * Allow plugins to replace the plain text content.
			 *
			 * @since 1.0.0
			 *
			 * @param string $content The plain text content.
			 */
			apply_filters( 'rewardswp_email_plain_text_template', $this->get_plain_text_template(), $this->get_name() ),
			$this->data
		);

		// phpcs:ignore WordPress.NamingConventions.ValidVariableName
		$mailer->AltBody = html_entity_decode(
			wp_specialchars_decode( $plain_text_content ),
			ENT_QUOTES,
			'UTF-8'
		);

		$this->sending = false;

		return $mailer;
	}

	/**
	 * Use the custom from_email option.
	 *
	 * @since 1.0.0
	 *
	 * @param string $from_email From email address.
	 *
	 * @return string
	 */
	public function filter_wp_mail_from( string $from_email ): string {
		$custom_from_email = get_option_value( FromEmailOption::class );

		if ( is_email( $custom_from_email ) ) {
			return $custom_from_email;
		}

		// Fall back to admin email instead of WordPress default
		$admin_email = get_option( 'admin_email' );

		return is_email( $admin_email ) ? $admin_email : $from_email;
	}

	/**
	 * Use the custom from_name option.
	 *
	 * @since 1.0.0
	 *
	 * @param string $from_name From name.
	 *
	 * @return string
	 */
	public function filter_wp_mail_from_name( string $from_name ): string {
		$custom_from_name = get_option_value( FromNameOption::class );

		if ( ! empty( $custom_from_name ) ) {
			$name = $custom_from_name;
		} else {
			// Fall back to site name instead of WordPress default
			$site_name = get_bloginfo( 'name' );
			$name      = ! empty( $site_name ) ? $site_name : $from_name;
		}

		// get_bloginfo( 'name' ) returns the stored title with HTML entities
		// (e.g. "Tom &amp; Jerry"); decode so the From header reads naturally.
		return wp_specialchars_decode( sanitize_text_field( $name ), ENT_QUOTES );
	}

	/**
	 * Clears the AltBody property of the global PHPMailer instance.
	 *
	 * Prevents the alternative plain text body from being carried over to subsequent emails.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function clear_alt_body_field(): void {
		$phpmailer = $this->global_config->get_mailer();

		if ( $phpmailer instanceof \PHPMailer\PHPMailer\PHPMailer ) {
			$phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}
	}

	/**
	 * Get custom display title for the email.
	 *
	 * Override this method in subclasses to provide a custom display title.
	 * If not overridden, the system will auto-generate a title from the email name.
	 *
	 * @since 1.1.0
	 *
	 * @return string|null Custom title or null to use auto-generated title.
	 */
	public function get_title(): ?string {
		return null;
	}

	/**
	 * Get email subject from database if available, otherwise from default.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_subject(): string {
		$db_subject = $this->get_subject_from_db();
		return $db_subject ? $db_subject : $this->get_default_subject();
	}

	/**
	 * Get HTML template from database if available, otherwise from default.
	 *
	 * @since 1.1.0
	 *
	 * @return string The HTML template.
	 */
	protected function get_html_template(): string {
		return $this->get_default_html_template();
	}

	/**
	 * Classes must add a method to retrieve the default email subject.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	abstract protected function get_default_subject(): string;

	/**
	 * Public method to get the default subject for seeding purposes.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	public function get_default_subject_for_seeding(): string {
		return $this->get_default_subject();
	}

	/**
	 * Classes must add a method to retrieve the default HTML email template.
	 *
	 * @since 1.1.0
	 *
	 * @return string The HTML template.
	 */
	abstract protected function get_default_html_template(): string;

	/**
	 * Public method to get the default HTML template for seeding purposes.
	 *
	 * @since 1.1.0
	 *
	 * @return string The HTML template.
	 */
	public function get_default_html_template_for_seeding(): string {
		return $this->get_default_html_template();
	}

	/**
	 * Classes must add a method to retrieve the plain text email template.
	 *
	 * @since 1.0.0
	 *
	 * @return string The plain text template.
	 */
	abstract protected function get_plain_text_template(): string;

	/**
	 * Classes must define the email context.
	 *
	 * @since 1.1.0
	 *
	 * @return string The email context (account, referral, points, order, marketing).
	 */
	abstract public function get_context(): string;

	/**
	 * Classes must define the recipient type.
	 *
	 * @since 1.1.0
	 *
	 * @return string The recipient type (member, admin, user).
	 */
	abstract public function get_recipient_type(): string;

	/**
	 * Get email subject from database if available and enabled.
	 *
	 * @since 1.1.0
	 *
	 * @return string|null
	 */
	protected function get_subject_from_db(): ?string {
		try {
			$email = $this->emails_service->get_email_by_name( $this->get_name() );
			return $email && $email->is_enabled && ! empty( $email->subject ) ? $email->subject : null;
		} catch ( \Exception $e ) {
			rewardswp_log_error(
				'Email DB error while retrieving subject: {message}',
				[
					'message' => $e->getMessage(),
					'name'    => $this->get_name(),
				]
			);
			return null;
		}
	}

	/**
	 * Retrieve the email content wrapped in the base email template.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_full_html(): string {
		$view = $this->views_factory->get_view( BaseEmailView::class );

		/**
		 * Allow plugins to replace the email subject.
		 *
		 * @since 1.0.0
		 *
		 * @param string $content The email subject.
		 */
		$email_subject = apply_filters( 'rewardswp_email_subject', $this->get_subject(), $this->get_name() );

		/**
		 * Allow plugins to replace the HTML email content.
		 *
		 * @since 1.0.0
		 *
		 * @param string $content The email HTML content.
		 */
		$html_template = apply_filters( 'rewardswp_email_html_template', $this->get_html_template(), $this->get_name() );

		if ( ! is_subclass_of( $view, AbstractView::class ) ) {
			return $html_template;
		}

		// Get banner data (allow preview overrides).
		$banner_data = apply_filters( 'rewardswp_email_banner_data', $this->get_banner_data(), $this->get_name() );

		// Get logo data from global settings.
		$logo_data = apply_filters( 'rewardswp_email_logo_data', $this->get_logo_data(), $this->get_name() );

		// Get brand colors from EmailsService.
		$brand_color      = $this->emails_service->get_brand_color();
		$brand_text_color = $this->emails_service->get_brand_contrasting_text_color();

		$args = [
			'email_title'             => $email_subject,
			'email_content'           => $html_template,
			'banner_data'             => $banner_data,
			'logo_data'               => $logo_data,
			'brand_color'             => $brand_color,
			'brand_text_color'        => $brand_text_color,
			// Default: do not show placeholder outside preview.
			'show_banner_placeholder' => false,
			'show_logo_placeholder'   => false,
		];

		// Allow controllers (preview) to adjust view args, e.g., enable placeholder.
		$args = apply_filters( 'rewardswp_email_view_args', $args, $this->get_name() );

		return $view->get_contents( $args );
	}

	/**
	 * Get banner data for the email.
	 *
	 * @since 1.1.0
	 *
	 * @return array|null Banner data or null if none is set.
	 */
	protected function get_banner_data(): ?array {
		return get_email_banner_data( $this->get_name() );
	}

	/**
	 * Get logo data from global settings.
	 *
	 * @since 1.1.0
	 *
	 * @return array|null Logo data or null if none is set.
	 */
	protected function get_logo_data(): ?array {
		return get_email_logo_data();
	}

	/**
	 * Set the recipient email addresses.
	 *
	 * Must be an array of emails.
	 *
	 * @since 1.0.0
	 *
	 * @param array|string $emails Array or comma-separated list of email addresses to send the message.
	 *
	 * @return self Returns the instance of the current class.
	 */
	public function set_recipient( $emails ): self {
		$this->recipients = $emails;
		return $this;
	}

	/**
	 * Retrieve the content for the email notification.
	 *
	 * The placeholders will be replaced before sending the email when the send() method is called.
	 *
	 * @since 1.0.0
	 *
	 * @return string The email content to be sent.
	 */
	public function get_content(): string {
		return replace_placeholders(
			'text/plain' === $this->content_type
				? $this->get_plain_text_template()
				: $this->get_full_html(),
			$this->get_data()
		);
	}

	/**
	 * Schedule the email notification to be sent asynchronously.
	 *
	 * @since 1.0.0
	 *
	 * @param int $delay The delay in seconds before the email is sent. Default 0 (send as soon as possible).
	 *
	 * @return bool True if the action was scheduled successfully, false otherwise.
	 */
	public function send_async( int $delay = 0 ): bool {
		// Check if email is enabled before scheduling.
		if ( ! $this->is_email_enabled() || ! $this->has_access() ) {
			return false;
		}

		if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_enqueue_async_action' ) ) {
			return false;
		}

		$action = 'rewardswp_task_send_email';
		$args   = [
			'mailer_name' => $this->get_name(),
			'recipients'  => $this->recipients,
			'data'        => $this->get_data(),
		];

		if ( $delay > 0 ) {
			as_schedule_single_action( time() + $delay, $action, $args );
			return true;
		}

		as_enqueue_async_action( $action, $args );
		return true;
	}


	/**
	 * Send the email notification with both HTML and plain text content.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $force_send Bypass is enabled check if true.
	 *
	 * @return bool Whether the message was sent or not.
	 */
	public function send( bool $force_send = false ): bool {
		// Check if email is enabled before sending.
		if ( ! $force_send && ( ! $this->is_email_enabled() || ! $this->has_access() ) ) {
			return false;
		}

		$this->sending = true;

		/**
		 * Allow plugins to replace the email subject.
		 *
		 * @since 1.0.0
		 *
		 * @param string $content The email subject.
		 */
		$email_subject = apply_filters( 'rewardswp_email_subject', $this->get_subject(), $this->get_name() );

		$sent = wp_mail(
			$this->recipients,
			replace_placeholders(
				$email_subject,
				$this->get_data()
			),
			$this->get_content()
		);

		// Remove the custom from name and email filters.
		remove_filter( 'wp_mail_from', [ $this, 'filter_wp_mail_from' ] );
		remove_filter( 'wp_mail_from_name', [ $this, 'filter_wp_mail_from_name' ] );

		// Prevent the alt body from being used for further email messages.
		$this->clear_alt_body_field();

		return $sent;
	}

	/**
	 * Check if the email is enabled in the database.
	 *
	 * @since 1.1.0
	 *
	 * @return bool True if enabled or email not found in database (default behavior), false if disabled.
	 */
	private function is_email_enabled(): bool {
		if ( ! $this->emails_service ) {
			// If no emails service, send by default (backward compatibility).
			return true;
		}

		try {
			$email = $this->emails_service->get_email_by_name( $this->get_name() );

			// If email doesn't exist in database, send by default (backward compatibility).
			if ( ! $email ) {
				return true;
			}

			// Return the enabled status from database.
			return (bool) $email->is_enabled;

		} catch ( \Exception $e ) {
			rewardswp_log_error(
				'Email status check error: {message}',
				[
					'message' => $e->getMessage(),
					'name'    => $this->get_name(),
				]
			);
			return true;
		}
	}

	/**
	 * Get available merge tags for this notification.
	 *
	 * Default implementation that extracts merge tags from get_merge_tag_definitions().
	 * Subclasses can override this method for custom behavior.
	 *
	 * @since 1.1.0
	 *
	 * @return array Array of merge tag names (without braces).
	 */
	public function get_available_merge_tags(): array {
		$definitions = $this->get_merge_tag_definitions();
		return array_column( $definitions, 'tag' );
	}

	/**
	 * Default implementation for backward compatibility.
	 *
	 * Subclasses should override this method to provide detailed merge tag definitions.
	 *
	 * @since 1.1.0
	 *
	 * @return array Array of merge tag definitions.
	 */
	public function get_merge_tag_definitions(): array {
		return [];
	}
}
