<?php
/**
 * ActiveTrail Cart Handler
 *
 * @package ActiveTrail_WooCommerce
 * @since 1.0.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * ActiveTrail Cart Handler Class
 */
class ActiveTrail_Cart_Handler {

	/**
	 * API Client instance
	 *
	 * @var ActiveTrail_API_Client
	 */
	private $api_client;

	/**
	 * Plugin settings
	 *
	 * @var array
	 */
	private $settings;

	/**
	 * Constructor
	 *
	 * @param ActiveTrail_API_Client $api_client API client instance.
	 * @param array                  $settings Plugin settings.
	 */
	public function __construct( $api_client, $settings ) {
		$this->api_client = $api_client;
		$this->settings   = $settings;

		$this->init_hooks();
	}

	/**
	 * Check if cart tracking is enabled
	 *
	 * @return bool True if cart tracking is enabled.
	 */
	private function is_cart_tracking_enabled() {
		$settings = ActiveTrail_Settings::get_all();
		return isset( $settings['enable_cart_tracking'] ) && $settings['enable_cart_tracking'];
	}

	/**
	 * Handle cart updated event (legacy/unused).
	 * Must not be hooked for cart add/remove debounce flow: debounce is timer-only and does not use this throttle.
	 */
	public function handle_cart_updated() {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Check if we should throttle cart updates.
		$minutes = (int) ActiveTrail_Settings::get( 'cart_event_interval_minutes', 15 );
		$key     = 'activetrail_cart_last_sent_' . ( WC()->session ? WC()->session->get_customer_id() : get_current_user_id() );
		$last    = (int) get_transient( $key );

		if ( $last && ( time() - $last ) < ( $minutes * 60 ) ) {
			// Don't send now, too soon since last update.
			$this->log( 'Cart update throttled - last update was ' . human_time_diff( $last, time() ) . ' ago' );
			return;
		}

		// Get cart data.
		$cart_data = $this->prepare_cart_data();
		if ( empty( $cart_data ) ) {
			return;
		}

		// Send cart data to ActiveTrail.
		$this->sync_cart( $cart_data );

		// Set transient to track last sent time.
		set_transient( $key, time(), $minutes * 60 );
	}

