<?php
// phpcs:disable WordPress.WP.I18n.TextDomainMismatch
// Text domain 'activetrail-for-woocommerce' is correct per WordPress review requirements.
/**
 * ActiveTrail Historical Sync Handler
 *
 * @package ActiveTrail_WooCommerce
 * @since 1.0.0
 */

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

/**
 * ActiveTrail Historical Sync Handler Class
 */
class ActiveTrail_Historical_Sync_Handler
{

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

    /**
     * Settings instance
     *
     * @var ActiveTrail_Settings
     */
    private $settings;

    /**
     * Order handler instance
     *
     * @var ActiveTrail_Order_Handler
     */
    private $order_handler;

    /**
     * Batch size for processing
     *
     * @var int
     */
    private $batch_size = 50;

    /**
     * Max contacts per API chunk when importing contacts
     * Smaller than API hard limit (1000) to reduce timeouts
     *
     * @var int
     */
    private $contact_chunk_size = 250;

    /**
     * Master list of marketing meta keys from various email marketing plugins
     * Used to detect historical opt-in consent from other plugins
     *
     * @var array
     */
    private $marketing_meta_keys = array(
        // ActiveTrail (our own standard keys).
        'activetrail_optin',
        'activetrail_subscribe',
        // Mailchimp.
        'mailchimp_woocommerce_is_subscribed',
        'mailchimp_woocommerce_newsletter',
        'mailchimp_woocommerce_marketing_status_updated_at',
        // MailPoet.
        'mailpoet_subscribed',
        'mailpoet_woocommerce_subscribe_on_checkout',
        // AutomateWoo.
        'automatewoo_optin',
        // Klaviyo.
        'klaviyo_optin',
        'klaviyo_is_subscribed',
        'klaviyo_subscribe',
        // ActiveCampaign.
        'activecampaign_for_woocommerce_accepts_marketing',
        'activecampaign_subscribe',
        // HubSpot.
        'hs_email_optin',
        'hubspot_optin',
        'hubspot_newsletter',
        // Drip.
        'drip_is_subscribed',
        'drip_optin',
        // Omnisend.
        'omnisend_opt_in',
        'omnisend_newsletter',
        // SendInBlue/Brevo.
        'sib_subscribe',
        'sib_user_confirmed',
        'sib_subscribe_on_checkout',
        'sib_optin',
        // Moosend.
        'moosend_optin',
        'moosend_subscribe',
        // GetResponse.
        'getresponse_subscribe',
        'gr_subscribed',
        'gr_optin',
        // Constant Contact.
        'ctct_subscribe',
        'constant_contact_optin',
        // ConvertKit.
        'convertkit_optin',
        'ck_subscribe',
        // MailerLite.
        'mailerlite_optin',
        'ml_subscribe',
        'ml_optin',
        // Flashy.
        'flashy_optin',
        'flashy_accepts_marketing',
        'flashy_subscribe',
        // רב מסר (Israeli).
        'rabm_optin',
        'rabm_subscribe',
        // שלח מסר (Israeli).
        'send_msg_optin',
        'send_optin',
        'send_newsletter',
        // סמוב (Israeli).
        'smoove_optin',
        'smoove_subscribe',
        'smoove_newsletter',
        // אינפוריו (Israeli).
        'inforu_optin',
        'inforu_subscribe',
        'marketing_optin',
        // Elementor / Custom.
        'elementor_newsletter',
        'elementor_optin',
        'form_newsletter',
        'form_subscribe',
        // Checkout Editor.
        'billing_newsletter',
        'billing_subscribe',
        'billing_marketing_optin',
        'billing_email_marketing',
        'billing_optin',
        'billing_accepts_marketing',
        // Generic.
        'newsletter',
        'newsletter_optin',
        'newsletter_subscribe',
        'subscribe_to_newsletter',
        'email_marketing',
        'opt_in',
        'optin',
        'accepts_marketing',
        'accept_marketing',
        'marketing_consent',
        'subscribe',
        'is_subscribed',
        'gdpr_consent',
        'gdpr_accept',
        // Misc.
        'remarkety_optin',
        'zoho_optin',
        'zoho_marketing',
        'keap_optin',
        'infusionsoft_optin',
        'pipedrive_optin',
        'sfmc_optin',
        'sfmc_subscribe',
    );

    /**
     * Normalize order objects to ensure billing data is available.
     *
     * @param WC_Order|WC_Order_Refund|object|null $order
     * @return WC_Order|null
     */
    private function resolve_order_with_billing_details($order)
    {
        if (!$order || !is_object($order)) {
            return null;
        }

        if (method_exists($order, 'get_billing_email')) {
            return $order;
        }

        $is_refund_like = $order instanceof WC_Order_Refund || is_a($order, 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund');
        $parent_order = null;

        if (method_exists($order, 'get_parent_id')) {
            $parent_id = $order->get_parent_id();
            if ($parent_id) {
                $parent_order = wc_get_order($parent_id);
            }
        }

        if ($parent_order && method_exists($parent_order, 'get_billing_email')) {
            $this->log(
                'Using parent order for billing data',
                array(
                    'child_order_id' => method_exists($order, 'get_id') ? $order->get_id() : null,
                    'parent_order_id' => $parent_order->get_id(),
                    'child_type' => get_class($order),
                    'was_refund' => $is_refund_like ? 'yes' : 'no'
                ),
                'debug'
            );
            return $parent_order;
        }

        $this->log(
            'Order missing billing information - skipping',
            array(
                'order_id' => method_exists($order, 'get_id') ? $order->get_id() : null,
                'order_type' => get_class($order)
            ),
            'warning'
        );

        return null;
    }

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

        // Instantiate order handler for order sync methods.
        require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-order-handler.php';
        $this->order_handler = new ActiveTrail_Order_Handler($api_client, $settings);

        $this->init_hooks();
    }

    /**
     * Initialize WordPress hooks
     */
    private function init_hooks()
    {
        // AJAX handlers for historical sync.
        add_action('wp_ajax_activetrail_start_historical_sync', array($this, 'ajax_start_historical_sync'));
        add_action('wp_ajax_activetrail_get_sync_progress', array($this, 'ajax_get_sync_progress'));
        add_action('wp_ajax_activetrail_cancel_historical_sync', array($this, 'ajax_cancel_historical_sync'));

        // AJAX handler for batch processing when cron is disabled (removed for simplicity).
        // Note: If WordPress cron is disabled, user should set up system cron instead.

        // Cron hook for background processing.
        add_action('activetrail_process_historical_sync_batch', array($this, 'process_historical_sync_batch'));

        // Hook to trigger historical sync when API key changes - REMOVED: sync should only start manually via button click
        // add_action('activetrail_api_key_changed', array($this, 'offer_historical_sync'));
    }

    /**
     * Generate all historical contacts once (without limit/offset)
     * Uses email-based map to prevent duplicates
     *
     * @param string $date_from Start date.
     * @param string $date_to End date.
     * @return array Array of unique contacts indexed by email
     */
    private function generate_all_contacts_once($date_from, $date_to)
    {
        // Switch to plugin's language setting for translations.
        $locale_switched = $this->switch_to_plugin_language();
        
        try {
            $contacts_map = array(); // Use map to ensure unique emails
            $users_processed = 0;
            $users_added = 0;
            $users_skipped = 0;
            $orders_processed = 0;
            $orders_added = 0;
            $orders_skipped = 0;
            $orders_duplicate_email = 0;

            $this->log('Starting to generate all contacts', array('date_from' => $date_from, 'date_to' => $date_to), 'info');

        // Get all users (without limit/offset).
        $user_query = new WP_User_Query(array(
            'date_query' => array(
                array(
                    'after' => $date_from,
                    'before' => $date_to,
                    'inclusive' => true,
                ),
            ),
            'number' => -1, // Get all users
            'fields' => 'all',
        ));

        $all_users = $user_query->get_results();
        $this->log('Found users in date range', array('total_users' => count($all_users)), 'info');

        foreach ($all_users as $user) {
            $users_processed++;
            $email = strtolower(trim($user->user_email));
            if (empty($email)) {
                $users_skipped++;
                $this->log('Skipping user - empty email', array('user_id' => $user->ID), 'debug');
                continue;
            }

            $contact_data = $this->prepare_user_contact_data($user);
            if ($contact_data) {
                // Always generate externalId based on email (one-to-one) to ensure consistency.
                $contact_data['externalId'] = 'WC-CONTACT-' . md5($email);
                $contacts_map[$email] = $contact_data;
                $users_added++;
                $this->log('Added user contact', array('user_id' => $user->ID, 'email' => $email, 'external_id' => $contact_data['externalId']), 'debug');
            } else {
                $users_skipped++;
                $this->log('Skipped user contact (no consent or filtered out)', array('user_id' => $user->ID, 'email' => $email), 'debug');
            }
        }

        $this->log('Users processing complete', array(
            'processed' => $users_processed,
            'added' => $users_added,
            'skipped' => $users_skipped
        ), 'info');

        // Get enabled statuses from settings.
        $settings = ActiveTrail_Settings::get_all();
        $enabled_statuses = isset($settings['order_statuses']) ? $settings['order_statuses'] : array();
        
        // Backward compatibility: if empty array, use all statuses.
        if (empty($enabled_statuses)) {
            $status_list = array_keys(wc_get_order_statuses());
        } else {
            // Map enabled statuses to WooCommerce format (add 'wc-' prefix if needed).
            $status_list = array();
            foreach ($enabled_statuses as $status) {
                $status_key = strpos($status, 'wc-') === 0 ? $status : 'wc-' . $status;
                if (array_key_exists($status_key, wc_get_order_statuses())) {
                    $status_list[] = $status_key;
                }
            }
        }
        
        // Get all orders (without limit/offset).
        $order_args = array(
            'status' => $status_list,
            'date_created' => $date_from . '...' . $date_to,
            'limit' => -1, // Get all orders
            'return' => 'objects',
        );

        $orders = wc_get_orders($order_args);
        $this->log('Found orders in date range', array('total_orders' => count($orders)), 'info');

        foreach ($orders as $order) {
            $orders_processed++;
            $order_with_billing = $this->resolve_order_with_billing_details($order);
            if (!$order_with_billing) {
                $orders_skipped++;
                continue;
            }

            $email = strtolower(trim($order_with_billing->get_billing_email()));
            if (empty($email)) {
                $orders_skipped++;
                $this->log('Skipping order - empty email', array('order_id' => $order_with_billing->get_id()), 'debug');
                continue;
            }

            // Skip if we already have this email from users (users take priority).
            if (isset($contacts_map[$email])) {
                $orders_duplicate_email++;
                $this->log('Skipping order contact - email already exists from user', array('order_id' => $order->get_id(), 'email' => $email), 'debug');
                continue;
            }

            $contact_data = $this->prepare_order_contact_data($order_with_billing);
            if ($contact_data) {
                // Always generate externalId based on email (one-to-one) to ensure consistency.
                $contact_data['externalId'] = 'WC-CONTACT-' . md5($email);
                $contacts_map[$email] = $contact_data;
                $orders_added++;
                $this->log('Added order contact', array('order_id' => $order->get_id(), 'email' => $email, 'external_id' => $contact_data['externalId']), 'debug');
            } else {
                $orders_skipped++;
                $this->log('Skipped order contact (no consent or filtered out)', array('order_id' => $order->get_id(), 'email' => $email), 'debug');
            }
        }

        $this->log('Orders processing complete', array(
            'processed' => $orders_processed,
            'added' => $orders_added,
            'skipped' => $orders_skipped,
            'duplicate_email' => $orders_duplicate_email
        ), 'info');

        $total_contacts = count($contacts_map);
        $this->log('All contacts generation complete', array(
            'total_contacts' => $total_contacts,
            'users_added' => $users_added,
            'orders_added' => $orders_added,
            'users_skipped' => $users_skipped,
            'orders_skipped' => $orders_skipped
        ), 'info');

            // Convert map to indexed array (maintain order).
            return array_values($contacts_map);
        } finally {
            // Restore original locale.
            if ($locale_switched) {
                $this->restore_plugin_language();
            }
        }
    }

