<?php
/**
 * Email Tracking Injector.
 *
 * Hooks into email sending to inject tracking pixels and wrap links for
 * open and click tracking in abandoned cart recovery emails.
 *
 * @package EDD\Pro\CartRecovery\Tracking
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\Tracking;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

use EDD\EventManagement\SubscriberInterface;
use EDD\Pro\CartRecovery\Database\Queries\RecoveryEmail as EmailQuery;
use EDD\Utils\Encryption;

/**
 * Email Tracker class.
 *
 * Injects tracking pixels and wraps links in abandoned cart recovery emails.
 *
 * @since 3.6.5
 */
final class EmailTracker implements SubscriberInterface {

	/**
	 * Stores the current email ID being processed.
	 *
	 * @var int|null
	 */
	private $current_email_id = null;

	/**
	 * Gets the events to subscribe to.
	 *
	 * @since 3.6.5
	 * @return array
	 */
	public static function get_subscribed_events(): array {
		return array(
			'phpmailer_init'            => array( 'inject_tracking', 999 ),
			'edd_acr_email_before_send' => array( 'set_email_context', 10, 1 ),
			'wp_mail_content_type'      => array( 'ensure_html_content_type', 999 ),
		);
	}

	/**
	 * Sets the email context before sending.
	 *
	 * This is called by the email service before wp_mail() to provide context.
	 *
	 * @since 3.6.5
	 * @param int $email_id The recovery email ID.
	 */
	public function set_email_context( int $email_id ): void {
		if ( ! edd_get_option( 'acr_enabled', false ) ) {
			return;
		}

		$email_query = new EmailQuery();
		$email       = $email_query->get_item( $email_id );

		if ( ! $email || empty( $email->date_created ) ) {
			return;
		}

		$this->current_email_id = $email_id;
	}

	/**
	 * Ensures content type is HTML for ACR emails.
	 *
	 * @since 3.6.5
	 * @param string $content_type The content type.
	 * @return string Modified content type.
	 */
	public function ensure_html_content_type( string $content_type ): string {
		if ( ! is_null( $this->current_email_id ) ) {
			return 'text/html';
		}

		return $content_type;
	}

	/**
	 * Injects tracking into email via PHPMailer.
	 *
	 * This is called by WordPress/WP SMTP when initializing PHPMailer.
	 *
	 * @since 3.6.5
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer The PHPMailer instance.
	 */
	public function inject_tracking( $phpmailer ): void {
		// Only track if we have an email context set.
		if ( null === $this->current_email_id ) {
			return;
		}

		// Only inject tracking for HTML emails.
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer uses camelCase.
		if ( 'text/html' !== $phpmailer->ContentType ) {
			return;
		}

		$email_id = $this->current_email_id;

		// Get the email body.
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer uses camelCase.
		$body = $phpmailer->Body;

		// Inject tracking pixel.
		$body = $this->inject_tracking_pixel( $body, $email_id );

		// Wrap links with click tracking.
		$body = $this->wrap_links( $body, $email_id );

		// Update the email body.
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer uses camelCase.
		$phpmailer->Body = $body;

		// Reset context after processing.
		$this->current_email_id = null;
	}

	/**
	 * Injects a tracking pixel into the email body.
	 *
	 * @since 3.6.5
	 * @param string $body     The email body HTML.
	 * @param int    $email_id The recovery email ID.
	 * @return string Modified email body with tracking pixel.
	 */
	private function inject_tracking_pixel( string $body, int $email_id ): string {
		$token = Encryption::encrypt( (string) $email_id );

		if ( null === $token ) {
			return $body;
		}

		$pixel_url  = rest_url( 'edd/v3/acr/record/open/' . $token );
		$pixel_html = sprintf(
			'<img src="%s" width="1" height="1" alt="" style="display:none;border:0;line-height:0;padding:0;margin:0;" />',
			esc_url( $pixel_url )
		);

		// Insert before closing body tag, or append if no body tag found.
		if ( stripos( $body, '</body>' ) !== false ) {
			$body = str_ireplace( '</body>', $pixel_html . '</body>', $body );
		} else {
			$body .= $pixel_html;
		}

		return $body;
	}

	/**
	 * Wraps links in the email with click tracking.
	 *
	 * @since 3.6.5
	 * @param string $body     The email body HTML.
	 * @param int    $email_id The recovery email ID.
	 * @return string Modified email body with tracked links.
	 */
	private function wrap_links( string $body, int $email_id ): string {
		// Match all anchor tags with href attributes.
		$pattern = '/<a\s+([^>]*?)href=["\']([^"\']+)["\']([^>]*?)>/i';

		$body = preg_replace_callback(
			$pattern,
			function ( $matches ) use ( $email_id ) {
				$before_href  = $matches[1];
				$original_url = $matches[2];
				$after_href   = $matches[3];

				// Skip if already a tracking link or certain special links.
				if ( $this->should_skip_link( $original_url ) ) {
					return $matches[0]; // Return original.
				}

				$token = Encryption::encrypt( $email_id . '|' . $original_url );

				// If encryption fails, leave the link unwrapped.
				if ( null === $token ) {
					return $matches[0];
				}

				$tracking_url = rest_url( 'edd/v3/acr/record/click/' . $token );

				// Reconstruct the anchor tag with tracking URL.
				return sprintf( '<a %shref="%s"%s>', $before_href, esc_url( $tracking_url ), $after_href );
			},
			$body
		);

		return $body;
	}

	/**
	 * Determines if a link should be skipped for tracking.
	 *
	 * @since 3.6.5
	 * @param string $url The URL to check.
	 * @return bool True if the link should be skipped.
	 */
	private function should_skip_link( string $url ): bool {
		// Skip if already a tracking link.
		if ( false !== strpos( $url, 'edd/v3/acr/record' ) ) {
			return true;
		}

		// Skip unsubscribe links (they're tracked separately via action handler).
		if ( false !== strpos( $url, 'edd_action=cart_recovery_unsubscribe' ) ) {
			return true;
		}

		// Skip restore links (they're tracked separately via action handler).
		if ( false !== strpos( $url, 'edd_action=cart_recovery_restore' ) ) {
			return true;
		}

		// Skip anchor links.
		if ( 0 === strpos( $url, '#' ) ) {
			return true;
		}

		// Skip mailto links.
		if ( 0 === stripos( $url, 'mailto:' ) ) {
			return true;
		}

		// Skip tel links.
		if ( 0 === stripos( $url, 'tel:' ) ) {
			return true;
		}

		return false;
	}
}