	/**
	 * Initialize WordPress hooks
	 */
	private function init_hooks() {
		// Cart hooks.
		add_action( 'woocommerce_add_to_cart', array( $this, 'handle_add_to_cart' ), 10, 6 );
		add_action( 'woocommerce_cart_item_removed', array( $this, 'handle_cart_item_removed' ), 10, 2 );
		add_action( 'woocommerce_cart_item_restored', array( $this, 'handle_cart_item_restored' ), 10, 2 );
		add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'handle_cart_quantity_update' ), 10, 4 );

		// Session hooks.
		add_action( 'wp_loaded', array( $this, 'track_cart_session' ) );
		add_action( 'wp_logout', array( $this, 'clear_cart_tracking' ) );

		// Checkout hooks to capture email - use checkout_process which fires earlier, and prevent duplicates.
		add_action( 'woocommerce_checkout_process', array( $this, 'capture_checkout_email' ) );
		// Only use update_order_meta as fallback if checkout_process didn't fire.
		add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'handle_checkout_email' ), 5 );

		// AJAX hooks for cart updates.
		add_action( 'wp_ajax_activetrail_wc_add_to_cart', array( $this, 'handle_ajax_add_to_cart' ), 5 );
		add_action( 'wp_ajax_nopriv_activetrail_wc_add_to_cart', array( $this, 'handle_ajax_add_to_cart' ), 5 );

		// Scheduled event for abandoned cart sync.
		add_action( 'activetrail_sync_abandoned_carts', array( $this, 'sync_abandoned_carts' ) );
		
		// Debounced cart sync event.
		add_action( 'activetrail_debounced_cart_sync', array( $this, 'handle_debounced_cart_sync' ) );
	}

	/**
	 * Handle add to cart
	 *
	 * @param string $cart_item_key Cart item key.
	 * @param int    $product_id Product ID.
	 * @param int    $quantity Quantity.
	 * @param int    $variation_id Variation ID.
	 * @param array  $variation Variation data.
	 * @param array  $cart_item_data Cart item data.
	 */
	public function handle_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Track cart update.
		$this->track_cart_update();
	}

	/**
	 * Handle cart item removed
	 *
	 * @param string  $cart_item_key Cart item key.
	 * @param WC_Cart $cart Cart object.
	 */
	public function handle_cart_item_removed( $cart_item_key, $cart ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Track cart update.
		$this->track_cart_update();
	}

	/**
	 * Handle cart item restored
	 *
	 * @param string  $cart_item_key Cart item key.
	 * @param WC_Cart $cart Cart object.
	 */
	public function handle_cart_item_restored( $cart_item_key, $cart ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Track cart update.
		$this->track_cart_update();
	}

	/**
	 * Handle cart quantity update
	 *
	 * @param string  $cart_item_key Cart item key.
	 * @param int     $quantity      New quantity.
	 * @param int     $old_quantity  Old quantity.
	 * @param WC_Cart $cart          Cart object.
	 */
	public function handle_cart_quantity_update( $cart_item_key, $quantity, $old_quantity, $cart ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Track cart update.
		$this->track_cart_update();
	}

	/**
	 * Handle AJAX add to cart
	 */
	public function handle_ajax_add_to_cart() {
		// This runs before the actual add to cart, so we'll track after.
		add_action( 'woocommerce_ajax_added_to_cart', array( $this, 'handle_ajax_cart_update' ) );
	}

	/**
	 * Handle AJAX cart update
	 *
	 * @param int $product_id Product ID.
	 */
	public function handle_ajax_cart_update( $product_id ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		// Track cart update.
		$this->track_cart_update();
	}

	/**
	 * Handle checkout email capture (fallback if checkout_process didn't fire)
	 *
	 * @param int $order_id Order ID.
	 */
	public function handle_checkout_email( $order_id ) {
		// Check if we already synced during checkout_process to avoid duplicates.
		if ( WC()->session && WC()->session->get( 'activetrail_checkout_synced' ) ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( $order ) {
			$email = $order->get_billing_email();
			if ( ! empty( $email ) && WC()->session ) {
				WC()->session->set( 'checkout_email', $email );

				// Mark as synced to prevent duplicate events.
				WC()->session->set( 'activetrail_checkout_synced', true );

				// Trigger cart sync with the captured email.
				$this->trigger_cart_sync_with_email( $email );
			}
		}
	}

	/**
	 * Capture checkout email during checkout process
	 */
	public function capture_checkout_email() {
		// Prevent duplicate syncs.
		if ( WC()->session && WC()->session->get( 'activetrail_checkout_synced' ) ) {
			return;
		}

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( isset( $_POST['billing_email'] ) && ! empty( $_POST['billing_email'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$email = sanitize_email( wp_unslash( $_POST['billing_email'] ) );
			if ( WC()->session ) {
				WC()->session->set( 'checkout_email', $email );

				// Mark as synced to prevent duplicate events.
				WC()->session->set( 'activetrail_checkout_synced', true );

				// Trigger cart sync with the captured email.
				$this->trigger_cart_sync_with_email( $email );
			}
		}
	}

	/**
	 * Trigger cart sync with captured email
	 *
	 * @param string $email Customer email.
	 */
	private function trigger_cart_sync_with_email( $email ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			return;
		}

		if ( ! WC()->cart || WC()->cart->is_empty() ) {
			return;
		}

		$session_id = $this->get_session_id();
		if ( empty( $session_id ) ) {
			return;
		}

		// Update cart data with the email.
		$cart_data                   = $this->prepare_cart_data();
		$cart_data['customer_email'] = $email;

		// Store updated cart data.
		$this->store_cart_data( $session_id, $cart_data );

		// Sync to ActiveTrail ללא ת׳רוטלינג גלובלי – שינוי בצ'קאאוט הוא גם שינוי עגלה לגיטימי.
		$result = $this->sync_cart( $cart_data );

		if ( is_array( $result ) && ! empty( $result['success'] ) ) {
			$this->update_last_cart_sync_time( $session_id );
		}
	}

	/**
	 * Track cart session
	 */
	public function track_cart_session() {
		if ( ! WC()->session ) {
			return;
		}

		// Get or create session ID.
		$session_id = WC()->session->get_customer_id();

		if ( empty( $session_id ) ) {
			return;
		}

		// Store session for tracking.
		WC()->session->set( 'activetrail_session_id', $session_id );
	}

	/**
	 * Clear cart tracking on logout
	 * Also clears session-based throttling data.
	 */
	public function clear_cart_tracking() {
		if ( WC()->session ) {
			WC()->session->set( 'activetrail_session_id', null );
			WC()->session->set( 'activetrail_cart_last_hash', null );
			WC()->session->set( 'activetrail_cart_last_sync_time', null );
		}
	}

	/**
	 * Track cart update
	 */
	private function track_cart_update() {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			$this->log( 'track_cart_update skipped - cart tracking disabled', 'debug' );
			return;
		}

		if ( ! WC()->cart || WC()->cart->is_empty() ) {
			$this->log( 'track_cart_update called but cart is empty, skipping', 'debug' );
			return;
		}

		if ( method_exists( WC()->cart, 'calculate_totals' ) ) {
			WC()->cart->calculate_totals();
		}

		$session_id = $this->get_session_id();
		if ( empty( $session_id ) ) {
			$this->log( 'track_cart_update called but session_id missing, skipping', 'debug' );
			return;
		}

		// Debounce is timer-only: we do not check "last sync" or throttle here; every change schedules a new 10s event.
		// Capture the time of this specific change - used to create a distinct
		// snapshot and a distinct debounced event for this change.
		$snapshot_time = time();

		$cart_data = $this->prepare_cart_data();

		// Only log cart data in debug mode if explicitly needed (too verbose for normal operation).
		// $this->log( 'track_cart_update called with cart data: ' . wp_json_encode( $cart_data ), 'debug' );

		// Store a cart snapshot for this specific change. We keep one snapshot per change,
		// keyed by its timestamp, so שכל שינוי יקבל איוונט נפרד אחרי 10 שניות.
		$this->store_cart_snapshot_for_debounce( $session_id, $cart_data, $snapshot_time );

		// Store cart_data in a transient so cron can load it without relying on cron args
		// (WordPress cron often drops or truncates large 3rd argument when serializing).
		$transient_key = 'activetrail_debounced_cart_' . $session_id . '_' . $snapshot_time;
		set_transient( $transient_key, $cart_data, 15 * MINUTE_IN_SECONDS );

		// Schedule with only 2 args so cron storage never drops payload; callback loads from transient.
		wp_schedule_single_event(
			$snapshot_time + 10,
			'activetrail_debounced_cart_sync',
			array( $session_id, $snapshot_time )
		);
	}

	/**
	 * Store cart snapshot for debounced sync
	 *
	 * @param string $session_id Session ID.
	 * @param array  $cart_data  Cart data.
	 * @param int    $timestamp  Snapshot timestamp (per change).
	 */
	private function store_cart_snapshot_for_debounce( $session_id, $cart_data, $timestamp ) {
		$user_id = get_current_user_id();
		
		if ( $user_id > 0 ) {
			// Logged-in user: store a list of snapshots in user_meta, keyed by timestamp.
			$meta_key  = '_activetrail_cart_snapshot_' . $session_id;
			$snapshots = get_user_meta( $user_id, $meta_key, true );

			if ( ! is_array( $snapshots ) ) {
				$snapshots = array();
			}

			$snapshots[ $timestamp ] = array(
				'cart_data' => $cart_data,
				'timestamp' => $timestamp,
			);

			update_user_meta( $user_id, $meta_key, $snapshots );
		} else {
			// Guest user: store a list of snapshots in WooCommerce session.
			if ( WC()->session ) {
				$snapshots = WC()->session->get( 'activetrail_cart_snapshots' );

				if ( ! is_array( $snapshots ) ) {
					$snapshots = array();
				}

				$snapshots[ $timestamp ] = array(
					'cart_data' => $cart_data,
					'timestamp' => $timestamp,
				);

				WC()->session->set( 'activetrail_cart_snapshots', $snapshots );
			}
		}
		
		// Also store in database for abandoned cart tracking.
		$this->store_cart_data( $session_id, $cart_data );
	}

	/**
	 * Handle debounced cart sync (called by WP-Cron after 10 seconds)
	 *
	 * @param string     $session_id           Session ID.
	 * @param int|array  $snapshot_time_or_data Snapshot timestamp (per change) OR cart_data array (newer scheduler).
	 * @param array|null $maybe_cart_data     Optional cart_data when scheduled with three args.
	 */
	public function handle_debounced_cart_sync( $session_id, $snapshot_time_or_data = 0, $maybe_cart_data = null ) {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			$this->log( 'Debounced cart sync skipped - cart tracking disabled', 'debug' );
			return;
		}

		$snapshot_time = is_numeric( $snapshot_time_or_data ) ? (int) $snapshot_time_or_data : 0;

		// Resolve cart_data: (1) 3rd arg, (2) transient (we always schedule with 2 args + transient now), (3) 2nd arg as array (legacy), (4) user_meta/session/DB below.
		$cart_data = null;
		if ( func_num_args() >= 3 && is_array( $maybe_cart_data ) && ! empty( $maybe_cart_data ) ) {
			$cart_data = $maybe_cart_data;
			$this->log( 'Debounced cart sync using direct cart_data payload (3rd arg) for session: ' . $session_id, 'debug' );
		}
		if ( $cart_data === null && $snapshot_time > 0 ) {
			$transient_key = 'activetrail_debounced_cart_' . $session_id . '_' . $snapshot_time;
			$from_transient = get_transient( $transient_key );
			if ( is_array( $from_transient ) && ! empty( $from_transient ) ) {
				$cart_data = $from_transient;
				delete_transient( $transient_key );
				$this->log( 'Debounced cart sync using transient payload for session: ' . $session_id, 'debug' );
			}
		}
		if ( $cart_data === null && is_array( $snapshot_time_or_data ) && ! empty( $snapshot_time_or_data ) ) {
			$cart_data = $snapshot_time_or_data;
			$this->log( 'Debounced cart sync using direct cart_data payload (2nd arg) for session: ' . $session_id, 'debug' );
		}

		// If we already have cart data from the event payload, we can skip reading from session/user_meta.
		if ( is_array( $cart_data ) && ! empty( $cart_data ) ) {
			// Always require a valid email to sync cart - don't create placeholder contacts.
			if ( empty( $cart_data['customer_email'] ) || strpos( $cart_data['customer_email'], '@cart.placeholder' ) !== false ) {
				$this->log( 'Debounced cart sync (direct payload) skipped - valid customer email is required', 'debug' );
				return;
			}

			// When the timer fires we always send this snapshot; we do not skip based on last sync time.
			$this->log( 'Sending debounced cart sync (direct payload) for session: ' . $session_id, 'info' );

			$result = $this->sync_cart( $cart_data );

			if ( is_array( $result ) && ! empty( $result['success'] ) ) {
				$this->update_last_cart_sync_time( $session_id );
				$this->log( 'Debounced cart sync (direct payload) successful for session: ' . $session_id, 'info' );
			} else {
				$this->log( 'Debounced cart sync (direct payload) failed for session: ' . $session_id . ' with result: ' . wp_json_encode( $result ), 'error' );
			}

			return;
		}

		// Backward‑compatible path: no cart_data from payload/transient, try user/session snapshots then DB.
		// In cron there is no logged-in user; for logged-in users session_id is the user ID, so use it for user_meta lookup.
		$user_id_for_snapshot = ( get_current_user_id() > 0 ) ? get_current_user_id() : ( is_numeric( $session_id ) ? (int) $session_id : 0 );
		$snapshots = null;

		// Retrieve stored cart snapshots collection.
		if ( $user_id_for_snapshot > 0 ) {
			// Logged-in user (or cron with numeric session_id = user ID): get from user_meta.
			$snapshots = get_user_meta( $user_id_for_snapshot, '_activetrail_cart_snapshot_' . $session_id, true );
		} else {
			// Guest user: get from WooCommerce session (null in cron).
			if ( WC()->session ) {
				$snapshots = WC()->session->get( 'activetrail_cart_snapshots' );
			}
		}

		if ( empty( $snapshots ) || ! is_array( $snapshots ) ) {
			// Third fallback: load cart from database by session_id.
			global $wpdb;
			$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
			$table      = esc_sql( $table_name );
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$cart_record = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT * FROM {$table} WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is whitelisted (prefix + literal).
					$session_id
				)
			);
			if ( $cart_record && ! empty( $cart_record->cart_data ) ) {
				$cart_data = json_decode( $cart_record->cart_data, true );
				if ( is_array( $cart_data ) ) {
					$cart_data = activetrail_wc_sanitize_array( $cart_data );
				}
				if ( ! isset( $cart_data['session_id'] ) || empty( $cart_data['session_id'] ) ) {
					$cart_data['session_id'] = $cart_record->session_id;
				}
				if ( ! isset( $cart_data['customer_email'] ) || empty( $cart_data['customer_email'] ) ) {
					$cart_data['customer_email'] = $cart_record->email;
				}
				if ( ! empty( $cart_data['items'] ) && ! empty( $cart_data['customer_email'] ) && strpos( $cart_data['customer_email'], '@cart.placeholder' ) === false ) {
					$this->log( 'Debounced cart sync using DB fallback for session: ' . $session_id, 'debug' );
					$result = $this->sync_cart( $cart_data );
					if ( is_array( $result ) && ! empty( $result['success'] ) ) {
						$this->update_last_cart_sync_time( $session_id );
						$this->log( 'Debounced cart sync (DB fallback) successful for session: ' . $session_id, 'info' );
					} else {
						$this->log( 'Debounced cart sync (DB fallback) failed for session: ' . $session_id . ' with result: ' . wp_json_encode( $result ), 'error' );
					}
					return;
				}
			}
			$this->log( 'Debounced cart sync skipped - no snapshots found for session: ' . $session_id, 'debug' );
			return;
		}
		
		// Pick the snapshot that matches the timestamp we were scheduled with.
		// If none provided (backward compatibility), fall back to the latest snapshot.
		if ( $snapshot_time && isset( $snapshots[ $snapshot_time ] ) ) {
			$snapshot = $snapshots[ $snapshot_time ];
		} else {
			// Backward compatible path: use the most recent snapshot.
			$latest        = end( $snapshots );
			$snapshot      = $latest;
			$snapshot_time = isset( $latest['timestamp'] ) ? (int) $latest['timestamp'] : 0;
		}

		$cart_data = isset( $snapshot['cart_data'] ) ? $snapshot['cart_data'] : null;
		
		if ( empty( $cart_data ) ) {
			$this->log( 'Debounced cart sync skipped - empty cart data for session: ' . $session_id, 'debug' );
			return;
		}
		
		// Always require a valid email to sync cart - don't create placeholder contacts.
		if ( empty( $cart_data['customer_email'] ) || strpos( $cart_data['customer_email'], '@cart.placeholder' ) !== false ) {
			$this->log( 'Debounced cart sync skipped - valid customer email is required', 'debug' );
			return;
		}
		
		$this->log( 'Sending debounced cart sync for session: ' . $session_id, 'info' );
		
		$result = $this->sync_cart( $cart_data );
		
		if ( is_array( $result ) && ! empty( $result['success'] ) ) {
			$this->update_last_cart_sync_time( $session_id );
			$this->log( 'Debounced cart sync successful for session: ' . $session_id, 'info' );
			
			// Clear this specific snapshot after successful sync.
			if ( isset( $snapshots[ $snapshot_time ] ) ) {
				unset( $snapshots[ $snapshot_time ] );
			}

			if ( $user_id_for_snapshot > 0 ) {
				if ( empty( $snapshots ) ) {
					delete_user_meta( $user_id_for_snapshot, '_activetrail_cart_snapshot_' . $session_id );
				} else {
					update_user_meta( $user_id_for_snapshot, '_activetrail_cart_snapshot_' . $session_id, $snapshots );
				}
			} else {
				if ( WC()->session ) {
					if ( empty( $snapshots ) ) {
						WC()->session->set( 'activetrail_cart_snapshots', null );
					} else {
						WC()->session->set( 'activetrail_cart_snapshots', $snapshots );
					}
				}
			}
		} else {
			$this->log( 'Debounced cart sync failed for session: ' . $session_id . ' with result: ' . wp_json_encode( $result ), 'error' );
		}
	}

	/**
	 * Check if cart update should be throttled.
	 * Must not be used for debounced cart sync (timer-only): when the debounce timer fires we always send.
	 * Throttling is session-based: only blocks duplicate snapshots within the same browser session.
	 * If browser is closed and reopened, it's considered a new session.
	 *
	 * @param string $session_id Session ID.
	 * @param array  $cart_data  Cart snapshot data.
	 * @return bool
	 */
	private function should_throttle_cart_update( $session_id, $cart_data ) {
		if ( empty( $session_id ) ) {
			return false;
		}

		// Use WooCommerce session data instead of transients - this ensures data is cleared when session ends.
		if ( ! WC()->session ) {
			return false;
		}

		// Calculate current cart hash.
		$items_signature = array();

		if ( ! empty( $cart_data['items'] ) ) {
			foreach ( $cart_data['items'] as $item ) {
				$product_id   = isset( $item['product_id'] ) ? $item['product_id'] : 0;
				$variation_id = isset( $item['variation_id'] ) ? $item['variation_id'] : 0;
				$quantity     = isset( $item['quantity'] ) ? $item['quantity'] : 0;
				$line_total   = isset( $item['line_total'] ) ? $item['line_total'] : 0;

				$items_signature[] = $product_id . ':' . $variation_id . ':' . $quantity . ':' . $line_total;
			}

			sort( $items_signature );
		}

		$current_hash = md5(
			wp_json_encode(
				array(
					'items'    => $items_signature,
					'total'    => isset( $cart_data['cart_total'] ) ? $cart_data['cart_total'] : 0,
					'count'    => isset( $cart_data['cart_count'] ) ? $cart_data['cart_count'] : 0,
					'subtotal' => isset( $cart_data['cart_subtotal'] ) ? $cart_data['cart_subtotal'] : 0,
					'tax'      => isset( $cart_data['cart_tax'] ) ? $cart_data['cart_tax'] : 0,
					'email'    => isset( $cart_data['customer_email'] ) ? $cart_data['customer_email'] : '', // Include email in hash to allow updates when email changes.
				)
			)
		);

		// Get last hash from session (not transient - this ensures it's cleared when session ends).
		$last_hash = WC()->session->get( 'activetrail_cart_last_hash' );
		$last_sync = WC()->session->get( 'activetrail_cart_last_sync_time' );

		// If the cart snapshot changed (including email), allow the update and store the new hash.
		if ( $last_hash !== $current_hash ) {
			WC()->session->set( 'activetrail_cart_last_hash', $current_hash );
			return false;
		}

		// If snapshot is identical, only block if we synced very recently (within last 60 seconds).
		// This prevents immediate duplicate sends within the same session.
		if ( $last_sync && ( time() - $last_sync ) < 60 ) {
			$this->log( 'Cart update throttled - identical snapshot synced ' . ( time() - $last_sync ) . ' seconds ago for session: ' . $session_id, 'debug' );
			return true;
		}

		// Allow update if cart changed or enough time passed (even if hash is same, for periodic sync within session).
		return false;
	}

	/**
	 * Update last cart sync time
	 * Store in session (not transient) so it's cleared when session ends.
	 *
	 * @param string $session_id Session ID.
	 */
	private function update_last_cart_sync_time( $session_id ) {
		// Store in WooCommerce session instead of transient.
		// This ensures data is cleared when browser session ends.
		if ( WC()->session ) {
			WC()->session->set( 'activetrail_cart_last_sync_time', time() );
		}
	}

	/**
	 * Get session ID
	 *
	 * @return string
	 */
	private function get_session_id() {
		if ( ! WC()->session ) {
			return '';
		}

		$session_id = WC()->session->get( 'activetrail_session_id' );

		if ( empty( $session_id ) ) {
			$session_id = WC()->session->get_customer_id();
			WC()->session->set( 'activetrail_session_id', $session_id );
		}

		return $session_id;
	}

	/**
	 * Get customer email
	 *
	 * @return string
	 */
	private function get_customer_email() {
		$email = '';

		// Try to get from current user.
		if ( is_user_logged_in() ) {
			$user  = wp_get_current_user();
			$email = $user->user_email;
		}

		// Try to get from session/checkout.
		if ( empty( $email ) && WC()->customer ) {
			$email = WC()->customer->get_billing_email();
		}

		// Try to get from checkout session data.
		if ( empty( $email ) && WC()->session ) {
			$email = WC()->session->get( 'checkout_email' );
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $email ) && isset( $_POST['billing_email'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$email = sanitize_email( wp_unslash( $_POST['billing_email'] ) );
		}

		return $email;
	}

	/**
	 * Get customer first name and last name
	 *
	 * @param string $cart_email Optional cart email to verify against logged-in user email.
	 * @return array Array with 'first_name' and 'last_name' keys
	 */
	private function get_customer_name( $cart_email = '' ) {
		$first_name = '';
		$last_name  = '';

		// Try to get from current user - but only if email matches (to avoid mixing data from different users).
		if ( is_user_logged_in() ) {
			$user       = wp_get_current_user();
			$user_email = strtolower( trim( $user->user_email ) );
			
			// Only use logged-in user data if email matches cart email (or cart email not provided).
			if ( empty( $cart_email ) || $user_email === strtolower( trim( $cart_email ) ) ) {
				$first_name = $user->first_name;
				$last_name  = $user->last_name;
			} else {
				$this->log( 'Skipping logged-in user data - email mismatch: user=' . $user_email . ', cart=' . $cart_email, 'debug' );
			}
		}

		// Try to get from WooCommerce customer object (billing info).
		if ( ( empty( $first_name ) || empty( $last_name ) ) && WC()->customer ) {
			$billing_first_name = WC()->customer->get_billing_first_name();
			$billing_last_name  = WC()->customer->get_billing_last_name();

			if ( ! empty( $billing_first_name ) ) {
				$first_name = $billing_first_name;
			}
			if ( ! empty( $billing_last_name ) ) {
				$last_name = $billing_last_name;
			}
		}

		// Try to get from checkout session data.
		if ( ( empty( $first_name ) || empty( $last_name ) ) && WC()->session ) {
			$session_first_name = WC()->session->get( 'billing_first_name' );
			$session_last_name  = WC()->session->get( 'billing_last_name' );

			if ( ! empty( $session_first_name ) ) {
				$first_name = $session_first_name;
			}
			if ( ! empty( $session_last_name ) ) {
				$last_name = $session_last_name;
			}
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( ( empty( $first_name ) || empty( $last_name ) ) && isset( $_POST['billing_first_name'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$first_name = sanitize_text_field( wp_unslash( $_POST['billing_first_name'] ) );
		}
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $last_name ) && isset( $_POST['billing_last_name'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$last_name = sanitize_text_field( wp_unslash( $_POST['billing_last_name'] ) );
		}

		return array(
			'first_name' => $first_name,
			'last_name'  => $last_name,
		);
	}

	/**
	 * Get customer phone number
	 *
	 * @param string $cart_email Optional cart email to verify against logged-in user email.
	 * @return string
	 */
	private function get_customer_phone( $cart_email = '' ) {
		$phone = '';

		// Try to get from current user - but only if email matches (to avoid mixing data from different users).
		if ( is_user_logged_in() ) {
			$user = wp_get_current_user();
			$user_email = strtolower( trim( $user->user_email ) );
			
			// Only use logged-in user data if email matches cart email (or cart email not provided).
			if ( empty( $cart_email ) || $user_email === strtolower( trim( $cart_email ) ) ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for user data
				$phone = get_user_meta( $user->ID, 'billing_phone', true );
			}
		}

		// Try to get from WooCommerce customer object (billing info).
		if ( empty( $phone ) && WC()->customer ) {
			$phone = WC()->customer->get_billing_phone();
		}

		// Try to get from checkout session data.
		if ( empty( $phone ) && WC()->session ) {
			$phone = WC()->session->get( 'billing_phone' );
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $phone ) && isset( $_POST['billing_phone'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$phone = sanitize_text_field( wp_unslash( $_POST['billing_phone'] ) );
		}

		return $phone;
	}

	/**
	 * Get customer address
	 *
	 * @param string $cart_email Optional cart email to verify against logged-in user email.
	 * @return string
	 */
	private function get_customer_address( $cart_email = '' ) {
		$address_1 = '';
		$address_2 = '';

		// Try to get from current user - but only if email matches (to avoid mixing data from different users).
		if ( is_user_logged_in() ) {
			$user = wp_get_current_user();
			$user_email = strtolower( trim( $user->user_email ) );
			
			// Only use logged-in user data if email matches cart email (or cart email not provided).
			if ( empty( $cart_email ) || $user_email === strtolower( trim( $cart_email ) ) ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for user data
				$address_1 = get_user_meta( $user->ID, 'billing_address_1', true );
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for user data
				$address_2 = get_user_meta( $user->ID, 'billing_address_2', true );
			}
		}

		// Try to get from WooCommerce customer object (billing info).
		if ( ( empty( $address_1 ) || empty( $address_2 ) ) && WC()->customer ) {
			$billing_address_1 = WC()->customer->get_billing_address_1();
			$billing_address_2 = WC()->customer->get_billing_address_2();

			if ( ! empty( $billing_address_1 ) ) {
				$address_1 = $billing_address_1;
			}
			if ( ! empty( $billing_address_2 ) ) {
				$address_2 = $billing_address_2;
			}
		}

		// Try to get from checkout session data.
		if ( ( empty( $address_1 ) || empty( $address_2 ) ) && WC()->session ) {
			$session_address_1 = WC()->session->get( 'billing_address_1' );
			$session_address_2 = WC()->session->get( 'billing_address_2' );

			if ( ! empty( $session_address_1 ) ) {
				$address_1 = $session_address_1;
			}
			if ( ! empty( $session_address_2 ) ) {
				$address_2 = $session_address_2;
			}
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $address_1 ) && isset( $_POST['billing_address_1'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$address_1 = sanitize_text_field( wp_unslash( $_POST['billing_address_1'] ) );
		}
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $address_2 ) && isset( $_POST['billing_address_2'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$address_2 = sanitize_text_field( wp_unslash( $_POST['billing_address_2'] ) );
		}

		return trim( $address_1 . ' ' . $address_2 );
	}

	/**
	 * Get customer city
	 *
	 * @param string $cart_email Optional cart email to verify against logged-in user email.
	 * @return string
	 */
	private function get_customer_city( $cart_email = '' ) {
		$city = '';

		// Try to get from current user - but only if email matches (to avoid mixing data from different users).
		if ( is_user_logged_in() ) {
			$user = wp_get_current_user();
			$user_email = strtolower( trim( $user->user_email ) );
			
			// Only use logged-in user data if email matches cart email (or cart email not provided).
			if ( empty( $cart_email ) || $user_email === strtolower( trim( $cart_email ) ) ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for user data
				$city = get_user_meta( $user->ID, 'billing_city', true );
			}
		}

		// Try to get from WooCommerce customer object (billing info).
		if ( empty( $city ) && WC()->customer ) {
			$city = WC()->customer->get_billing_city();
		}

		// Try to get from checkout session data.
		if ( empty( $city ) && WC()->session ) {
			$city = WC()->session->get( 'billing_city' );
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $city ) && isset( $_POST['billing_city'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$city = sanitize_text_field( wp_unslash( $_POST['billing_city'] ) );
		}

		return $city;
	}

	/**
	 * Get customer zip code / postcode
	 *
	 * @param string $cart_email Optional cart email to verify against logged-in user email.
	 * @return string
	 */
	private function get_customer_zip_code( $cart_email = '' ) {
		$zip_code = '';

		// Try to get from current user - but only if email matches (to avoid mixing data from different users).
		if ( is_user_logged_in() ) {
			$user = wp_get_current_user();
			$user_email = strtolower( trim( $user->user_email ) );
			
			// Only use logged-in user data if email matches cart email (or cart email not provided).
			if ( empty( $cart_email ) || $user_email === strtolower( trim( $cart_email ) ) ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for user data
				$zip_code = get_user_meta( $user->ID, 'billing_postcode', true );
			}
		}

		// Try to get from WooCommerce customer object (billing info).
		if ( empty( $zip_code ) && WC()->customer ) {
			$zip_code = WC()->customer->get_billing_postcode();
		}

		// Try to get from checkout session data.
		if ( empty( $zip_code ) && WC()->session ) {
			$zip_code = WC()->session->get( 'billing_postcode' );
		}

		// Try to get from POST data (if form was submitted).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		if ( empty( $zip_code ) && isset( $_POST['billing_postcode'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
			$zip_code = sanitize_text_field( wp_unslash( $_POST['billing_postcode'] ) );
		}

		return $zip_code;
	}

	/**
	 * Prepare cart data for API
	 *
	 * @return array
	 */
	private function prepare_cart_data() {
		$cart = WC()->cart;

		if ( $cart && method_exists( $cart, 'calculate_totals' ) ) {
			$cart->calculate_totals();
		}

		$items = array();

		foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
			$product = $cart_item['data'];

			$items[] = array(
				'product_id'   => $cart_item['product_id'],
				'variation_id' => $cart_item['variation_id'],
				'sku'          => $product->get_sku(),
				'name'         => $product->get_name(),
				'quantity'     => $cart_item['quantity'],
				'price'        => $product->get_price(),
				'line_total'   => $cart_item['line_total'],
				'image_url'    => wp_get_attachment_image_url( $product->get_image_id(), 'medium' ),
				'product_url'  => get_permalink( $cart_item['product_id'] ),
				'categories'   => $this->get_product_categories( $product ),
			);
		}

		$data = array(
			'session_id'     => $this->get_session_id(),
			'customer_email' => $this->get_customer_email(),
			'cart_total'     => $cart->get_cart_contents_total(),
			'cart_tax'       => $cart->get_cart_contents_tax(),
			'cart_subtotal'  => $cart->get_subtotal(),
			'cart_count'     => $cart->get_cart_contents_count(),
			'currency'       => get_woocommerce_currency(),
			'items'          => $items,
			'group_id'       => ! empty( $this->settings['group_id'] ) ? intval( $this->settings['group_id'] ) : null,
			'created_at'     => current_time( 'mysql' ),
			'updated_at'     => current_time( 'mysql' ),
		);

		return apply_filters( 'activetrail_cart_data', $data );
	}

	/**
	 * Store cart data in database
	 *
	 * @param string $session_id Session ID.
	 * @param array  $cart_data Cart data.
	 */
	private function store_cart_data( $session_id, $cart_data ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
		$table      = esc_sql( $table_name );

		// Check if record exists.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Direct query needed for cart tracking, table name is escaped
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
		$existing = $wpdb->get_row(
			$wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql()
				"SELECT id FROM {$table} WHERE session_id = %s",
				$session_id
			)
		);

		$data = array(
			'session_id' => $session_id,
			'user_id'    => get_current_user_id() ?: null,
			'cart_data'  => wp_json_encode( $cart_data ),
			'email'      => $cart_data['customer_email'],
			'updated_at' => current_time( 'mysql' ),
		);

		if ( $existing ) {
			// Update existing record.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
			$result = $wpdb->update(
				$table_name,
				$data,
				array( 'session_id' => $session_id ),
				array( '%s', '%d', '%s', '%s', '%s' ),
				array( '%s' )
			);
		} else {
			// Insert new record.
			$data['created_at'] = current_time( 'mysql' );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Direct query needed for cart tracking
			$result = $wpdb->insert(
				$table_name,
				$data,
				array( '%s', '%d', '%s', '%s', '%s', '%s' )
			);
		}

		if ( $result === false ) {
			$this->log( 'Database error storing cart data: ' . $wpdb->last_error, 'error' );
		} else {
			$this->log( 'Cart data stored successfully: ' . $session_id, 'info' );
		}
	}

	/**
	 * Sync cart to ActiveTrail using the proper Cart API endpoint
	 *
	 * @param array $cart_data Cart data.
	 * @return array
	 */
	public function sync_cart( $cart_data ) {
		// CRITICAL: Check if cart tracking is enabled (defense in depth).
		if ( ! $this->is_cart_tracking_enabled() ) {
			$this->log( 'sync_cart called but cart tracking is disabled - skipping', 'debug' );
			return array(
				'success' => false,
				'message' => 'Cart tracking is disabled',
			);
		}

		if ( empty( $this->settings['api_key'] ) ) {
			$this->log( 'API key not configured for cart sync', 'error' );
			return array(
				'success' => false,
				'message' => 'API key not configured',
			);
		}

		// Inject group_id into cart_data before transforming to ActiveTrail format.
		if ( ! empty( $this->settings['group_id'] ) && ( empty( $cart_data['group_id'] ) || $cart_data['group_id'] === null ) ) {
			$cart_data['group_id'] = intval( $this->settings['group_id'] );
			$this->log( 'Injected group_id into cart_data for sync_cart: ' . $cart_data['group_id'], 'debug' );
		}

		// Transform cart data to ActiveTrail format.
		$activetrail_cart_data = $this->transform_cart_to_activetrail_format( $cart_data );

		// Validate email presence after transformation (in case overridden).
		if ( empty( $activetrail_cart_data['email'] ) ) {
			$this->log( 'Failed to sync cart: Customer email is required after transformation', 'error' );
			return array(
				'success' => false,
				'message' => 'Customer email is required',
			);
		}

		// Do not send Cart when no valid externalId (e.g. all contacts Blocked or debounce without session-stored id).
		if ( empty( $activetrail_cart_data['externalId'] ) ) {
			$this->log( 'Skip cart sync - no valid externalId (contact may be Blocked or no existing contact for email)', 'debug' );
			return array(
				'success' => false,
				'message' => 'No valid externalId for cart sync',
			);
		}

		$result = $this->api_client->submit_cart( $activetrail_cart_data );

		if ( is_array( $result ) && isset( $result['success'] ) && $result['success'] ) {
			// Mark cart as synced.
			$this->mark_cart_synced( $cart_data['session_id'] );
			$this->log( 'Cart synced successfully: ' . $cart_data['session_id'], 'info' );
			return array(
				'success' => true,
				'message' => 'Cart synced successfully',
			);
		} else {
			$error_message = is_array( $result ) && isset( $result['message'] ) ? $result['message'] : 'Unknown error';
			$this->log( 'Failed to sync cart ' . $cart_data['session_id'] . ': ' . $error_message, 'error' );
			return array(
				'success' => false,
				'message' => $error_message,
			);
		}
	}

	/**
	 * Transform cart data to ActiveTrail API format
	 *
	 * @param array $cart_data Internal cart data.
	 * @return array ActiveTrail formatted cart data
	 */
	private function transform_cart_to_activetrail_format( $cart_data ) {
		$settings = $this->settings;

		// Get applied coupons from the cart, check if cart exists to avoid null error.
		$applied_coupons = array();
		if ( WC()->cart ) {
			$applied_coupons = WC()->cart->get_applied_coupons();
		} else {
			$this->log( 'Warning: WC()->cart is null in transform_cart_to_activetrail_format', 'warning' );
		}
		$coupon_details = array();

		// Get details of each applied coupon.
		foreach ( $applied_coupons as $coupon_code ) {
			$coupon                         = new WC_Coupon( $coupon_code );
			$coupon_details[ $coupon_code ] = array(
				'code'                        => $coupon_code,
				'type'                        => $coupon->get_discount_type(),
				'product_ids'                 => $coupon->get_product_ids(),
				'excluded_product_ids'        => $coupon->get_excluded_product_ids(),
				'product_categories'          => $coupon->get_product_categories(),
				'excluded_product_categories' => $coupon->get_excluded_product_categories(),
				'amount'                      => $coupon->get_amount(),
			);
		}

		// Determine if there's an order-level coupon.
		$has_order_level_coupon = false;
		foreach ( $coupon_details as $coupon_data ) {
			if ( 'fixed_cart' === $coupon_data['type'] ) {
				$has_order_level_coupon = true;
				break;
			}
		}

		// Prepare products array according to ActiveTrail Cart API format.
		$products = array();
		if ( ! empty( $cart_data['items'] ) ) {
			foreach ( $cart_data['items'] as $item ) {
				// Determine which product-specific coupons apply to this product.
				$product_coupon_codes = array();

				if ( ! $has_order_level_coupon ) {  // Only check for product coupons if there's no order-level coupon
					foreach ( $coupon_details as $coupon_code => $coupon_data ) {
						// Skip order-level coupons.
						if ( 'fixed_cart' === $coupon_data['type'] ) {
							continue;
						}

						$product_id   = $item['product_id'];
						$product_cats = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) );

						// Check if coupon applies to this product.
						$applies_to_product = false;

						// Check product IDs.
						if ( ! empty( $coupon_data['product_ids'] ) ) {
							if ( in_array( $product_id, $coupon_data['product_ids'], true ) ) {
								$applies_to_product = true;
							}
						}
						// Check product categories.
						elseif ( ! empty( $coupon_data['product_categories'] ) && ! empty( $product_cats ) ) {
							foreach ( $product_cats as $cat_id ) {
								if ( in_array( $cat_id, $coupon_data['product_categories'], true ) ) {
									$applies_to_product = true;
									break;
								}
							}
						}
						// If no specific products or categories are set, coupon applies to all products.
						elseif ( empty( $coupon_data['product_ids'] ) && empty( $coupon_data['product_categories'] ) ) {
							$applies_to_product = true;
						}

						// Check excluded products.
						if ( $applies_to_product && ! empty( $coupon_data['excluded_product_ids'] ) ) {
							if ( in_array( $product_id, $coupon_data['excluded_product_ids'], true ) ) {
								$applies_to_product = false;
							}
						}

						// Check excluded categories.
						if ( $applies_to_product && ! empty( $coupon_data['excluded_product_categories'] ) && ! empty( $product_cats ) ) {
							foreach ( $product_cats as $cat_id ) {
								if ( in_array( $cat_id, $coupon_data['excluded_product_categories'], true ) ) {
									$applies_to_product = false;
									break;
								}
							}
						}

						// If coupon applies to this product, add it to the list.
						if ( $applies_to_product ) {
							$product_coupon_codes[] = $coupon_code;
						}
					}
				}

				// Create product data array.
				$product_data = array(
					'id'          => strval( $item['product_id'] ),
					'name'        => $item['name'],
					'quantity'    => floatval( $item['quantity'] ),
					'totalamount' => floatval( $item['line_total'] ),
					'description' => $item['name'], // Use name as description if no description available
					'sku'         => $item['sku'] ? $item['sku'] : strval( $item['product_id'] ),
					'type'        => 'product',
					'category'    => is_array( $item['categories'] ) ? implode( ', ', $item['categories'] ) : $item['categories'],
					'unitprice'   => floatval( $item['price'] ),
					'totalprice'  => floatval( $item['line_total'] ),
				);
				
				// Only add image if it exists - ActiveTrail may reject carts with empty image strings
				if ( ! empty( $item['image_url'] ) && $item['image_url'] !== false ) {
					$product_data['image'] = $item['image_url'];
				}

				// Add product-specific coupon codes if any.
				if ( ! empty( $product_coupon_codes ) ) {
					$product_data['coupon'] = implode( ', ', $product_coupon_codes );
				}

				$products[] = $product_data;
			}
		}

		// Get user ID if available (0 in cron/debounced context).
		$user_id = get_current_user_id();
		$session_id = isset( $cart_data['session_id'] ) ? $cart_data['session_id'] : '';
		$customer_email = $cart_data['customer_email'];
		$external_id    = null;
		$existing_contact_data = null;
		$had_contacts_from_api = false; // True when API returned contacts (even if all Blocked).

		// One externalId per session: try stored transient first (debounced sync reuses same externalId).
		if ( ! empty( $session_id ) ) {
			$stored_external = get_transient( 'activetrail_session_external_' . $session_id );
			if ( ! empty( $stored_external ) && is_string( $stored_external ) ) {
				$external_id = $stored_external;
				$this->log( 'Using session-stored external_id for cart: ' . $external_id, 'debug' );
			}
		}

		if ( empty( $external_id ) && ! empty( $customer_email ) && ! empty( $this->api_client ) ) {
			try {
				$existing_check = $this->api_client->get_customer_by_email( $customer_email );
				if ( ! is_wp_error( $existing_check ) && ! empty( $existing_check['exists'] ) ) {
					$existing_data     = $existing_check['data'];
					$contacts_to_check = is_array( $existing_data ) && isset( $existing_data[0] )
						? $existing_data
						: array( $existing_data );

					// Exclude Blocked contacts; ActiveTrail rejects Cart for Blocked.
					$allowed = array();
					foreach ( $contacts_to_check as $c ) {
						$state = isset( $c['state'] ) ? $c['state'] : '';
						if ( $state === 'Blocked' ) {
							$this->log( 'Skipping Blocked contact for cart: ' . ( isset( $c['externalId'] ) ? $c['externalId'] : 'unknown' ), 'debug' );
							continue;
						}
						$allowed[] = $c;
					}
					$had_contacts_from_api = ! empty( $contacts_to_check );
					$contacts_to_check = $allowed;

					$logged_in_user  = $user_id ? wp_get_current_user() : null;
					$logged_in_email = $logged_in_user ? strtolower( trim( $logged_in_user->user_email ) ) : '';
					$cart_email_lower = strtolower( trim( $customer_email ) );
					$email_matches   = ( $logged_in_email === $cart_email_lower );
					// Debounce context: session_id is often the user ID for logged-in users.
					$session_id_numeric = is_numeric( $session_id ) ? (int) $session_id : 0;

					// Priority: (1) Registered WC-CONTACT-{user_id} or WC-CONTACT-{session_id}, (2) Subscribed, (3) Unsubscribed.
					$chosen = null;
					$by_priority = array( 'registered' => null, 'subscribed' => null, 'unsubscribed' => null );
					foreach ( $contacts_to_check as $contact_data ) {
						$existing_external_id = isset( $contact_data['externalId'] ) ? $contact_data['externalId'] : null;
						if ( empty( $existing_external_id ) ) {
							continue;
						}
						$state = isset( $contact_data['state'] ) ? $contact_data['state'] : '';
						// Registered: WC-CONTACT-{user_id} or WC-CONTACT-{session_id} (debounce).
						if ( ( $user_id && strpos( $existing_external_id, 'WC-CONTACT-' . $user_id ) === 0 ) ||
							 ( $session_id_numeric && strpos( $existing_external_id, 'WC-CONTACT-' . $session_id_numeric ) === 0 ) ) {
							$by_priority['registered'] = $contact_data;
							break;
						}
						if ( $state === 'Subscribed' && empty( $by_priority['subscribed'] ) ) {
							$by_priority['subscribed'] = $contact_data;
						}
						if ( ( $state === 'Unsubscribed' || $state === '' ) && empty( $by_priority['unsubscribed'] ) ) {
							$by_priority['unsubscribed'] = $contact_data;
						}
					}
					$chosen = $by_priority['registered'] ?: ( $by_priority['subscribed'] ?: $by_priority['unsubscribed'] );
					if ( $chosen ) {
						$external_id = $chosen['externalId'];
						$existing_contact_data = $chosen;
						$this->log( 'Chosen contact for cart: ' . $external_id . ' (state: ' . ( isset( $chosen['state'] ) ? $chosen['state'] : 'n/a' ) . ')', 'debug' );
						if ( ! empty( $session_id ) ) {
							set_transient( 'activetrail_session_external_' . $session_id, $external_id, HOUR_IN_SECONDS );
						}
					}

					if ( $user_id && ! $email_matches && ! empty( $existing_contact_data ) ) {
						$this->log( 'Email mismatch: logged-in user email (' . $logged_in_email . ') differs from cart email (' . $cart_email_lower . ') - will use existing contact data', 'info' );
					} elseif ( $user_id && ! $email_matches && ! empty( $contacts_to_check[0] ) ) {
						$existing_contact_data = $contacts_to_check[0];
						$this->log( 'Email mismatch: using first available contact data', 'info' );
					}
				}
			} catch ( Exception $check_e ) {
				$this->log( 'Could not check for existing contact in cart: ' . $check_e->getMessage(), 'debug' );
			}
		}

		// Use standard GUEST only when no existing contact was found for this email. Do not create GUEST when we had contacts but all were Blocked.
		if ( empty( $external_id ) && ! $had_contacts_from_api ) {
			$external_id = $user_id ? activetrail_external_id_for_contact( $user_id ) : activetrail_external_id_for_contact( 0, $customer_email );
			$this->log( 'Using standard external_id for cart: ' . $external_id, 'debug' );
			if ( ! empty( $session_id ) ) {
				set_transient( 'activetrail_session_external_' . $session_id, $external_id, HOUR_IN_SECONDS );
			}
		}

		// Check if user has given consent for marketing - DEFAULT TO FALSE.
		$consent = false;

		// If we are currently in a checkout submit, also look at the checkbox value from the form.
		// This is critical for guest checkouts (user_id = 0), where there is no user meta yet.
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification
		$checkout_post_consent = ActiveTrail_Consent_Helper::has_consent_from_data( $_POST );

		if ( $user_id ) {
			// Logged-in user: start from stored meta consent.
			// Check user meta for consent from registration.
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for consent detection
			$user_consent = get_user_meta( $user_id, '_activetrail_registration_consent', true );
			$consent      = ( 'yes' === $user_consent );

			// Also check for checkout consent saved on the user.
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for consent detection
			$checkout_consent_meta = get_user_meta( $user_id, '_activetrail_consent', true );
			if ( 'yes' === $checkout_consent_meta ) {
				$consent = true;
			}

			// Finally, if in the current checkout request הלקוח סימן שוב צ'קבוקס – זה גובר ועדכן אותו כמאשר.
			if ( $checkout_post_consent ) {
				$consent = true;
			}
		} else {
			// Guest checkout (user_id = 0): הסתמך רק על מה שסומן עכשיו בטופס הצ'קאאוט.
			if ( $checkout_post_consent ) {
				$consent = true;
			}
		}

		// Log consent status for debugging.
		$this->log( 'Cart for user ' . $user_id . ' with email ' . $cart_data['customer_email'] . ' consent status: ' . ( $consent ? 'true' : 'false' ), 'debug' );

		// Get order-level coupons for the cart-level coupon field.
		$order_level_coupon_codes = array();
		foreach ( $coupon_details as $coupon_code => $coupon_data ) {
			if ( 'fixed_cart' === $coupon_data['type'] ) {
				$order_level_coupon_codes[] = $coupon_code;
			}
		}

		// Get customer name - prioritize existing contact data if available (to avoid mixing data from logged-in user)
		$first_name = '';
		$last_name  = '';
		
		// If we have existing contact data (guest contact or email mismatch), use its data first
		if ( ! empty( $existing_contact_data ) ) {
			if ( ! empty( $existing_contact_data['first_name'] ) ) {
				$first_name = $existing_contact_data['first_name'];
			}
			if ( ! empty( $existing_contact_data['last_name'] ) ) {
				$last_name = $existing_contact_data['last_name'];
			}
			$this->log( 'Using name from existing contact data: ' . $first_name . ' ' . $last_name, 'debug' );
		}
		
		// If no name from existing contact, get from WooCommerce Customer or fallback to email extraction.
		if ( empty( $first_name ) && empty( $last_name ) ) {
			$customer_name = $this->get_customer_name( $customer_email );
			$first_name    = $customer_name['first_name'];
			$last_name     = $customer_name['last_name'];
		}

		// If still no name found, try to extract from email as fallback.
		if ( empty( $first_name ) && ! empty( $cart_data['customer_email'] ) ) {
			$extracted_name = $this->extract_name_from_email( $cart_data['customer_email'] );
			$first_name     = $extracted_name['first_name'];
			$last_name      = $extracted_name['last_name'];
		}

		// Get customer phone, address, city, and zip code - prioritize existing contact data if available
		$customer_phone    = '';
		$customer_address  = '';
		$customer_city     = '';
		$customer_zip_code = '';
		
		// If we have existing contact data, use its data first
		if ( ! empty( $existing_contact_data ) ) {
			if ( ! empty( $existing_contact_data['sms'] ) ) {
				$customer_phone = $existing_contact_data['sms'];
			}
			if ( ! empty( $existing_contact_data['street'] ) ) {
				$customer_address = $existing_contact_data['street'];
			}
			if ( ! empty( $existing_contact_data['city'] ) ) {
				$customer_city = $existing_contact_data['city'];
			}
			if ( ! empty( $existing_contact_data['zip_code'] ) ) {
				$customer_zip_code = $existing_contact_data['zip_code'];
			}
			$this->log( 'Using address data from existing contact', 'debug' );
		}
		
		// If no data from existing contact, get from WooCommerce Customer
		if ( empty( $customer_phone ) ) {
			$customer_phone = $this->get_customer_phone( $customer_email );
		}
		if ( empty( $customer_address ) ) {
			$customer_address = $this->get_customer_address( $customer_email );
		}
		if ( empty( $customer_city ) ) {
			$customer_city = $this->get_customer_city( $customer_email );
		}
		if ( empty( $customer_zip_code ) ) {
			$customer_zip_code = $this->get_customer_zip_code( $customer_email );
		}

		// Format according to ActiveTrail Cart API structure.
		$activetrail_data = array(
			'externalId'         => $external_id,
			'externalName'       => ACTIVETRAIL_EXTERNAL_NAME,
			'name'               => 'Cart #' . substr( $cart_data['session_id'], -8 ),
			'firstName'          => $first_name,
			'lastName'           => $last_name,
			'email'              => $cart_data['customer_email'],
			'mobile'             => $customer_phone,
			'netAmount'          => floatval( $cart_data['cart_subtotal'] ),
			'tax'                => floatval( $cart_data['cart_tax'] ),
			'coupon'             => ! empty( $order_level_coupon_codes ) ? implode( ', ', $order_level_coupon_codes ) : '', // Set cart-level coupons
			'quantity'           => floatval( $cart_data['cart_count'] ),
			'totalAmount'        => floatval( $cart_data['cart_total'] ),
			'address'            => $customer_address,
			'city'               => $customer_city,
			'zip_code'           => $customer_zip_code,
			'products'           => $products,
			'url'                => wc_get_cart_url(),
			'lastUpdate'         => gmdate( 'c' ), // ISO 8601 format
			'purchaseType'       => 'online',
			'group'              => ! empty( $settings['group_id'] ) ? intval( $settings['group_id'] ) : null, // Added group from settings key 'group_id'
		);

		// Log presence or absence of group.
		if ( ! empty( $settings['group_id'] ) ) {
			$this->log( 'Added group field to cart data: ' . intval( $settings['group_id'] ), 'debug' );
		} else {
			$this->log( 'No group configured in settings; group field missing from cart data', 'warning' );
		}

		// Add store ID if configured, otherwise fallback to 1.
		if ( ! empty( $settings['store_id'] ) ) {
			$activetrail_data['storeId'] = intval( $settings['store_id'] );
		} else {
			$activetrail_data['storeId'] = 1;
		}

		return apply_filters( 'activetrail_cart_data_for_api', $activetrail_data, $cart_data );
	}

	/**
	 * Extract first name and last name from email (fallback when no name is available)
	 * Tries to split the username by common separators (., _, -) or space
	 *
	 * @param string $email Email address.
	 * @return array Array with 'first_name' and 'last_name' keys
	 */
	private function extract_name_from_email( $email ) {
		if ( empty( $email ) ) {
			return array(
				'first_name' => '',
				'last_name'  => '',
			);
		}

		$parts    = explode( '@', $email );
		$username = $parts[0];

		// Try to split by common separators: dot, underscore, dash, or space.
		$name_parts = preg_split( '/[._\-\s]+/', $username );

		// Remove numbers and special characters from each part.
		$cleaned_parts = array();
		foreach ( $name_parts as $part ) {
			$cleaned = preg_replace( '/[^a-zA-Z]/', '', $part );
			if ( ! empty( $cleaned ) ) {
				$cleaned_parts[] = ucfirst( strtolower( $cleaned ) );
			}
		}

		// If we have multiple parts, use first as first_name and rest as last_name.
		if ( count( $cleaned_parts ) > 1 ) {
			$first_name = array_shift( $cleaned_parts );
			$last_name  = implode( ' ', $cleaned_parts );
		} elseif ( count( $cleaned_parts ) === 1 ) {
			// Only one part - use as first name.
			$first_name = $cleaned_parts[0];
			$last_name  = '';
		} else {
			// No valid name found.
			$first_name = '';
			$last_name  = '';
		}

		return array(
			'first_name' => $first_name,
			'last_name'  => $last_name,
		);
	}

	/**
	 * Mark cart as synced
	 *
	 * @param string $session_id Session ID.
	 */
	private function mark_cart_synced( $session_id ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
		$result = $wpdb->update(
			$table_name,
			array( 
				'synced' => 1,
				'sync_attempts' => 0, // Reset attempts on successful sync
			),
			array( 'session_id' => $session_id ),
			array( '%d', '%d' ),
			array( '%s' )
		);

		if ( $result === false ) {
			$this->log( 'Failed to mark cart as synced: ' . $session_id . ' - ' . $wpdb->last_error, 'error' );
		} else {
			$this->log( 'Cart marked as synced: ' . $session_id, 'info' );
		}
	}

	/**
	 * Increment sync attempt counter for a cart
	 *
	 * @param string $session_id Session ID.
	 */
	private function increment_sync_attempt( $session_id ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
		$table      = esc_sql( $table_name );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
		$result = $wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql()
				"UPDATE {$table} SET sync_attempts = sync_attempts + 1, last_sync_attempt = %s WHERE session_id = %s",
				current_time( 'mysql' ),
				$session_id
			)
		);

		if ( $result === false ) {
			$this->log( 'Failed to increment sync attempt for cart: ' . $session_id . ' - ' . $wpdb->last_error, 'error' );
		}
	}

	/**
	 * Sync abandoned carts
	 */
	public function sync_abandoned_carts() {
		// Check if cart tracking is enabled.
		if ( ! $this->is_cart_tracking_enabled() ) {
			$this->log( 'Abandoned cart sync skipped - cart tracking disabled', 'debug' );
			return;
		}
		global $wpdb;
		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
		$table      = esc_sql( $table_name );

		// Get abandoned carts (not synced, older than 60 minutes, with email).
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Direct query needed for cart tracking, table name is escaped
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
		$abandoned_carts = $wpdb->get_results(
			$wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql()
				"SELECT * FROM {$table} 
             WHERE synced = 0 
             AND email IS NOT NULL 
             AND email != '' 
             AND updated_at < %s 
             LIMIT 50",
				gmdate( 'Y-m-d H:i:s', strtotime( '-60 minutes' ) )
			)
		);

		$synced_count = 0;
		$error_count  = 0;

		foreach ( $abandoned_carts as $cart_record ) {
			$cart_data = json_decode( $cart_record->cart_data, true );

			// Sanitize decoded data.
			if ( is_array( $cart_data ) ) {
				$cart_data = activetrail_wc_sanitize_array( $cart_data );
			}

			// Ensure session_id is set from database record (needed for mark_cart_synced).
			if ( ! isset( $cart_data['session_id'] ) || empty( $cart_data['session_id'] ) ) {
				$cart_data['session_id'] = $cart_record->session_id;
			}

			// Ensure email is set from database record.
			if ( ! isset( $cart_data['customer_email'] ) || empty( $cart_data['customer_email'] ) ) {
				$cart_data['customer_email'] = $cart_record->email;
			}

			// Check if cart has exceeded max retry attempts (default: 3).
			$max_retry_attempts = apply_filters( 'activetrail_max_cart_sync_attempts', 3 );
			$current_attempts = isset( $cart_record->sync_attempts ) ? (int) $cart_record->sync_attempts : 0;

			if ( $current_attempts >= $max_retry_attempts ) {
				$this->log( 'Abandoned cart sync skipped: max retry attempts (' . $max_retry_attempts . ') reached for session ' . $cart_record->session_id, 'warning' );
				// Mark as synced to prevent further attempts (even though it failed).
				$this->mark_cart_synced( $cart_record->session_id );
				++$error_count;
				continue;
			}

			if ( $cart_data && ! empty( $cart_data['items'] ) && ! empty( $cart_data['session_id'] ) ) {
				// Increment sync attempt counter before sync.
				$this->increment_sync_attempt( $cart_record->session_id );

				$result = $this->sync_cart( $cart_data );
				if ( is_array( $result ) ? ! empty( $result['success'] ) : (bool) $result ) {
					++$synced_count;
				} else {
					++$error_count;
				}
			} else {
				// Log if cart data is invalid.
				if ( empty( $cart_data['items'] ) ) {
					$this->log( 'Abandoned cart sync skipped: cart has no items for session ' . ( isset( $cart_record->session_id ) ? $cart_record->session_id : 'unknown' ), 'warning' );
				} elseif ( empty( $cart_data['session_id'] ) ) {
					$this->log( 'Abandoned cart sync skipped: missing session_id for cart record ID ' . ( isset( $cart_record->id ) ? $cart_record->id : 'unknown' ), 'warning' );
				}
			}
		}

		// Log results only if there were carts to process or errors occurred.
		if ( ! empty( $abandoned_carts ) && ( $synced_count > 0 || $error_count > 0 ) ) {
			$this->log( "Abandoned cart sync completed: {$synced_count} synced, {$error_count} errors", 'info' );
		}
	}

	/**
	 * Get product categories
	 *
	 * @param WC_Product $product Product object.
	 * @return array
	 */
	private function get_product_categories( $product ) {
		$categories = array();
		$terms      = get_the_terms( $product->get_id(), 'product_cat' );

		if ( $terms && ! is_wp_error( $terms ) ) {
			foreach ( $terms as $term ) {
				$categories[] = $term->name;
			}
		}

		return $categories;
	}

	/**
	 * Get abandoned carts for admin display
	 *
	 * @param int $limit Limit.
	 * @return array
	 */
	public function get_abandoned_carts( $limit = 20 ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
		$table      = esc_sql( $table_name );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking
		return $wpdb->get_results(
			$wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql()
				"SELECT * FROM {$table} 
             WHERE email IS NOT NULL 
             AND email != '' 
             ORDER BY updated_at DESC 
             LIMIT %d",
				$limit
			)
		);
	}

	/**
	 * Clean old cart data
	 *
	 * @param int $days Days to keep.
	 */
	public function clean_old_cart_data( $days = 30 ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'activetrail_cart_tracking';
		$table      = esc_sql( $table_name );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for cart tracking cleanup
		$deleted = $wpdb->query(
			$wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql()
				"DELETE FROM {$table} WHERE created_at < %s",
				gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) )
			)
		);

		return $deleted;
	}

	/**
	 * Log message
	 *
	 * @param string $message Log message.
	 * @param string $level Log level.
	 */
	private function log( $message, $level = 'info' ) {
		if ( function_exists( 'activetrail_logger' ) ) {
			$level_map    = array(
				'info'    => 'INFO',
				'debug'   => 'DEBUG',
				'warning' => 'WARNING',
				'error'   => 'ERROR',
			);
			$mapped_level = isset( $level_map[ $level ] ) ? $level_map[ $level ] : 'INFO';
			activetrail_logger()->log( $message, $mapped_level, 'CART' );
		}
	}
}