    /**
     * Get historical contacts from the last year
     * Uses transient to cache all contacts and provides stable pagination
     *
     * @param string $date_from Start date (default: 1 year ago).
     * @param string $date_to End date (default: now).
     * @param int $limit Limit number of results.
     * @param int $offset Offset for pagination.
     * @return array
     */
    public function get_historical_contacts($date_from = null, $date_to = null, $limit = 100, $offset = 0)
    {
        // Note: Language switching is handled in generate_all_contacts_once().
        // Default to last year if no date specified.
        if (!$date_from) {
            $date_from = gmdate('Y-m-d H:i:s', strtotime('-1 year'));
        }
        if (!$date_to) {
            $date_to = current_time('mysql');
        }

        // Create unique transient key based on date range.
        $transient_key = 'activetrail_historical_contacts_' . md5($date_from . '_' . $date_to);

        // Try to get from transient.
        $all_contacts = get_transient($transient_key);

        if (false === $all_contacts) {
            // Generate all contacts once.
            $this->log('Generating all historical contacts (not in cache)', array('date_from' => $date_from, 'date_to' => $date_to), 'info');
            $all_contacts = $this->generate_all_contacts_once($date_from, $date_to);

            // Store in transient for 12 hours.
            set_transient($transient_key, $all_contacts, 12 * HOUR_IN_SECONDS);

            $this->log('Generated and cached historical contacts', array('count' => count($all_contacts)), 'info');
        } else {
            $this->log('Using cached historical contacts', array('count' => count($all_contacts)), 'debug');
        }

        // Use array_slice for stable pagination.
        return array_slice($all_contacts, $offset, $limit);
    }

    /**
     * Get historical orders from WooCommerce by date range
     *
     * @param string|null $date_from Start date (Y-m-d H:i:s).
     * @param string|null $date_to End date (Y-m-d H:i:s).
     * @param int $limit Number of orders to retrieve.
     * @param int $offset Offset for pagination.
     * @return array Array of WC_Order objects
     */
    public function get_historical_orders($date_from = null, $date_to = null, $limit = 100, $offset = 0)
    {
        // Switch to plugin's language setting for translations.
        $locale_switched = $this->switch_to_plugin_language();
        
        try {
            if (!$date_from) {
                $date_from = gmdate('Y-m-d H:i:s', strtotime('-1 year'));
            }
            if (!$date_to) {
                $date_to = current_time('mysql');
            }

            // Get enabled statuses from settings.
            $settings = ActiveTrail_Settings::get_all();
            $enabled_statuses = isset($settings['order_statuses']) ? $settings['order_statuses'] : array();
            
            // Backward compatibility: if empty array, use all statuses.
            if (empty($enabled_statuses)) {
                $status_list = array_keys(wc_get_order_statuses());
            } else {
                // Map enabled statuses to WooCommerce format (add 'wc-' prefix if needed).
                $status_list = array();
                foreach ($enabled_statuses as $status) {
                    $status_key = strpos($status, 'wc-') === 0 ? $status : 'wc-' . $status;
                    if (array_key_exists($status_key, wc_get_order_statuses())) {
                        $status_list[] = $status_key;
                    }
                }
            }
            
            $order_args = array(
                'status' => $status_list,
                'date_created' => $date_from . '...' . $date_to,
                'limit' => $limit,
                'offset' => $offset,
                'return' => 'objects',
            );

            $orders = wc_get_orders($order_args);

            return $orders;
        } finally {
            // Restore original locale.
            if ($locale_switched) {
                $this->restore_plugin_language();
            }
        }
    }

