<?php
/**
 * EmailTracker — transparent email engagement tracking.
 *
 * - Embeds a 1×1 pixel in outgoing HTML emails.
 * - Wraps all <a href> links with a redirect URL that records the click.
 * - Exposes /cartcue-track/ rewrite rules for open and click recording.
 * - Does NOT track test emails (_is_test flag) or unsubscribed addresses.
 *
 * @package CartCue
 */

namespace CartCue;

defined( 'ABSPATH' ) || exit;

final class EmailTracker {

	public static function init(): void {
		add_action( 'init',                     [ __CLASS__, 'register_rewrite_rules' ] );
		add_action( 'template_redirect',        [ __CLASS__, 'maybe_handle_request' ], 0 );
		add_filter( 'cartcue_pre_send_email', [ __CLASS__, 'inject_tracking' ], 10, 3 );
		add_action( 'init', static function (): void {
			Migrations::import_legacy_flags();
			$migration = 'rewrite_flushed_' . CARTCUE_VERSION;
			if ( ! Migrations::has_run( $migration ) ) {
				flush_rewrite_rules( false );
				Migrations::mark_run( $migration );
			}
		}, 999 );
	}

	// -------------------------------------------------------------------------
	// Rewrite rules
	// -------------------------------------------------------------------------

	public static function register_rewrite_rules(): void {
		add_rewrite_rule(
			'^cartcue-track/(open|click)/?$',
			'index.php?cartcue_track_type=$matches[1]',
			'top'
		);
		add_rewrite_tag( '%cartcue_track_type%', '(open|click)' );
	}