    /**
     * Get total count of historical contacts
     *
     * @param string $date_from Start date.
     * @param string $date_to End date.
     * @return int
     */
    public function get_historical_contacts_count($date_from = null, $date_to = null)
    {
        global $wpdb;

        if (!$date_from) {
            $date_from = gmdate('Y-m-d H:i:s', strtotime('-1 year'));
        }
        if (!$date_to) {
            $date_to = current_time('mysql');
        }

        // Count unique emails from users.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for historical sync
        $user_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT user_email) 
             FROM {$wpdb->users} 
             WHERE user_registered >= %s AND user_registered <= %s",
            $date_from,
            $date_to
        ));

        // Count unique emails from orders.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Direct query needed for historical sync, meta queries are necessary
        $order_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT pm.meta_value) 
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
             WHERE p.post_type = 'shop_order'
             AND p.post_date >= %s AND p.post_date <= %s
             AND pm.meta_key = '_billing_email'
             AND pm.meta_value != ''",
            $date_from,
            $date_to
        ));

        // Get all unique emails to avoid double counting.
        $all_emails = array();

        // Get user emails.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for historical sync
        $user_emails = $wpdb->get_col($wpdb->prepare(
            "SELECT DISTINCT user_email 
             FROM {$wpdb->users} 
             WHERE user_registered >= %s AND user_registered <= %s",
            $date_from,
            $date_to
        ));

        foreach ($user_emails as $email) {
            $all_emails[strtolower(trim($email))] = true;
        }

        // Get order emails.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Direct query needed for historical sync, meta queries are necessary
        $order_emails = $wpdb->get_col($wpdb->prepare(
            "SELECT DISTINCT pm.meta_value 
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
             WHERE p.post_type = 'shop_order'
             AND p.post_date >= %s AND p.post_date <= %s
             AND pm.meta_key = '_billing_email'
             AND pm.meta_value != ''",
            $date_from,
            $date_to
        ));

        foreach ($order_emails as $email) {
            $all_emails[strtolower(trim($email))] = true;
        }

        return count($all_emails);
    }

    /**
     * Get total count of historical orders
     *
     * @param string|null $date_from Start date (Y-m-d H:i:s).
     * @param string|null $date_to End date (Y-m-d H:i:s).
     * @return int
     */
    public function get_historical_orders_count($date_from = null, $date_to = null)
    {
        if (!$date_from) {
            $date_from = gmdate('Y-m-d H:i:s', strtotime('-1 year'));
        }
        if (!$date_to) {
            $date_to = current_time('mysql');
        }

        // Get enabled statuses from settings.
        $settings = ActiveTrail_Settings::get_all();
        $enabled_statuses = isset($settings['order_statuses']) ? $settings['order_statuses'] : array();
        
        // Backward compatibility: if empty array, use all statuses.
        if (empty($enabled_statuses)) {
            $status_list = array_keys(wc_get_order_statuses());
        } else {
            // Map enabled statuses to WooCommerce format (add 'wc-' prefix if needed).
            $status_list = array();
            foreach ($enabled_statuses as $status) {
                $status_key = strpos($status, 'wc-') === 0 ? $status : 'wc-' . $status;
                if (array_key_exists($status_key, wc_get_order_statuses())) {
                    $status_list[] = $status_key;
                }
            }
        }
        
        // Use wc_get_orders() to get count (works with both HPOS and legacy post-based orders).
        $order_args = array(
            'status' => $status_list,
            'date_created' => $date_from . '...' . $date_to,
            'limit' => -1, // Get all to count
            'return' => 'ids', // Just get IDs for performance
        );

        $order_ids = wc_get_orders($order_args);
        $count = count($order_ids);

        $this->log('get_historical_orders_count result', array(
            'date_from' => $date_from,
            'date_to' => $date_to,
            'total_orders' => $count
        ), 'info');

        return $count;
    }

    /**
     * Detect opt-in from marketing meta keys
     * Checks user meta or post meta for opt-in consent from various email marketing plugins
     * Checks ALL keys before making a decision - prioritizes opt-in over opt-out
     *
     * @param int $object_id User ID or Post ID.
     * @param string $meta_type Type of meta: 'user' or 'post'.
     * @return array Array with 'optin' (bool|null), 'meta_key' (string), 'meta_value' (mixed)
     */
    private function detect_optin_from_meta_keys($object_id, $meta_type = 'user')
    {
        // Define opt-in values (case-insensitive).
        $optin_values = array('1', '1', 'yes', 'true', 'on', 'active', 'subscribe', 'subscribed', 'accepted', 'consent');

        // Define opt-out values (case-insensitive).
        $optout_values = array('0', '0', 'no', 'false', 'off', 'declined', 'unsubscribed', 'rejected');

        $optin_found = null;
        $optout_found = null;

        // Single pass: Check ALL keys before making a decision.
        foreach ($this->marketing_meta_keys as $key) {
            // Get meta value based on type.
            if ( 'user' === $meta_type ) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                $value = get_user_meta($object_id, $key, true);
            } else {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                $value = get_post_meta($object_id, $key, true);
            }

            // Skip if empty or null.
            if ($value === '' || $value === null) {
                continue;
            }

            // Convert to string for comparison.
            $value_str = is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
            $value_lower = strtolower(trim($value_str));

            // Check if it's an opt-in value.
            if (in_array($value_lower, array_map('strtolower', $optin_values), true) || $value === 1 || $value === true) {
                // Store first opt-in found (we'll use this if no other opt-in is found).
                if ($optin_found === null) {
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                    $optin_found = array(
                        'optin' => true,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                        'meta_key' => $key,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                        'meta_value' => $value
                    );
                    $this->log('Opt-in detected from marketing meta key', array(
                        'object_id' => $object_id,
                        'meta_type' => $meta_type,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                        'meta_key' => $key,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta query necessary for opt-in detection
                        'meta_value' => $value
                    ), 'debug');
                }
                // Continue checking all keys - don't break.
            }
            // Check if it's an opt-out value (only if we haven't found opt-in yet, or continue checking).
            elseif (in_array($value_lower, array_map('strtolower', $optout_values), true) || $value === 0 || $value === false) {
                // Store first opt-out found (we'll use this only if no opt-in is found).
                if ($optout_found === null) {
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                    $optout_found = array(
                        'optin' => false,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                        'meta_key' => $key,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                        'meta_value' => $value
                    );
                    $this->log('Opt-out detected from marketing meta key', array(
                        'object_id' => $object_id,
                        'meta_type' => $meta_type,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                        'meta_key' => $key,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta query necessary for opt-in detection
                        'meta_value' => $value
                    ), 'debug');
                }
                // Continue checking all keys - don't break.
            }
        }

        // Decision: Prioritize opt-in over opt-out.
        // If we found ANY opt-in, return opt-in (even if there's also opt-out).
        if ($optin_found !== null) {
            return $optin_found;
        }

        // If no opt-in found but opt-out found, return opt-out.
        if ($optout_found !== null) {
            return $optout_found;
        }

        // No relevant meta found.
        return array('optin' => null);
    }

    /**
     * Detect opt-in from user's order meta
     * Checks all orders for the user to find opt-in consent
     *
     * @param int $user_id User ID.
     * @param string $email User email.
     * @return array Array with 'optin' (bool), 'source' (string), 'order_id' (int|null)
     */
    private function detect_optin_from_order_meta($user_id, $email)
    {
        // Get orders for this user (by customer_id or email).
        $order_args = array(
            'status' => array_keys(wc_get_order_statuses()),
            'limit' => 50, // Limit to avoid performance issues
            'return' => 'ids',
        );

        // Try by customer_id first.
        if ($user_id > 0) {
            $order_args['customer_id'] = $user_id;
            $order_ids = wc_get_orders($order_args);
        } else {
            $order_ids = array();
        }

        // If no orders found by customer_id, try by email.
        if (empty($order_ids) && !empty($email)) {
            $order_args = array(
                'status' => array_keys(wc_get_order_statuses()),
                'billing_email' => $email,
                'limit' => 50,
                'return' => 'ids',
            );
            $order_ids = wc_get_orders($order_args);
        }

        if (empty($order_ids)) {
            return array('optin' => false);
        }

        // Check each order for opt-in.
        foreach ($order_ids as $order_id) {
            $detection = $this->detect_optin_from_meta_keys($order_id, 'post');

            if ($detection['optin'] === true) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                $this->log('Opt-in found in order meta', array(
                    'user_id' => $user_id,
                    'email' => $email,
                    'order_id' => $order_id,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detection['meta_key']
                ), 'debug');

                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                return array(
                    'optin' => true,
                    'source' => 'order',
                    'order_id' => $order_id,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detection['meta_key']
                );
            }
        }

        return array('optin' => false);
    }

    /**
     * Check if user has consent in any of their orders
     *
     * @param int $user_id User ID.
     * @param string $email User email.
     * @return array
     */
    private function check_user_orders_for_consent($user_id, $email)
    {
        $result = array(
            'has_consent' => false,
            'count' => 0,
            'sample_order_ids' => array()
        );

        // Get orders for this user (by customer_id or email).
        $order_args = array(
            'status' => array_keys(wc_get_order_statuses()),
            'limit' => 50, // Limit to avoid performance issues
            'return' => 'ids',
        );

        // Try by customer_id first.
        if ($user_id > 0) {
            $order_args['customer_id'] = $user_id;
            $order_ids = wc_get_orders($order_args);
        } else {
            $order_ids = array();
        }

        // If no orders found by customer_id, try by email.
        if (empty($order_ids) && !empty($email)) {
            $order_args = array(
                'status' => array_keys(wc_get_order_statuses()),
                'billing_email' => $email,
                'limit' => 50,
                'return' => 'ids',
            );
            $order_ids = wc_get_orders($order_args);
        }

        if (empty($order_ids)) {
            return $result;
        }

        // Check consent in orders.
        $orders_with_consent = array();
        foreach ($order_ids as $order_id) {
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for consent detection
            $consent = get_post_meta($order_id, '_activetrail_consent', true);
            if ( 'yes' === $consent ) {
                $orders_with_consent[] = $order_id;
            }
        }

        $result['has_consent'] = !empty($orders_with_consent);
        $result['count'] = count($orders_with_consent);
        $result['sample_order_ids'] = array_slice($orders_with_consent, 0, 5); // First 5 for logging

        return $result;
    }

    /**
     * Prepare user contact data for ActiveTrail
     *
     * @param WP_User $user User object.
     * @return array|null
     */
    private function prepare_user_contact_data($user)
    {
        if (!$user || empty($user->user_email)) {
            $this->log('prepare_user_contact_data: Invalid user or empty email', array('user_id' => $user ? $user->ID : 'null'), 'debug');
            return null;
        }

        // Get user meta data.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $first_name = get_user_meta($user->ID, 'first_name', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $last_name = get_user_meta($user->ID, 'last_name', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $phone = get_user_meta($user->ID, 'billing_phone', true);

        // Get billing address data.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $city = get_user_meta($user->ID, 'billing_city', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $address_1 = get_user_meta($user->ID, 'billing_address_1', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $address_2 = get_user_meta($user->ID, 'billing_address_2', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for user data retrieval
        $postcode = get_user_meta($user->ID, 'billing_postcode', true);

        // Combine address lines.
        $street = trim($address_1 . ' ' . $address_2);

        // Check ActiveTrail consent status first.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for consent detection
        $consent = get_user_meta($user->ID, '_activetrail_registration_consent', true);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta queries necessary for consent detection
        $checkout_consent = get_user_meta($user->ID, '_activetrail_consent', true);

        // Debug: Get all user meta to see what's actually stored.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for debugging
        $all_user_meta = get_user_meta($user->ID);
        $activetrail_meta = array();
        foreach ($all_user_meta as $key => $value) {
            if (strpos($key, 'activetrail-for-woocommerce') !== false) {
                $activetrail_meta[$key] = is_array($value) ? $value[0] : $value;
            }
        }

        // Determine subscription status.
        $subscription_status = false;
        $consent_source = 'none';
        $detected_meta_key = null;

        // Priority 1: Check ActiveTrail consent.
        if ( 'yes' === $consent ) {
            $subscription_status = true;
            $consent_source = 'registration';
            $this->log('User has ActiveTrail registration consent', array('user_id' => $user->ID, 'email' => $user->user_email), 'debug');
        } elseif ( 'yes' === $checkout_consent ) {
            $subscription_status = true;
            $consent_source = 'checkout';
            $this->log('User has ActiveTrail checkout consent', array('user_id' => $user->ID, 'email' => $user->user_email), 'debug');
        } else {
            // Priority 2: Check marketing meta keys from other plugins.
            $detection = $this->detect_optin_from_meta_keys($user->ID, 'user');

            if ($detection['optin'] === true) {
                $subscription_status = true;
                $consent_source = 'marketing_plugin';
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                $detected_meta_key = $detection['meta_key'];
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                $this->log('User has opt-in from marketing plugin', array(
                    'user_id' => $user->ID,
                    'email' => $user->user_email,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detected_meta_key,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta query necessary for opt-in detection
                    'meta_value' => $detection['meta_value']
                ), 'debug');
            } elseif ($detection['optin'] === false) {
                // Explicit opt-out found.
                $subscription_status = false;
                $consent_source = 'marketing_plugin_optout';
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                $detected_meta_key = $detection['meta_key'];
                $this->log('User has explicit opt-out from marketing plugin', array(
                    'user_id' => $user->ID,
                    'email' => $user->user_email,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detected_meta_key
                ), 'debug');
            } else {
                // Priority 3: Check order meta.
                $order_detection = $this->detect_optin_from_order_meta($user->ID, $user->user_email);

                if ($order_detection['optin'] === true) {
                    $subscription_status = true;
                    $consent_source = 'order_marketing_plugin';
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    $detected_meta_key = $order_detection['meta_key'];
                    $this->log('User has opt-in from order marketing plugin', array(
                        'user_id' => $user->ID,
                        'email' => $user->user_email,
                        'order_id' => $order_detection['order_id'],
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                        'meta_key' => $detected_meta_key
                    ), 'debug');
                } elseif ($order_detection['optin'] === false) {
                    // Explicit opt-out found in order.
                    $subscription_status = false;
                    $consent_source = 'order_marketing_plugin_optout';
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    $detected_meta_key = isset($order_detection['meta_key']) ? $order_detection['meta_key'] : null;
                    $order_id_for_log = isset($order_detection['order_id']) ? $order_detection['order_id'] : null;
                    $this->log('User has explicit opt-out from order marketing plugin', array(
                        'user_id' => $user->ID,
                        'email' => $user->user_email,
                        'order_id' => $order_id_for_log,
                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                        'meta_key' => $detected_meta_key
                    ), 'debug');
                } else {
                    // No consent found anywhere.
                    $subscription_status = false;
                    $consent_source = 'none';
                    $this->log('User has no consent - syncing as unsubscribed', array('user_id' => $user->ID, 'email' => $user->user_email), 'debug');
                }
            }
        }

        $this->log('Final user consent decision', array(
            'user_id' => $user->ID,
            'email' => $user->user_email,
            'subscription_status' => $subscription_status,
            'consent_source' => $consent_source,
            'detected_meta_key' => $detected_meta_key,
            'all_activetrail_meta' => $activetrail_meta
        ), 'debug');

        // Generate external ID - always use email-based format (one-to-one) for consistency.
        $email = strtolower(trim($user->user_email));
        $external_id = 'WC-CONTACT-' . md5($email);

        return array(
            'email' => $user->user_email,
            'first_name' => $first_name ? $first_name : '',
            'last_name' => $last_name ? $last_name : '',
            'phone' => $phone ? $phone : '',
            'city' => $city ? $city : '',
            'street' => $street ? $street : '',
            'zip_code' => $postcode ? $postcode : '',
            'subscriptionStatus' => $subscription_status,
            'registration_date' => $user->user_registered,
            'source' => 'woocommerce_historical_sync',
            'externalId' => $external_id,
            'externalName' => ACTIVETRAIL_EXTERNAL_NAME,
            'user_id' => $user->ID,
            'consent_source' => $consent_source,
            'detected_meta_key' => $detected_meta_key
        );
    }

    /**
     * Prepare order contact data for ActiveTrail
     *
     * @param WC_Order $order Order object.
     * @return array|null
     */
    private function prepare_order_contact_data($order)
    {
        $order = $this->resolve_order_with_billing_details($order);
        if (!$order || empty($order->get_billing_email())) {
            $this->log('prepare_order_contact_data: Invalid order or empty email', array('order_id' => $order ? $order->get_id() : 'null'), 'debug');
            return null;
        }

        // Check ActiveTrail consent status from order first.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for consent detection
        $consent = get_post_meta($order->get_id(), '_activetrail_consent', true);

        // Debug: Get all order meta to see what's actually stored.
        $all_order_meta = $order->get_meta_data();
        $activetrail_meta = array();
        foreach ($all_order_meta as $meta) {
            $key = $meta->get_data()['key'];
            if (strpos($key, 'activetrail-for-woocommerce') !== false) {
                $activetrail_meta[$key] = $meta->get_data()['value'];
            }
        }

        // Determine subscription status.
        $subscription_status = false;
        $consent_source = 'none';
        $detected_meta_key = null;

        // Priority 1: Check ActiveTrail consent.
        if ( 'yes' === $consent ) {
            $subscription_status = true;
            $consent_source = 'checkout';
            $this->log('Order has ActiveTrail consent', array('order_id' => $order->get_id(), 'email' => $order->get_billing_email()), 'debug');
        } else {
            // Priority 2: Check marketing meta keys from other plugins in order meta.
            $detection = $this->detect_optin_from_meta_keys($order->get_id(), 'post');

            if ($detection['optin'] === true) {
                $subscription_status = true;
                $consent_source = 'order_marketing_plugin';
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                $detected_meta_key = $detection['meta_key'];
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta queries necessary for opt-in detection
                $this->log('Order has opt-in from marketing plugin', array(
                    'order_id' => $order->get_id(),
                    'email' => $order->get_billing_email(),
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detected_meta_key,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Meta query necessary for opt-in detection
                    'meta_value' => $detection['meta_value']
                ), 'debug');
            } elseif ($detection['optin'] === false) {
                // Explicit opt-out found.
                $subscription_status = false;
                $consent_source = 'order_marketing_plugin_optout';
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                $detected_meta_key = $detection['meta_key'];
                $this->log('Order has explicit opt-out from marketing plugin', array(
                    'order_id' => $order->get_id(),
                    'email' => $order->get_billing_email(),
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for opt-in detection
                    'meta_key' => $detected_meta_key
                ), 'debug');
            } else {
                // No consent found.
                $subscription_status = false;
                $consent_source = 'none';
                $this->log('Order has no consent - syncing as unsubscribed', array('order_id' => $order->get_id(), 'email' => $order->get_billing_email()), 'debug');
            }
        }

        $this->log('Final order consent decision', array(
            'order_id' => $order->get_id(),
            'email' => $order->get_billing_email(),
            'subscription_status' => $subscription_status,
            'consent_source' => $consent_source,
            'detected_meta_key' => $detected_meta_key,
            'all_activetrail_meta' => $activetrail_meta
        ), 'debug');

        // Generate external ID - always use email-based format (one-to-one) for consistency.
        $email = strtolower(trim($order->get_billing_email()));
        $external_id = 'WC-CONTACT-' . md5($email);
        $customer_id = $order->get_customer_id();

        return array(
            'email' => $order->get_billing_email(),
            'first_name' => $order->get_billing_first_name(),
            'last_name' => $order->get_billing_last_name(),
            'phone' => $order->get_billing_phone(),
            'subscriptionStatus' => $subscription_status,
            'registration_date' => $order->get_date_created()->format('Y-m-d H:i:s'),
            'source' => 'woocommerce_historical_sync',
            'externalId' => $external_id,
            'externalName' => ACTIVETRAIL_EXTERNAL_NAME,
            'user_id' => $customer_id,
            'order_id' => $order->get_id(),
            'consent_source' => $consent_source,
            'detected_meta_key' => $detected_meta_key
        );
    }

    /**
     * Start historical sync process
     *
     * @param array $options Sync options.
     * @return array
     */
    public function start_historical_sync($options = array())
    {
        $this->log('start_historical_sync() called with options: ' . wp_json_encode($options), array(), 'debug');
        // Validate API configuration.
        if (!ActiveTrail_Settings::is_api_configured()) {
            $this->log('API key not configured, aborting historical sync', array(), 'error');
            return array(
                'success' => false,
                'message' => esc_html__('API key not configured', 'activetrail-for-woocommerce')
            );
        }

        // Set default options with sync_contacts and sync_orders flags.
        $options = wp_parse_args($options, array(
            'date_from' => gmdate('Y-m-d H:i:s', strtotime('-1 year')),
            'date_to' => current_time('mysql'),
            'batch_size' => $this->batch_size,
            'force_sync' => false,
            'sync_contacts' => true,
            'sync_orders' => true,
        ));

        // Clear transient for contacts when starting new sync (to ensure fresh data).
        $transient_key = 'activetrail_historical_contacts_' . md5($options['date_from'] . '_' . $options['date_to']);
        delete_transient($transient_key);
        $this->log('Cleared contacts transient for new sync', array('transient_key' => $transient_key), 'debug');

        // Check if sync is already running.
        $sync_status = get_option('activetrail_historical_sync_status', array());
        if (isset($sync_status['status']) && $sync_status['status'] === 'running') {
            // Cancel previous running sync to allow fresh start.
            $this->log('Historical sync already running. Cancelling previous sync to start a new one.', array('last_batch_at' => $sync_status['last_batch_at'] ?? null), 'warning');

            // Cancel any scheduled batches and reset status.
            $sync_status['status'] = 'cancelled';
            $sync_status['cancelled_at'] = current_time('mysql');

            update_option('activetrail_historical_sync_status', $sync_status);
            wp_clear_scheduled_hook('activetrail_process_historical_sync_batch');

            // Allow slight delay before restarting.
            sleep(1);
        }

        $total_contacts = 0;
        $total_orders = 0;
        $total_items = 0;

        if ($options['sync_contacts']) {
            // Generate all contacts once to get accurate count (accounts for consent filtering).
            // This also populates the transient for later use.
            $all_contacts = $this->generate_all_contacts_once($options['date_from'], $options['date_to']);
            $total_contacts = count($all_contacts);

            // Store in transient for later use in get_historical_contacts().
            $transient_key = 'activetrail_historical_contacts_' . md5($options['date_from'] . '_' . $options['date_to']);
            set_transient($transient_key, $all_contacts, 12 * HOUR_IN_SECONDS);

            $this->log("Total contacts to sync: {$total_contacts} (after consent filtering)", array(), 'info');
        }

        if ($options['sync_orders']) {
            $total_orders = $this->get_historical_orders_count($options['date_from'], $options['date_to']);
            $this->log("Total orders to sync: {$total_orders}", array(), 'debug');
        }

        $total_items = $total_contacts + $total_orders;
        $this->log("Total items to sync (contacts + orders): {$total_items}", array(), 'info');

        if ($total_items === 0) {
            // Build detailed message explaining why no items were found.
            $message_parts = array();
            
            if ($options['sync_contacts'] && $options['sync_orders']) {
                $message_parts[] = esc_html__('No contacts or orders found', 'activetrail-for-woocommerce');
            } elseif ($options['sync_contacts']) {
                $message_parts[] = esc_html__('No contacts found', 'activetrail-for-woocommerce');
            } elseif ($options['sync_orders']) {
                $message_parts[] = esc_html__('No orders found', 'activetrail-for-woocommerce');
            }
            
            $date_from_formatted = date_i18n(get_option('date_format'), strtotime($options['date_from']));
            $date_to_formatted = date_i18n(get_option('date_format'), strtotime($options['date_to']));
            
            $message = sprintf(
                /* translators: 1: sync type (contacts/orders), 2: start date, 3: end date */
                esc_html__('%1$s for the date range %2$s to %3$s. Please check:', 'activetrail-for-woocommerce'),
                implode(' or ', $message_parts),
                $date_from_formatted,
                $date_to_formatted
            );
            
            $message .= '<ul style="margin: 10px 0 0 20px;">';
            if ($options['sync_contacts']) {
                $message .= '<li>' . esc_html__('That there are users or orders created in this date range', 'activetrail-for-woocommerce') . '</li>';
                $message .= '<li>' . esc_html__('That users have provided consent (if consent is required)', 'activetrail-for-woocommerce') . '</li>';
            }
            if ($options['sync_orders']) {
                $message .= '<li>' . esc_html__('That there are orders with the selected statuses in this date range', 'activetrail-for-woocommerce') . '</li>';
            }
            $message .= '<li>' . esc_html__('That the date range is correct', 'activetrail-for-woocommerce') . '</li>';
            $message .= '</ul>';
            
            $this->log('No items found for historical sync in given date range', array(
                'date_from' => $options['date_from'],
                'date_to' => $options['date_to'],
                'sync_contacts' => $options['sync_contacts'],
                'sync_orders' => $options['sync_orders'],
                'total_contacts' => $total_contacts,
                'total_orders' => $total_orders
            ), 'warning');
            
            return array(
                'success' => false,
                'message' => $message,
                'no_items' => true,
                'details' => array(
                    'date_from' => $options['date_from'],
                    'date_to' => $options['date_to'],
                    'sync_contacts' => $options['sync_contacts'],
                    'sync_orders' => $options['sync_orders'],
                    'total_contacts' => $total_contacts,
                    'total_orders' => $total_orders
                )
            );
        }

        // Initialize sync status.
        $sync_status = array(
            'status' => 'running',
            'sync_phase' => 'contacts', // Start with contacts phase
            'started_at' => current_time('mysql'),
            'total_contacts' => $total_contacts,
            'total_orders' => $total_orders,
            'processed_contacts' => 0,
            'processed_orders' => 0,
            'successful_contacts' => 0,
            'failed_contacts_count' => 0,
            'failed_contacts' => 0, // Alias for failed_contacts_count for JavaScript compatibility
            'successful_orders' => 0,
            'failed_orders' => 0,
            'successful_syncs' => 0,
            'failed_syncs' => 0,
            'current_batch' => 0,
            'options' => $options,
            'errors' => array(),
            'failed_contacts_list' => array() // Store individual failed contacts for CSV export
        );

        update_option('activetrail_historical_sync_status', $sync_status);

        // Check if WordPress cron is disabled.
        $cron_disabled = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;

        if ($cron_disabled) {
            $this->log('WordPress cron is disabled - sync batches will run when WordPress cron is triggered manually or via system cron', array(), 'warning');
        }

        // Try to schedule first batch.
        $this->log('Scheduling first batch for historical sync', array('batch' => 0), 'debug');
        $scheduled = wp_schedule_single_event(time() + 5, 'activetrail_process_historical_sync_batch', array(0));

        if ($scheduled === false) {
            $this->log('Failed to schedule cron event - sync may not run automatically', array(), 'error');
            // Don't fail completely - just warn user.
            $sync_status['cron_warning'] = true;
            update_option('activetrail_historical_sync_status', $sync_status);
        }

        $this->log('Historical sync started', array(
            'total_contacts' => $total_contacts,
            'total_orders' => $total_orders,
            'date_range' => $options['date_from'] . ' to ' . $options['date_to']
        ));

        return array(
            'success' => true,
            'message' => sprintf(
                /* translators: %d: Total number of items to sync */
                esc_html__('Historical sync started. Processing %d items (contacts + orders).', 'activetrail-for-woocommerce'),
                $total_items
            ),
            'data' => $sync_status
        );
    }

    /**
     * Process a batch of historical contacts and orders
     *
     * @param int $batch_number Batch number to process.
     */
    public function process_historical_sync_batch($batch_number)
    {
        // Switch to plugin's language setting for translations.
        $locale_switched = $this->switch_to_plugin_language();
        
        try {
            $this->log('process_historical_sync_batch() called for batch: ' . $batch_number, array(), 'debug');
            $sync_status = get_option('activetrail_historical_sync_status', array());

            // Check if sync was cancelled.
            if (!isset($sync_status['status']) || $sync_status['status'] !== 'running') {
                $this->log('Sync status is not running, exiting batch processing early', array('status' => $sync_status['status'] ?? 'unknown'), 'warning');
                return;
            }

            $options = $sync_status['options'];
            $sync_phase = isset($sync_status['sync_phase']) ? $sync_status['sync_phase'] : 'contacts';

            $batch_successful = 0;
            $batch_failed = 0;
            $batch_errors = array();

        // PHASE 1: Process contacts.
        if ($sync_phase === 'contacts' && !empty($options['sync_contacts'])) {
            // Use batch size of 1000 for contacts (API limit).
            $contacts_batch_size = 1000;
            $contacts_offset = $sync_status['processed_contacts'] ?? 0;

            $contacts = $this->get_historical_contacts(
                $options['date_from'],
                $options['date_to'],
                $contacts_batch_size,
                $contacts_offset
            );

            $this->log('Processing contacts batch', array('count' => count($contacts), 'offset' => $contacts_offset), 'debug');

            if (!empty($contacts)) {
                // Get group ID for batch import.
                $group_id = ActiveTrail_Settings::get('group_id');
                if (empty($group_id)) {
                    $this->log('No group_id configured, skipping contacts batch', array(), 'warning');
                    $batch_errors[] = array(
                        'type' => 'contacts',
                        'error' => esc_html__('Group ID is required for contact import', 'activetrail-for-woocommerce')
                    );
                } else {
                    // Prepare contacts for batch import.
                    $contacts_to_import = array();
                    foreach ($contacts as $contact_data) {
                        // Ensure external ID is set.
                        if (empty($contact_data['externalId'])) {
                            if (function_exists('activetrail_external_id_for_contact')) {
                                $user_id = isset($contact_data['user_id']) ? $contact_data['user_id'] : 0;
                                $contact_data['externalId'] = activetrail_external_id_for_contact($user_id, $contact_data['email']);
                            }
                        }

                        // Ensure external name is set.
                        if (empty($contact_data['externalName'])) {
                            $contact_data['externalName'] = ACTIVETRAIL_EXTERNAL_NAME;
                        }

                        $contacts_to_import[] = $contact_data;
                    }

                    // Import contacts in smaller chunks to avoid timeouts.
                    $chunk_size = apply_filters('activetrail_historical_contacts_chunk_size', $this->contact_chunk_size);
                    $contact_chunks = array_chunk($contacts_to_import, max(1, intval($chunk_size)));

                    foreach ($contact_chunks as $chunk_index => $chunk_contacts) {
                        try {
                            $this->log('Importing contacts chunk', array(
                                'chunk_index' => $chunk_index,
                                'chunk_size' => count($chunk_contacts),
                                'total_contacts' => count($contacts_to_import)
                            ), 'debug');

                            $result = $this->api_client->import_contacts_batch($chunk_contacts, intval($group_id));

                            if ($result['success']) {
                                $success_count = isset($result['success_count']) ? intval($result['success_count']) : count($chunk_contacts);
                                $failed_count = isset($result['failed_count']) ? intval($result['failed_count']) : 0;

                                $batch_successful += $success_count;
                                $batch_failed += $failed_count;

                                // Mark users as synced (only for successful contacts).
                                // Note: We mark all as synced because API doesn't tell us which specific contacts failed.
                                // If failed_count > 0, we still mark all as processed to avoid infinite retries.
                                // The failed_count is tracked separately for reporting.
                                foreach ($chunk_contacts as $contact_data) {
                                    if (!empty($contact_data['user_id'])) {
                                        update_user_meta($contact_data['user_id'], '_activetrail_synced', 'yes');
                                        update_user_meta($contact_data['user_id'], '_activetrail_sync_date', current_time('mysql'));
                                    }
                                }

                                if ($failed_count > 0) {
                                    // Extract detailed error information from API response if available.
                                    $api_data = isset($result['data']) ? $result['data'] : array();
                                    $error_details = array();
                                    
                                    // Check if API provides detailed failure information.
                                    if (isset($api_data['errors']) && is_array($api_data['errors'])) {
                                        $error_details['api_errors'] = $api_data['errors'];
                                    }
                                    if (isset($api_data['failed_contacts']) && is_array($api_data['failed_contacts'])) {
                                        $error_details['failed_contacts'] = $api_data['failed_contacts'];
                                    }
                                    
                                    // Extract error reason from API response.
                                    $error_reason = $this->extract_error_reason($api_data, esc_html__('Partial failure in batch import', 'activetrail-for-woocommerce'));
                                    
                                    // Add failed contacts to tracking (if API provides specific failed contacts).
                                    if (isset($api_data['failed_contacts']) && is_array($api_data['failed_contacts']) && !empty($api_data['failed_contacts'])) {
                                        // API provided specific failed contacts - use them.
                                        foreach ($api_data['failed_contacts'] as $failed_contact) {
                                            $email = '';
                                            $user_id = '';
                                            
                                            // Try to extract email from various possible structures.
                                            if (isset($failed_contact['email'])) {
                                                $email = $failed_contact['email'];
                                            } elseif (isset($failed_contact['contact']['email'])) {
                                                $email = $failed_contact['contact']['email'];
                                            } elseif (isset($failed_contact['contact']) && is_string($failed_contact['contact'])) {
                                                // Sometimes contact might be just the email string.
                                                $email = $failed_contact['contact'];
                                            }
                                            
                                            // Try to extract user_id.
                                            if (isset($failed_contact['user_id'])) {
                                                $user_id = $failed_contact['user_id'];
                                            } elseif (isset($failed_contact['contact']['user_id'])) {
                                                $user_id = $failed_contact['contact']['user_id'];
                                            }
                                            
                                            if (!empty($email)) {
                                                $contact_error_reason = isset($failed_contact['error']) ? $failed_contact['error'] : 
                                                                        (isset($failed_contact['message']) ? $failed_contact['message'] : $error_reason);
                                                $this->add_failed_contact($email, $contact_error_reason, array(
                                                    'user_id' => $user_id,
                                                    'additional_info' => wp_json_encode($failed_contact)
                                                ));
                                            }
                                        }
                                    } else {
                                        // API doesn't provide specific failed contacts.
                                        // We can't know which specific ones failed, so we log a general error.
                                        // but don't mark individual contacts as failed (to avoid false positives).
                                        $this->log('Partial failure detected but API did not provide specific failed contacts', array(
                                            'chunk_index' => $chunk_index,
                                            'failed_count' => $failed_count,
                                            'total_in_chunk' => count($chunk_contacts)
                                        ), 'warning');
                                    }
                                    
                                    $this->log('Contacts chunk imported with partial failures', array(
                                        'chunk_index' => $chunk_index,
                                        'success_count' => $success_count,
                                        'failed_count' => $failed_count,
                                        'total_in_chunk' => count($chunk_contacts),
                                        'api_response_data' => $api_data,
                                        'error_details' => $error_details
                                    ), 'warning');
                                    
                                    // Log partial failure as error for tracking.
                                    /* translators: 1: Number of succeeded items, 2: Number of failed items */
                                    $error_message = sprintf(esc_html__('Partial failure: %1$d succeeded, %2$d failed', 'activetrail-for-woocommerce'), $success_count, $failed_count);
                                    if (!empty($error_details)) {
                                        $error_message .= ' - ' . esc_html__('Check logs for detailed error information', 'activetrail-for-woocommerce');
                                    }
                                    
                                    $batch_errors[] = array(
                                        'type' => 'contacts',
                                        'error' => $error_message,
                                        'chunk_index' => $chunk_index,
                                        'success_count' => $success_count,
                                        'failed_count' => $failed_count,
                                        'error_details' => $error_details
                                    );
                                } else {
                                    $this->log('Contacts chunk imported successfully', array(
                                        'chunk_index' => $chunk_index,
                                        'success_count' => $success_count,
                                        'failed_count' => $failed_count
                                    ), 'info');
                                }
                            } else {
                                $failed_contacts = count($chunk_contacts);
                                $batch_failed += $failed_contacts;
                                
                                // Extract detailed error information from API response if available.
                                $api_data = isset($result['data']) ? $result['data'] : array();
                                $error_message = isset($result['message']) ? $result['message'] : esc_html__('Unknown error', 'activetrail-for-woocommerce');
                                $error_reason = $this->extract_error_reason($api_data, $error_message);
                                
                                // Add all contacts in chunk as failed.
                                foreach ($chunk_contacts as $contact_data) {
                                    $email = isset($contact_data['email']) ? $contact_data['email'] : '';
                                    if (!empty($email)) {
                                        $this->add_failed_contact($email, $error_reason, array(
                                            'user_id' => isset($contact_data['user_id']) ? $contact_data['user_id'] : '',
                                            'additional_info' => esc_html__('Full chunk import failed', 'activetrail-for-woocommerce')
                                        ));
                                    }
                                }
                                
                                $this->log('Contacts chunk import failed', array(
                                    'chunk_index' => $chunk_index,
                                    'error' => $error_message,
                                    'api_response_data' => $api_data,
                                    'chunk_size' => count($chunk_contacts),
                                    'sample_emails' => array_slice(array_column($chunk_contacts, 'email'), 0, 5) // Log first 5 emails for debugging
                                ), 'error');
                                
                                $batch_errors[] = array(
                                    'type' => 'contacts',
                                    'error' => $error_message,
                                    'chunk_index' => $chunk_index,
                                    'api_response_data' => $api_data
                                );
                            }
                        } catch (Exception $e) {
                            $failed_contacts = count($chunk_contacts);
                            $batch_failed += $failed_contacts;
                            
                            // Extract sample emails for debugging.
                            $sample_emails = array_slice(array_column($chunk_contacts, 'email'), 0, 5);
                            
                            /* translators: %s: Exception message */
                            $error_reason = sprintf(esc_html__('Exception: %s', 'activetrail-for-woocommerce'), $e->getMessage());
                            
                            // Add all contacts in chunk as failed.
                            foreach ($chunk_contacts as $contact_data) {
                                $email = isset($contact_data['email']) ? $contact_data['email'] : '';
                                if (!empty($email)) {
                                    $this->add_failed_contact($email, $error_reason, array(
                                        'user_id' => isset($contact_data['user_id']) ? $contact_data['user_id'] : '',
                                        /* translators: %s: Exception class name */
                                        'additional_info' => sprintf(esc_html__('Exception type: %s', 'activetrail-for-woocommerce'), get_class($e))
                                    ));
                                }
                            }
                            
                            $this->log('Exception during contacts chunk import', array(
                                'chunk_index' => $chunk_index,
                                'exception' => $e->getMessage(),
                                'exception_type' => get_class($e),
                                'chunk_size' => count($chunk_contacts),
                                'sample_emails' => $sample_emails,
                                'trace' => $e->getTraceAsString()
                            ), 'error');
                            
                            $batch_errors[] = array(
                                'type' => 'contacts',
                                'error' => $e->getMessage(),
                                'chunk_index' => $chunk_index,
                                'exception_type' => get_class($e),
                                'chunk_size' => count($chunk_contacts)
                            );
                        }
                    }
                }

                $sync_status['processed_contacts'] += count($contacts);
            }

            // Check if we've finished the contacts phase.
            if (empty($contacts) || ($sync_status['processed_contacts'] >= $sync_status['total_contacts'])) {
                $this->log('Contacts phase completed, moving to orders phase', array(
                    'processed_contacts' => $sync_status['processed_contacts'],
                    'total_contacts' => $sync_status['total_contacts']
                ), 'info');

                // Clear contacts transient when finishing contacts phase (not needed for orders phase).
                $transient_key = 'activetrail_historical_contacts_' . md5($options['date_from'] . '_' . $options['date_to']);
                delete_transient($transient_key);
                $this->log('Cleared contacts transient after contacts phase completion', array('transient_key' => $transient_key), 'debug');

                // Move to orders phase if orders sync is enabled.
                if (!empty($options['sync_orders'])) {
                    $sync_status['sync_phase'] = 'orders';
                    $sync_status['current_batch'] = 0; // Reset batch counter for orders phase
                } else {
                    // No orders to sync, finish sync.
                    $this->finish_historical_sync();
                    return;
                }
            }
        }

        // PHASE 2: Process orders (only after contacts phase is complete).
        if ($sync_phase === 'orders' && !empty($options['sync_orders'])) {
            $orders_batch_size = $options['batch_size']; // Use configured batch size for orders
            $orders_offset = $sync_status['processed_orders'] ?? 0;

            $orders = $this->get_historical_orders(
                $options['date_from'],
                $options['date_to'],
                $orders_batch_size,
                $orders_offset
            );

            $this->log('Processing orders batch', array('count' => count($orders), 'offset' => $orders_offset), 'debug');

            foreach ($orders as $order) {
                try {
                    $resolved_order = $this->resolve_order_with_billing_details($order);
                    if (!$resolved_order) {
                        $batch_failed++;
                        $this->log('Skipping order sync - unable to resolve billing details', array('order_id' => method_exists($order, 'get_id') ? $order->get_id() : null, 'order_type' => is_object($order) ? get_class($order) : gettype($order)), 'warning');
                        continue;
                    }
                    
                    // Check if order status should trigger events (respect status filter).
                    $order_status = $resolved_order->get_status();
                    $settings = ActiveTrail_Settings::get_all();
                    $enabled_statuses = isset($settings['order_statuses']) ? $settings['order_statuses'] : array();
                    
                    // Backward compatibility: if empty array, treat as all enabled.
                    if (!empty($enabled_statuses)) {
                        $normalized_status = str_replace('wc-', '', $order_status);
                        if (!in_array($normalized_status, $enabled_statuses, true)) {
                            $this->log('Skipping order sync - status not enabled: ' . $order_status, array('order_id' => $resolved_order->get_id()), 'debug');
                            continue;
                        }
                    }

                    // Prepare order data using API client's transformer.
                    $order_data = $this->api_client->transform_wc_order_to_activetrail($resolved_order);

                    // Get external ID - use same format as contacts phase (email-based, one-to-one).
                    $customer_email = strtolower(trim($resolved_order->get_billing_email()));
                    $external_id = 'WC-CONTACT-' . md5($customer_email);

                    $order_data['externalId'] = $external_id;
                    $order_data['externalName'] = ACTIVETRAIL_EXTERNAL_NAME;

                    // Add group ID if configured.
                    $group_id = ActiveTrail_Settings::get('group_id');
                    if (!empty($group_id)) {
                        $order_data['groupId'] = intval($group_id);
                    }

                    // Sync order using API client.
                    $result = $this->api_client->submit_order($order_data);
                    $order_id = $resolved_order->get_id();
                    $this->log('Order sync result', array('order_id' => $order_id, 'result' => $result), 'debug');

                    if ($result['success']) {
                        $batch_successful++;
                        // Mark order as synced.
                        $order->update_meta_data('_activetrail_synced', 'yes');
                        $order->update_meta_data('_activetrail_sync_date', current_time('mysql'));
                        $order->save();
                    } else {
                        $batch_failed++;
                        
                        // Extract detailed error information from API response if available.
                        $api_data = isset($result['data']) ? $result['data'] : array();
                        $error_message = isset($result['message']) ? $result['message'] : esc_html__('Unknown error', 'activetrail-for-woocommerce');
                        
                        $this->log('Order sync failed', array(
                            'order_id' => $order_id,
                            'error' => $error_message,
                            'api_response_data' => $api_data,
                            'order_email' => $resolved_order->get_billing_email(),
                            'order_total' => $resolved_order->get_total(),
                            'order_date' => $resolved_order->get_date_created() ? $resolved_order->get_date_created()->format('Y-m-d H:i:s') : 'N/A'
                        ), 'error');
                        
                        $batch_errors[] = array(
                            'type' => 'order',
                            'order_id' => $order_id,
                            'error' => $error_message,
                            'api_response_data' => $api_data,
                            'order_email' => $resolved_order->get_billing_email()
                        );
                    }

                    usleep(100000); // 0.1 second between orders
                } catch (Exception $e) {
                    $batch_failed++;
                    $log_order_id = isset($resolved_order) && is_object($resolved_order) && method_exists($resolved_order, 'get_id')
                        ? $resolved_order->get_id()
                        : (is_object($order) && method_exists($order, 'get_id') ? $order->get_id() : null);
                    
                    $error_details = array(
                        'order_id' => $log_order_id,
                        'exception' => $e->getMessage(),
                        'exception_type' => get_class($e),
                        'trace' => $e->getTraceAsString()
                    );
                    
                    // Add order details if available.
                    if (isset($resolved_order) && is_object($resolved_order)) {
                        $error_details['order_email'] = method_exists($resolved_order, 'get_billing_email') ? $resolved_order->get_billing_email() : 'N/A';
                        $error_details['order_total'] = method_exists($resolved_order, 'get_total') ? $resolved_order->get_total() : 'N/A';
                    }
                    
                    $this->log('Exception during order sync', $error_details, 'error');
                    $batch_errors[] = array(
                        'type' => 'order',
                        'order_id' => $log_order_id,
                        'error' => $e->getMessage(),
                        'exception_type' => get_class($e)
                    );
                }
            }

            $sync_status['processed_orders'] += count($orders);
        }

        // Update sync status.
        $sync_status['successful_syncs'] += $batch_successful;
        $sync_status['failed_syncs'] += $batch_failed;
        
        // Track contacts and orders separately.
        if ( 'contacts' === $sync_phase ) {
            if (!isset($sync_status['successful_contacts'])) {
                $sync_status['successful_contacts'] = 0;
            }
            if (!isset($sync_status['failed_contacts_count'])) {
                $sync_status['failed_contacts_count'] = 0;
            }
            $sync_status['successful_contacts'] += $batch_successful;
            $sync_status['failed_contacts_count'] += $batch_failed;
            // Keep failed_contacts in sync with failed_contacts_count for JavaScript compatibility
            $sync_status['failed_contacts'] = $sync_status['failed_contacts_count'];
        } elseif ( 'orders' === $sync_phase ) {
            if (!isset($sync_status['successful_orders'])) {
                $sync_status['successful_orders'] = 0;
            }
            if (!isset($sync_status['failed_orders'])) {
                $sync_status['failed_orders'] = 0;
            }
            $sync_status['successful_orders'] += $batch_successful;
            $sync_status['failed_orders'] += $batch_failed;
        }
        
        $sync_status['current_batch'] = $batch_number + 1;
        $sync_status['last_batch_at'] = current_time('mysql');

        // Add errors (keep only last 50 errors to avoid memory issues).
        $sync_status['errors'] = array_merge($sync_status['errors'], $batch_errors);
        if (count($sync_status['errors']) > 50) {
            $sync_status['errors'] = array_slice($sync_status['errors'], -50);
        }

        update_option('activetrail_historical_sync_status', $sync_status);

        $this->log('Historical sync batch processed', array(
            'batch_number' => $batch_number,
            'sync_phase' => $sync_status['sync_phase'],
            'contacts_processed' => $sync_status['processed_contacts'],
            'orders_processed' => $sync_status['processed_orders'],
            'successful' => $batch_successful,
            'failed' => $batch_failed
        ));

        // Determine if we need to continue.
        $should_continue = false;

        if ( 'contacts' === $sync_status['sync_phase'] ) {
            // Continue if there are more contacts to process.
            $should_continue = ($sync_status['processed_contacts'] < $sync_status['total_contacts']);
        } elseif ( 'orders' === $sync_status['sync_phase'] ) {
            // Continue if there are more orders to process.
            $should_continue = ($sync_status['processed_orders'] < $sync_status['total_orders']);
        }

        if ($should_continue) {
            $this->log('Scheduling next batch', array('next_batch' => $batch_number + 1, 'phase' => $sync_status['sync_phase']), 'debug');

            // Schedule next batch.
            $scheduled = wp_schedule_single_event(time() + 10, 'activetrail_process_historical_sync_batch', array($batch_number + 1));

            if ($scheduled === false) {
                $this->log('Failed to schedule next batch via cron - sync may stop', array(), 'error');
                // Mark sync as having issues but don't stop it completely.
                $sync_status['cron_warning'] = true;
                update_option('activetrail_historical_sync_status', $sync_status);
            }
        } else {
            $this->log('All batches processed, finishing sync', array('phase' => $sync_status['sync_phase']), 'debug');
            // All items processed - finish sync.
            $this->finish_historical_sync();
        }
        } finally {
            // Restore original locale.
            if ($locale_switched) {
                $this->restore_plugin_language();
            }
        }
    }

    /**
     * Finish historical sync process
     */
    private function finish_historical_sync()
    {
        $sync_status = get_option('activetrail_historical_sync_status', array());

        $sync_status['status'] = 'completed';
        $sync_status['completed_at'] = current_time('mysql');

        // Generate CSV file with failed contacts if there are any.
        if (!empty($sync_status['failed_contacts_list']) && is_array($sync_status['failed_contacts_list'])) {
            $csv_path = $this->generate_failed_contacts_csv($sync_status['failed_contacts_list']);
            if ($csv_path) {
                $this->log('Generated failed contacts CSV file', array(
                    'filepath' => $csv_path,
                    'failed_count' => count($sync_status['failed_contacts_list'])
                ), 'info');
            }
        }

        update_option('activetrail_historical_sync_status', $sync_status);

        $this->log('Historical sync completed', array(
            'total_processed' => $sync_status['processed_contacts'],
            'successful' => $sync_status['successful_syncs'],
            'failed' => $sync_status['failed_syncs'],
            'failed_contacts_count' => isset($sync_status['failed_contacts_count']) ? $sync_status['failed_contacts_count'] : 0
        ));

        // Store sync history.
        $this->store_sync_history($sync_status);
    }

    /**
     * Store sync history for future reference
     *
     * @param array $sync_status Final sync status.
     */
    private function store_sync_history($sync_status)
    {
        $history = get_option('activetrail_historical_sync_history', array());

        $history[] = array(
            'started_at' => $sync_status['started_at'],
            'completed_at' => $sync_status['completed_at'],
            'total_contacts' => $sync_status['total_contacts'],
            'successful_syncs' => $sync_status['successful_syncs'],
            'failed_syncs' => $sync_status['failed_syncs'],
            'date_range' => $sync_status['options']['date_from'] . ' to ' . $sync_status['options']['date_to']
        );

        // Keep only last 10 sync records.
        if (count($history) > 10) {
            $history = array_slice($history, -10);
        }

        update_option('activetrail_historical_sync_history', $history);
    }

    /**
     * Cancel historical sync process
     *
     * @return array
     */
    public function cancel_historical_sync()
    {
        $sync_status = get_option('activetrail_historical_sync_status', array());

        if (!isset($sync_status['status']) || $sync_status['status'] !== 'running') {
            return array(
                'success' => false,
                'message' => esc_html__('No active sync to cancel', 'activetrail-for-woocommerce')
            );
        }

        $sync_status['status'] = 'cancelled';
        $sync_status['cancelled_at'] = current_time('mysql');

        update_option('activetrail_historical_sync_status', $sync_status);

        // Clear any scheduled batches.
        wp_clear_scheduled_hook('activetrail_process_historical_sync_batch');

        $this->log('Historical sync cancelled', array(
            'processed_before_cancel' => $sync_status['processed_contacts'],
            'successful_before_cancel' => $sync_status['successful_syncs']
        ));

        return array(
            'success' => true,
            'message' => esc_html__('Historical sync cancelled successfully', 'activetrail-for-woocommerce')
        );
    }

    /**
     * Get current sync progress
     *
     * @return array
     */
    public function get_sync_progress()
    {
        $sync_status = get_option('activetrail_historical_sync_status', array());

        if (empty($sync_status)) {
            return array(
                'status' => 'none',
                'message' => esc_html__('No sync has been started', 'activetrail-for-woocommerce')
            );
        }

        $sync_phase = isset($sync_status['sync_phase']) ? $sync_status['sync_phase'] : 'contacts';

        // Ensure all required fields are set with default values.
        $sync_status['total_contacts'] = isset($sync_status['total_contacts']) ? (int) $sync_status['total_contacts'] : 0;
        $sync_status['processed_contacts'] = isset($sync_status['processed_contacts']) ? (int) $sync_status['processed_contacts'] : 0;
        $sync_status['total_orders'] = isset($sync_status['total_orders']) ? (int) $sync_status['total_orders'] : 0;
        $sync_status['processed_orders'] = isset($sync_status['processed_orders']) ? (int) $sync_status['processed_orders'] : 0;
        $sync_status['successful_contacts'] = isset($sync_status['successful_contacts']) ? (int) $sync_status['successful_contacts'] : 0;
        $sync_status['failed_contacts'] = isset($sync_status['failed_contacts_count']) ? (int) $sync_status['failed_contacts_count'] : (isset($sync_status['failed_contacts']) ? (int) $sync_status['failed_contacts'] : 0);
        $sync_status['successful_orders'] = isset($sync_status['successful_orders']) ? (int) $sync_status['successful_orders'] : 0;
        $sync_status['failed_orders'] = isset($sync_status['failed_orders']) ? (int) $sync_status['failed_orders'] : 0;
        $sync_status['successful_syncs'] = isset($sync_status['successful_syncs']) ? (int) $sync_status['successful_syncs'] : 0;
        $sync_status['failed_syncs'] = isset($sync_status['failed_syncs']) ? (int) $sync_status['failed_syncs'] : 0;

        // Calculate progress percentage based on current phase.
        $progress_percentage = 0;
        $contacts_progress = 0;
        $orders_progress = 0;

        if ($sync_status['total_contacts'] > 0) {
            $contacts_progress = round(($sync_status['processed_contacts'] / $sync_status['total_contacts']) * 100, 1);
        }

        if ($sync_status['total_orders'] > 0) {
            $orders_progress = round(($sync_status['processed_orders'] / $sync_status['total_orders']) * 100, 1);
        }

        // Overall progress: weighted average (contacts + orders).
        $total_items = $sync_status['total_contacts'] + $sync_status['total_orders'];
        if ($total_items > 0) {
            $processed_items = $sync_status['processed_contacts'] + $sync_status['processed_orders'];
            $progress_percentage = round(($processed_items / $total_items) * 100, 1);
        }

        $sync_status['progress_percentage'] = $progress_percentage;
        $sync_status['contacts_progress'] = $contacts_progress;
        $sync_status['orders_progress'] = $orders_progress;
        $sync_status['sync_phase'] = $sync_phase;
        
        // Keep failed_contacts in sync with failed_contacts_count for JavaScript compatibility
        if (isset($sync_status['failed_contacts_count'])) {
            $sync_status['failed_contacts'] = $sync_status['failed_contacts_count'];
        } elseif (!isset($sync_status['failed_contacts'])) {
            $sync_status['failed_contacts'] = 0;
        }

        // Include failed contacts CSV info if available.
        if (isset($sync_status['failed_contacts_csv'])) {
            // CSV info is already in sync_status.
        }

        return $sync_status;
    }

    /**
     * Offer historical sync when API key changes
     */
    public function offer_historical_sync()
    {
        // Set a flag to show the historical sync offer in admin.
        update_option('activetrail_show_historical_sync_offer', true);
    }

    /**
     * AJAX handler for starting historical sync
     */
    public function ajax_start_historical_sync()
    {
        try {
            check_ajax_referer('activetrail_admin_nonce', 'nonce');
        } catch (Exception $e) {
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Security check failed', 'activetrail-for-woocommerce')
            ));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Insufficient permissions', 'activetrail-for-woocommerce')
            ));
            return;
        }

        // Check if WordPress cron is disabled and warn user.
        $cron_disabled = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;
        if ($cron_disabled) {
            $this->log('Historical sync started with WordPress cron disabled', array(), 'warning');
        }

        $date_from = isset($_POST['date_from']) ? sanitize_text_field(wp_unslash($_POST['date_from'])) : '';
        $date_to = isset($_POST['date_to']) ? sanitize_text_field(wp_unslash($_POST['date_to'])) : '';
        $force_sync = isset($_POST['force_sync']) && sanitize_text_field(wp_unslash($_POST['force_sync']));
        $sync_contacts = isset($_POST['sync_contacts']) ? filter_var(wp_unslash($_POST['sync_contacts']), FILTER_VALIDATE_BOOLEAN) : true;
        $sync_orders = isset($_POST['sync_orders']) ? filter_var(wp_unslash($_POST['sync_orders']), FILTER_VALIDATE_BOOLEAN) : false;

        $options = array();
        if (!empty($date_from)) {
            $options['date_from'] = $date_from;
        }
        if (!empty($date_to)) {
            $options['date_to'] = $date_to;
        }
        $options['force_sync'] = $force_sync;
        $options['sync_contacts'] = $sync_contacts;
        $options['sync_orders'] = $sync_orders;

        try {
            $result = $this->start_historical_sync($options);

            if ($result && isset($result['success']) && $result['success']) {
                // Add cron status to response.
                if ($cron_disabled) {
                    $result['cron_disabled'] = true;
                    $result['message'] .= ' ' . esc_html__('Note: WordPress cron is disabled, using alternative processing method.', 'activetrail-for-woocommerce');
                }
                wp_send_json_success($result);
            } else {
                $error_message = isset($result['message']) ? $result['message'] : esc_html__('Failed to start historical sync', 'activetrail-for-woocommerce');
                wp_send_json_error(array(
                    'success' => false,
                    'message' => $error_message
                ));
            }
        } catch (Exception $e) {
            $this->log('Exception in ajax_start_historical_sync', array('error' => $e->getMessage(), 'trace' => $e->getTraceAsString()), 'error');
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Error starting historical sync: ', 'activetrail-for-woocommerce') . $e->getMessage()
            ));
        } catch (Error $e) {
            // PHP 7+ Error handling.
            $this->log('Fatal error in ajax_start_historical_sync', array('error' => $e->getMessage(), 'trace' => $e->getTraceAsString()), 'error');
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Fatal error starting historical sync: ', 'activetrail-for-woocommerce') . $e->getMessage()
            ));
        }
    }

    /**
     * AJAX handler for getting sync progress
     */
    public function ajax_get_sync_progress()
    {
        try {
            check_ajax_referer('activetrail_admin_nonce', 'nonce');
        } catch (Exception $e) {
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Security check failed', 'activetrail-for-woocommerce')
            ));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Insufficient permissions', 'activetrail-for-woocommerce')
            ));
            return;
        }

        try {
            $progress = $this->get_sync_progress();
            wp_send_json_success($progress);
        } catch (Exception $e) {
            $this->log('Exception in ajax_get_sync_progress', array('error' => $e->getMessage(), 'trace' => $e->getTraceAsString()), 'error');
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Error getting sync progress: ', 'activetrail-for-woocommerce') . $e->getMessage()
            ));
        } catch (Error $e) {
            // PHP 7+ Error handling.
            $this->log('Fatal error in ajax_get_sync_progress', array('error' => $e->getMessage(), 'trace' => $e->getTraceAsString()), 'error');
            wp_send_json_error(array(
                'success' => false,
                'message' => esc_html__('Fatal error getting sync progress: ', 'activetrail-for-woocommerce') . $e->getMessage()
            ));
        }
    }

    /**
     * AJAX handler for cancelling historical sync
     */
    public function ajax_cancel_historical_sync()
    {
        check_ajax_referer('activetrail_admin_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Insufficient permissions', 'activetrail-for-woocommerce'));
        }

        $result = $this->cancel_historical_sync();

        if ($result['success']) {
            wp_send_json_success($result);
        } else {
            wp_send_json_error($result);
        }
    }

    /**
     * Add failed contact to tracking array
     *
     * @param string $email Contact email.
     * @param string $error_reason Error reason/message.
     * @param array $additional_data Additional data about the failure.
     */
    private function add_failed_contact($email, $error_reason, $additional_data = array())
    {
        $sync_status = get_option('activetrail_historical_sync_status', array());
        
        if (!isset($sync_status['failed_contacts_list'])) {
            $sync_status['failed_contacts_list'] = array();
        }
        
        // Avoid duplicates - check if email already exists.
        $email_lower = strtolower(trim($email));
        $exists = false;
        foreach ($sync_status['failed_contacts_list'] as $index => $failed) {
            if (strtolower(trim($failed['email'])) === $email_lower) {
                // Update existing entry if we have more details.
                if (!empty($error_reason) && empty($failed['error_reason'])) {
                    $sync_status['failed_contacts_list'][$index]['error_reason'] = $error_reason;
                }
                if (!empty($additional_data)) {
                    $sync_status['failed_contacts_list'][$index] = array_merge($sync_status['failed_contacts_list'][$index], $additional_data);
                }
                $exists = true;
                break;
            }
        }
        
        if (!$exists) {
            $sync_status['failed_contacts_list'][] = array_merge(array(
                'email' => $email,
                'error_reason' => $error_reason,
                'failed_at' => current_time('mysql')
            ), $additional_data);
        }
        
        update_option('activetrail_historical_sync_status', $sync_status);
    }

    /**
     * Generate CSV file with failed contacts
     *
     * @param array $failed_contacts Array of failed contacts.
     * @return string|false File path on success, false on failure
     */
    private function generate_failed_contacts_csv($failed_contacts)
    {
        if (empty($failed_contacts)) {
            return false;
        }

        $upload_dir = wp_upload_dir();
        if ($upload_dir['error']) {
            $this->log('Failed to get upload directory', array('error' => $upload_dir['error']), 'error');
            return false;
        }

        // Create activetrail directory if it doesn't exist.
        $activetrail_dir = $upload_dir['basedir'] . '/activetrail';
        if (!file_exists($activetrail_dir)) {
            wp_mkdir_p($activetrail_dir);
        }

        // Generate filename with timestamp.
        $timestamp = gmdate('Y-m-d_H-i-s');
        $filename = 'failed_contacts_' . $timestamp . '.csv';
        $filepath = $activetrail_dir . '/' . $filename;

        // Open file for writing.
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- fputcsv requires native fopen
        $file = fopen($filepath, 'w');
        if (!$file) {
            $this->log('Failed to create CSV file', array('filepath' => $filepath), 'error');
            return false;
        }

        // Add BOM for UTF-8 (helps Excel display Hebrew correctly).
        fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));

        // Write CSV header.
        $headers = array(
            esc_html__('Email', 'activetrail-for-woocommerce'),
            esc_html__('Error Reason', 'activetrail-for-woocommerce'),
            esc_html__('Failed At', 'activetrail-for-woocommerce'),
            esc_html__('User ID', 'activetrail-for-woocommerce'),
            esc_html__('Additional Info', 'activetrail-for-woocommerce')
        );
        fputcsv($file, $headers);

        // Write failed contacts data.
        foreach ($failed_contacts as $failed) {
            $row = array(
                isset($failed['email']) ? $failed['email'] : '',
                isset($failed['error_reason']) ? $failed['error_reason'] : '',
                isset($failed['failed_at']) ? $failed['failed_at'] : '',
                isset($failed['user_id']) ? $failed['user_id'] : '',
                isset($failed['additional_info']) ? $failed['additional_info'] : ''
            );
            fputcsv($file, $row);
        }
        
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- fputcsv requires native fclose
        fclose($file);

        // Store file path in sync status.
        $sync_status = get_option('activetrail_historical_sync_status', array());
        $sync_status['failed_contacts_csv'] = array(
            'filename' => $filename,
            'filepath' => $filepath,
            'url' => $upload_dir['baseurl'] . '/activetrail/' . $filename,
            'count' => count($failed_contacts)
        );
        update_option('activetrail_historical_sync_status', $sync_status);

        $this->log('Generated failed contacts CSV', array(
            'filepath' => $filepath,
            'count' => count($failed_contacts)
        ), 'info');

        return $filepath;
    }

    /**
     * Extract error reason from API response
     *
     * @param array $api_data API response data.
     * @param string $default_message Default error message.
     * @return string Error reason
     */
    private function extract_error_reason($api_data, $default_message = '')
    {
        $error_reason = $default_message;

        // Check for common error patterns in API response.
        // ActiveTrail API may return errors in different formats.
        if (isset($api_data['Message'])) {
            // ActiveTrail sometimes uses capital M.
            $error_reason = $api_data['Message'];
        } elseif (isset($api_data['message'])) {
            $error_reason = $api_data['message'];
        } elseif (isset($api_data['Error'])) {
            // ActiveTrail sometimes uses capital E.
            $error_reason = is_array($api_data['Error']) ? wp_json_encode($api_data['Error']) : $api_data['Error'];
        } elseif (isset($api_data['error'])) {
            $error_reason = is_array($api_data['error']) ? wp_json_encode($api_data['error']) : $api_data['error'];
        } elseif (isset($api_data['errors']) && is_array($api_data['errors'])) {
            $error_messages = array();
            foreach ($api_data['errors'] as $error) {
                if (is_array($error)) {
                    // Check for nested error structures.
                    if (isset($error['message'])) {
                        $error_messages[] = $error['message'];
                    } elseif (isset($error['Message'])) {
                        $error_messages[] = $error['Message'];
                    } elseif (isset($error['error'])) {
                        $error_messages[] = is_array($error['error']) ? wp_json_encode($error['error']) : $error['error'];
                    } else {
                        $error_messages[] = wp_json_encode($error);
                    }
                } else {
                    $error_messages[] = $error;
                }
            }
            $error_reason = implode('; ', $error_messages);
        } elseif (isset($api_data['ErrorMessage'])) {
            $error_reason = $api_data['ErrorMessage'];
        } elseif (isset($api_data['errorMessage'])) {
            $error_reason = $api_data['errorMessage'];
        }

        // Normalize error reason to lowercase for pattern matching.
        $error_reason_lower = strtolower($error_reason);

        // Check for specific error types and provide user-friendly messages.
        // 1. Contact already exists / Duplicate.
        if (stripos($error_reason, 'already exists') !== false || 
            stripos($error_reason, 'duplicate') !== false ||
            stripos($error_reason, 'כבר קיים') !== false ||
            stripos($error_reason, 'exists') !== false ||
            stripos($error_reason, 'already a member') !== false) {
            $error_reason = esc_html__('Contact already exists in ActiveTrail', 'activetrail-for-woocommerce');
        }
        // 2. Email validation errors.
        elseif (stripos($error_reason, 'invalid email') !== false || 
                stripos($error_reason, 'email is required') !== false ||
                stripos($error_reason, 'email format') !== false ||
                stripos($error_reason, 'email address') !== false ||
                (stripos($error_reason, 'email') !== false && stripos($error_reason, 'invalid') !== false)) {
            $error_reason = esc_html__('Invalid email address', 'activetrail-for-woocommerce');
        }
        // 3. Validation errors.
        elseif (stripos($error_reason, 'validation') !== false ||
                stripos($error_reason, 'validate') !== false ||
                stripos($error_reason, 'invalid') !== false) {
            $error_reason = esc_html__('Validation error', 'activetrail-for-woocommerce');
        }
        // 4. API authentication errors.
        elseif (stripos($error_reason, 'unauthorized') !== false ||
                stripos($error_reason, 'authentication') !== false ||
                stripos($error_reason, 'api key') !== false ||
                stripos($error_reason, 'forbidden') !== false ||
                stripos($error_reason, '401') !== false ||
                stripos($error_reason, '403') !== false) {
            $error_reason = esc_html__('API authentication failed', 'activetrail-for-woocommerce');
        }
        // 5. Rate limiting / Too many requests.
        elseif (stripos($error_reason, 'rate limit') !== false ||
                stripos($error_reason, 'too many requests') !== false ||
                stripos($error_reason, '429') !== false ||
                stripos($error_reason, 'limit') !== false) {
            $error_reason = esc_html__('API rate limit exceeded', 'activetrail-for-woocommerce');
        }
        // 6. Server errors.
        elseif (stripos($error_reason, '500') !== false ||
                stripos($error_reason, '502') !== false ||
                stripos($error_reason, '503') !== false ||
                stripos($error_reason, '504') !== false ||
                stripos($error_reason, 'server error') !== false ||
                stripos($error_reason, 'internal server') !== false) {
            $error_reason = esc_html__('ActiveTrail server error', 'activetrail-for-woocommerce');
        }
        // 7. Network/Connection errors.
        elseif (stripos($error_reason, 'connection') !== false ||
                stripos($error_reason, 'timeout') !== false ||
                stripos($error_reason, 'network') !== false ||
                stripos($error_reason, 'curl') !== false) {
            $error_reason = esc_html__('Network connection error', 'activetrail-for-woocommerce');
        }
        // 8. Missing required fields.
        elseif (stripos($error_reason, 'required') !== false ||
                stripos($error_reason, 'missing') !== false ||
                stripos($error_reason, 'mandatory') !== false) {
            $error_reason = esc_html__('Missing required field', 'activetrail-for-woocommerce');
        }
        // 9. Group/List errors.
        elseif (stripos($error_reason, 'group') !== false ||
                stripos($error_reason, 'list') !== false) {
            $error_reason = esc_html__('Group/List error', 'activetrail-for-woocommerce');
        }
        // 10. Import limit errors.
        elseif (stripos($error_reason, 'import limit') !== false ||
                stripos($error_reason, 'limit reached') !== false ||
                stripos($error_reason, 'limited to') !== false) {
            $error_reason = esc_html__('ActiveTrail import limit reached', 'activetrail-for-woocommerce');
        }

        // If we still have the default message or empty, return unknown error.
        if (empty($error_reason) || $error_reason === $default_message) {
            // If we have some API data, try to extract more info.
            if (!empty($api_data)) {
                // Try to get any error-related field.
                foreach (array('error', 'Error', 'errors', 'Errors', 'message', 'Message', 'errorMessage', 'ErrorMessage') as $field) {
                    if (isset($api_data[$field]) && !empty($api_data[$field])) {
                        $extracted = is_array($api_data[$field]) ? wp_json_encode($api_data[$field]) : $api_data[$field];
                        if ($extracted !== $error_reason) {
                            $error_reason = $extracted;
                            break;
                        }
                    }
                }
            }
            
            // If still empty or default, return unknown.
            if (empty($error_reason) || $error_reason === $default_message) {
                $error_reason = esc_html__('Unknown error', 'activetrail-for-woocommerce');
            }
        }

        return $error_reason;
    }

    /**
     * Switch to plugin's configured language
     *
     * @return bool True if locale was switched, false otherwise
     */
    private function switch_to_plugin_language()
    {
        $current_language = ActiveTrail_Settings::get_current_language();
        
        if (empty($current_language)) {
            return false;
        }
        
        // Support WPML if installed.
        if (function_exists('icl_object_id') && function_exists('do_action')) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WPML core global variable
            global $sitepress;
            if ($sitepress) {
                // Convert language code format if needed (e.g., en_US -> en).
                $wpml_lang = strstr($current_language, '_', true) ?: $current_language;
                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML hook
                do_action('wpml_switch_language', $wpml_lang);
            }
        }
        
        // Support Polylang if installed.
        if (function_exists('pll_current_language') && function_exists('PLL')) {
            $polylang = PLL();
            if ($polylang) {
                // Convert language code format if needed.
                $pll_lang = strstr($current_language, '_', true) ?: $current_language;
                $polylang->curlang = $polylang->model->get_language($pll_lang);
                if (!$polylang->curlang) {
                    // Try full code.
                    $polylang->curlang = $polylang->model->get_language($current_language);
                }
            }
        }
        
        // Switch WordPress locale for text domain translations.
        if (function_exists('switch_to_locale')) {
            return switch_to_locale($current_language);
        }
        
        return false;
    }

    /**
     * Restore original locale
     */
    private function restore_plugin_language()
    {
        // Restore WordPress locale.
        if (function_exists('restore_current_locale')) {
            restore_current_locale();
        }
        
        // WPML and Polylang don't need explicit restoration as they handle it automatically.
    }

    /**
     * Log message
     *
     * @param string $message Message to log.
     * @param array $data Additional data.
     * @param string $level Log level.
     */
    private function log($message, $data = array(), $level = 'info')
    {
        // Always log to WordPress error log for debugging.
        $log_message = '[ActiveTrail Historical Sync] ' . $message;
        if (!empty($data)) {
            $log_message .= ' - ' . wp_json_encode($data);
        }

        // Use the ActiveTrail logger if available.
        if (class_exists('ActiveTrail_Logger')) {
            $logger = ActiveTrail_Logger::get_instance();
            switch (strtolower($level)) {
                case 'error':
                    $logger->error($message, 'HISTORICAL_SYNC', $data);
                    break;
                case 'warning':
                    $logger->warning($message, 'HISTORICAL_SYNC', $data);
                    break;
                case 'debug':
                    $logger->debug($message, 'HISTORICAL_SYNC', $data);
                    break;
                default:
                    $logger->info($message, 'HISTORICAL_SYNC', $data);
                    break;
            }
        }
    }
}