	public static function maybe_handle_request(): void {
		$type = get_query_var( 'cartcue_track_type' );
		if ( ! $type ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public email tracking pixel/link.
		$token = sanitize_text_field( wp_unslash( $_GET['t'] ?? '' ) );
		if ( ! $token ) {
			wp_die( '', '', [ 'response' => 404 ] );
		}

		if ( 'open' === $type ) {
			self::record_open( $token );
			// Serve a transparent 1×1 GIF.
			header( 'Content-Type: image/gif' );
			header( 'Cache-Control: no-store, no-cache' );
			// phpcs:ignore WordPress.Security.EscapeOutput
			echo base64_decode( 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' );
			exit;
		}

		if ( 'click' === $type ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- public email tracking link; validated via esc_url_raw().
			$raw = isset( $_GET['url'] ) ? wp_unslash( $_GET['url'] ) : '';
			$url = $raw ? rawurldecode( (string) $raw ) : '';
			self::record_click( $token );
			self::redirect_click_target( $url );
		}
	}

	/**
	 * Redirect to the original link (including external URLs).
	 *
	 * @param string $url Destination from the tracking query string.
	 */
	private static function redirect_click_target( string $url ): void {
		$url = esc_url_raw( $url );
		if ( ! $url || ! wp_http_validate_url( $url ) ) {
			wp_safe_redirect( home_url() );
			exit;
		}

		$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
		if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
			wp_safe_redirect( home_url() );
			exit;
		}

		// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- external merchant/product links must work.
		wp_redirect( $url, 302 );
		exit;
	}

	// -------------------------------------------------------------------------
	// Token management
	// -------------------------------------------------------------------------

	/**
	 * Create a tracking record and return the generated token.
	 *
	 * @param int    $workflow_id
	 * @param string $customer_email
	 * @param string $subject
	 * @return string
	 */
	public static function create_record( int $workflow_id, string $customer_email, string $subject ): string {
		global $wpdb;

		$token = wp_generate_password( 32, false );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->insert(
			DB::email_tracking(),
			[
				'workflow_id'    => $workflow_id,
				'customer_email' => sanitize_email( $customer_email ),
				'token'          => $token,
				'subject'        => sanitize_text_field( $subject ),
				'sent_at'        => current_time( 'mysql' ),
			],
			[ '%d', '%s', '%s', '%s', '%s' ]
		);

		return $token;
	}

	private static function record_open( string $token ): void {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET opens_count = opens_count + 1, opened_at = COALESCE(opened_at, %s) WHERE token = %s',
				DB::email_tracking(),
				current_time( 'mysql' ),
				$token
			)
		);
	}

	private static function record_click( string $token ): void {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET clicks = clicks + 1 WHERE token = %s',
				DB::email_tracking(),
				$token
			)
		);
	}

	// -------------------------------------------------------------------------
	// Email injection (called from Mailer)
	// -------------------------------------------------------------------------

	/**
	 * Filter hook: inject pixel and wrap links in email HTML.
	 *
	 * @param string $html
	 * @param int    $workflow_id
	 * @param array  $context  Must include 'customer_email', 'subject'. Skip if '_is_test' set.
	 * @return string
	 */
	public static function inject_tracking( string $html, int $workflow_id, array $context ): string {
		if ( ! empty( $context['_is_test'] ) || ! $workflow_id || ! empty( $context['is_plain'] ) ) {
			return $html;
		}

		$email   = $context['customer_email'] ?? '';
		$subject = $context['subject']        ?? '';

		if ( ! is_email( $email ) ) {
			return $html;
		}

		$token = self::create_record( $workflow_id, $email, $subject );

		// Tracking pixel — append before closing </body>.
		$pixel_url = add_query_arg( 't', $token, home_url( '/cartcue-track/open/' ) );
		$pixel     = sprintf( '<img src="%s" width="1" height="1" alt="" style="display:none">', esc_url( $pixel_url ) );
		if ( false !== stripos( $html, '</body>' ) ) {
			$html = preg_replace( '#</body>#i', $pixel . '</body>', $html, 1 );
		} else {
			$html .= $pixel;
		}

		// Wrap all anchor hrefs with click-tracking redirect.
		$html = preg_replace_callback(
			'/<a\s([^>]*\s)?href=["\']([^"\']+)["\']([^>]*)>/i',
			function ( $matches ) use ( $token ) {
				$href = $matches[2];
				// Skip anchors, mailto:, tel:, unsubscribe links.
				if ( 0 === strpos( $href, '#' ) || 0 === strpos( $href, 'mailto:' ) || 0 === strpos( $href, 'tel:' ) ) {
					return $matches[0];
				}
				if ( false !== strpos( $href, 'cartcue_unsubscribe=' ) ) {
					return $matches[0];
				}
				$tracked = add_query_arg( [
					't'   => $token,
					'url' => rawurlencode( $href ),
				], home_url( '/cartcue-track/click/' ) );
				return sprintf( '<a %shref="%s"%s>', $matches[1], esc_url( $tracked ), $matches[3] );
			},
			$html
		);

		return $html;
	}

	// -------------------------------------------------------------------------
	// Reporting helpers
	// -------------------------------------------------------------------------

	/**
	 * Return aggregated 30-day stats: sent, opens, clicks, open_rate, click_rate.
	 *
	 * @param int $workflow_id  0 = all workflows.
	 * @return array
	 */
	public static function get_stats( int $workflow_id = 0 ): array {
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( $workflow_id ) {
			$row = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT COUNT(*) AS sent, SUM( COALESCE( opens_count, 0 ) ) AS opens, SUM(clicks) AS clicks
					 FROM %i
					 WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND workflow_id = %d',
					DB::email_tracking(),
					$workflow_id
				),
				ARRAY_A
			);
		} else {
			$row = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT COUNT(*) AS sent, SUM( COALESCE( opens_count, 0 ) ) AS opens, SUM(clicks) AS clicks
					 FROM %i
					 WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)',
					DB::email_tracking()
				),
				ARRAY_A
			);
		}
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$sent   = (int) ( $row['sent']   ?? 0 );
		$opens  = (int) ( $row['opens']  ?? 0 );
		$clicks = (int) ( $row['clicks'] ?? 0 );

		return [
			'sent'       => $sent,
			'opens'      => $opens,
			'clicks'     => $clicks,
			'open_rate'  => $sent > 0 ? round( ( $opens  / $sent ) * 100, 1 ) : 0,
			'click_rate' => $sent > 0 ? round( ( $clicks / $sent ) * 100, 1 ) : 0,
		];
	}

	/**
	 * Find the most recent workflow_id that emailed a given address within $days days.
	 *
	 * Used by ConversionTracker to attribute an order to a play.
	 *
	 * @param string $email
	 * @param int    $days  Attribution window (default 14).
	 * @return int  workflow_id, or 0 if none found.
	 */
	public static function find_recent_workflow( string $email, int $days = 14 ): int {
		global $wpdb;

		$email = sanitize_email( $email );
		$table = DB::email_tracking();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$id = (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT workflow_id FROM %i
				 WHERE customer_email = %s
				   AND sent_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
				   AND clicks > 0
				 ORDER BY sent_at DESC
				 LIMIT 1',
				$table,
				$email,
				$days
			)
		);
		if ( $id ) {
			return $id;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$id = (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT workflow_id FROM %i
				 WHERE customer_email = %s
				   AND sent_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
				   AND opened_at IS NOT NULL
				 ORDER BY sent_at DESC
				 LIMIT 1',
				$table,
				$email,
				$days
			)
		);
		if ( $id ) {
			return $id;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$id = (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT workflow_id FROM %i
				 WHERE customer_email = %s
				   AND sent_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
				 ORDER BY sent_at DESC
				 LIMIT 1',
				$table,
				$email,
				$days
			)
		);
		if ( $id ) {
			return $id;
		}

		return 0;
	}

	/**
	 * Whether this play previously sent an email the customer opened.
	 *
	 * @param string $email
	 * @param int    $workflow_id
	 */
	public static function customer_has_opened( string $email, int $workflow_id ): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$found = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT id FROM %i
				 WHERE customer_email = %s AND workflow_id = %d AND opened_at IS NOT NULL
				 LIMIT 1',
				DB::email_tracking(),
				sanitize_email( $email ),
				$workflow_id
			)
		);

		return (bool) $found;
	}

	/**
	 * Whether this play previously sent an email with at least one click.
	 *
	 * @param string $email
	 * @param int    $workflow_id
	 */
	public static function customer_has_clicked( string $email, int $workflow_id ): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$found = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT id FROM %i
				 WHERE customer_email = %s AND workflow_id = %d AND clicks > 0
				 LIMIT 1',
				DB::email_tracking(),
				sanitize_email( $email ),
				$workflow_id
			)
		);

		return (bool) $found;
	}

	/**
	 * Whether the customer opened any CartCue email within the last N days.
	 */
	public static function customer_opened_within_days( string $email, int $days ): bool {
		global $wpdb;
		$days = max( 1, $days );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$found = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT id FROM %i
				 WHERE customer_email = %s AND opened_at IS NOT NULL
				   AND sent_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
				 LIMIT 1',
				DB::email_tracking(),
				sanitize_email( $email ),
				$days
			)
		);

		return (bool) $found;
	}
}
