<?php
/**
 * Import background runner (real CRUD).
 *
 * @package PrimeLink
 */

namespace PrimeLink\Modules\Imports\Service;

use PrimeLink\Modules\Imports\Parsers\ParserFactory;
use PrimeLink\Modules\Imports\Service\IncrementalImportService;
use PrimeLink\Modules\Imports\Service\AdvancedDuplicateDetectionService;
use PrimeLink\Modules\Imports\Service\ImportTrackingService;
use PrimeLink\Modules\Imports\Service\PlatformConnectorService;
use PrimeLink\Modules\Imports\Service\AnomalyDetectionService;
use PrimeLink\Helpers\ImportPerformanceHelper;
use PrimeLink\Modules\Orders\OrderImportService;
use PrimeLink\Modules\Customers\CustomerImportService;
use PrimeLink\Modules\Coupons\CouponImportService;
use PrimeLink\Modules\Categories\CategoryImportService;
use PrimeLink\Modules\Tags\TagImportService;
use PrimeLink\Modules\Attributes\AttributeImportService;
use PrimeLink\Modules\Variations\VariationImportService;
use PrimeLink\Modules\Reviews\ReviewImportService;
use PrimeLink\Modules\Downloads\DownloadImportService;

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

final class ImportRunner {

    private static function state_key( $import_id ) {
        return 'mt_primelink_import_state_' . absint( $import_id );
    }

    public static function get_draft( $import_id ) {
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        return $drafts[ $import_id ] ?? null;
    }

    private static function save_drafts( $drafts ) {
        update_option( 'mt_primelink_import_drafts', $drafts, false );
    }

    private static function get_settings() {
        $settings = get_option( 'mt_primelink_settings', array() );
        if ( ! is_array( $settings ) ) {
            $settings = array();
        }
        $defaults = array(
            'cron_mode'  => 'wp',
            'cron_token' => '',
        );
        return array_merge( $defaults, $settings );
    }

    /**
     * Start scheduled imports if they are due (based on stored next_run).
     * Useful as a fallback when WP-Cron is delayed.
     */
    public static function maybe_start_schedules() {
        $settings = self::get_settings();
        $drafts   = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts ) || ! is_array( $drafts ) ) {
            return;
        }

        $now     = time();
        $updated = false;

        foreach ( $drafts as $import_id => &$draft ) {
            $import_id = absint( $import_id );
            if ( $import_id < 1 ) {
                continue;
            }
            $schedule = $draft['step_data'][6]['schedule'] ?? array();
            $enabled  = ! empty( $schedule['enabled'] );
            $next_run = isset( $schedule['next_run'] ) ? absint( $schedule['next_run'] ) : 0;
            if ( ! $enabled || $next_run < 1 || $next_run > $now ) {
                continue;
            }
            $state = get_option( self::state_key( $import_id ), array() );
            if ( isset( $state['status'] ) && $state['status'] === 'running' ) {
                continue;
            }
            self::start( $import_id );
            $draft['step_data'][6]['schedule'] = self::bump_next_run( $schedule );
            $updated = true;
        }
        unset( $draft );

        if ( $updated ) {
            self::save_drafts( $drafts );
        }
    }

    private static function bump_next_run( $schedule ) {
        $interval = $schedule['interval'] ?? 'daily';
        $now      = time();
        // Use current time as base to ensure proper interval spacing
        // This prevents immediate re-runs if the import takes longer than expected
        $current  = $now;
        $map = array(
            'hourly'     => HOUR_IN_SECONDS,
            'twicedaily' => 12 * HOUR_IN_SECONDS,
            'daily'      => DAY_IN_SECONDS,
            'weekly'     => WEEK_IN_SECONDS,
            'monthly'    => 30 * DAY_IN_SECONDS,
        );
        $step = $map[ $interval ] ?? DAY_IN_SECONDS;
        $next = $current + $step;
        $schedule['next_run'] = $next;
        return $schedule;
    }

    public static function tick_all() {
        $settings = self::get_settings();
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts ) || ! is_array( $drafts ) ) {
            return;
        }

        // In external cron mode, proactively start due schedules (in case WP-Cron misses).
        if ( ( $settings['cron_mode'] ?? 'wp' ) === 'external' ) {
            $now = time();
            $updated = false;
            foreach ( $drafts as $import_id => &$draft ) {
                $import_id = absint( $import_id );
                if ( $import_id < 1 ) {
                    continue;
                }
                $schedule = $draft['step_data'][6]['schedule'] ?? array();
                $enabled  = ! empty( $schedule['enabled'] );
                $next_run = isset( $schedule['next_run'] ) ? absint( $schedule['next_run'] ) : 0;
                if ( ! $enabled || $next_run < 1 || $next_run > $now ) {
                    continue;
                }
                $state = get_option( self::state_key( $import_id ), array() );
                if ( isset( $state['status'] ) && $state['status'] === 'running' ) {
                    continue;
                }
                self::start( $import_id );
                $draft['step_data'][6]['schedule'] = self::bump_next_run( $schedule );
                $updated = true;
            }
            unset( $draft );
            if ( $updated ) {
                self::save_drafts( $drafts );
            }
        }

        foreach ( array_keys( $drafts ) as $import_id ) {
            $import_id = absint( $import_id );
            if ( $import_id < 1 ) {
                continue;
            }
            $state = get_option( self::state_key( $import_id ), array() );
            if ( empty( $state ) || ( $state['status'] ?? '' ) !== 'running' ) {
                continue;
            }
            self::tick( $import_id );
        }
    }

    private static function save_state( $import_id, $state ) {
        update_option( self::state_key( $import_id ), $state, false );
    }

    public static function start( $import_id ) {
        $settings = self::get_settings();
        $draft = self::get_draft( $import_id );
        if ( empty( $draft ) ) {
            return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
        }

        $state = array(
            'status'     => 'running',
            'current'    => 0,
            'total'      => 0,
            'progress'   => 0,
            'log'        => array(),
            'started'    => current_time( 'mysql' ),
            'started_at' => current_time( 'mysql' ), // For statistics compatibility
            'paused'     => false,
            'finished'   => false,
            'errors'     => 0,
            'created'    => 0,
            'updated'    => 0,
            'skipped'    => 0,
        );

        self::save_state( $import_id, $state );

        // Start processing immediately instead of waiting for cron
        // This ensures import starts right away when user clicks Start
        self::tick( $import_id );

        // Also schedule next tick for continuation (if still running)
        if ( ( $settings['cron_mode'] ?? 'wp' ) !== 'external' ) {
            $current_state = get_option( self::state_key( $import_id ), array() );
            if ( ( $current_state['status'] ?? '' ) === 'running' ) {
                if ( ! wp_next_scheduled( 'mt_primelink_import_tick', array( $import_id ) ) ) {
                    wp_schedule_single_event( time() + 1, 'mt_primelink_import_tick', array( $import_id ) );
                }
            }
        }

        // Return updated state after first tick
        return get_option( self::state_key( $import_id ), $state );
    }

    public static function pause( $import_id ) {
        $state = get_option( self::state_key( $import_id ), array() );
        $state['status'] = 'paused';
        $state['paused'] = true;
        self::save_state( $import_id, $state );
        return $state;
    }

    public static function resume( $import_id ) {
        $state = get_option( self::state_key( $import_id ), array() );
        $state['status'] = 'running';
        $state['paused'] = false;
        self::save_state( $import_id, $state );

        // Schedule next tick only if not in external cron mode
        $settings = self::get_settings();
        if ( ( $settings['cron_mode'] ?? 'wp' ) !== 'external' ) {
            if ( ! wp_next_scheduled( 'mt_primelink_import_tick', array( $import_id ) ) ) {
                wp_schedule_single_event( time() + 1, 'mt_primelink_import_tick', array( $import_id ) );
            }
        }

        return $state;
    }

    public static function status( $import_id ) {
        $state = get_option( self::state_key( $import_id ), array(
            'status'   => 'idle',
            'current'  => 0,
            'total'    => 0,
            'progress' => 0,
            'log'      => array(),
            'errors'   => 0,
            'created'  => 0,
            'updated'  => 0,
            'skipped'  => 0,
        ) );
        
        // Add anomaly information if available
        if ( isset( $state['has_anomalies'] ) && $state['has_anomalies'] ) {
            $anomalies_data = get_post_meta( $import_id, '_mt_primelink_anomalies', true );
            $state['anomalies_count'] = is_array( $anomalies_data ) ? count( $anomalies_data ) : 0;
        }
        
        return $state;
    }

    /**
     * Tick handler: process next N records.
     */
    public static function tick( $import_id ) {
        $state = get_option( self::state_key( $import_id ), array() );
        if ( empty( $state ) || $state['status'] !== 'running' ) {
            return;
        }

        // Enable Fast Mode if configured (global setting or per-import setting)
        $fast_mode_enabled = false;
        $should_enable_fast_mode = ImportPerformanceHelper::is_fast_mode_enabled();
        
        // Check if this specific import has speed_optimization enabled in Step 6
        $draft = self::get_draft( $import_id );
        if ( ! empty( $draft ) ) {
            $speed_optimization = ! empty( $draft['step_data'][6]['speed_optimization'] );
            if ( $speed_optimization ) {
                $should_enable_fast_mode = true;
            }
        }
        
        if ( $should_enable_fast_mode ) {
            ImportPerformanceHelper::enable_fast_mode();
            $fast_mode_enabled = true;
        }

        try {
            // Re-get draft if we didn't get it above
            if ( empty( $draft ) ) {
                $draft = self::get_draft( $import_id );
            }
            if ( empty( $draft ) ) {
                $state['status'] = 'error';
                $state['log'][]  = 'Draft not found. Import cannot continue.';
                self::save_state( $import_id, $state );
                // Disable Fast Mode before return
                if ( $fast_mode_enabled ) {
                    ImportPerformanceHelper::disable_fast_mode();
                }
                return;
            }
        // Try multiple ways to get import type
        $import_type = $draft['type'] ?? ($draft['step_data'][1]['type'] ?? 'product');
        if ( isset($_GET['import_type']) ) {
            $import_type = sanitize_text_field($_GET['import_type']);
        }
        // Normalize: 'customers' -> 'customer', 'coupons' -> 'coupon', 'categories' -> 'category', 'tags' -> 'tag', 'attributes' -> 'attribute'
        if ( $import_type === 'customers' ) {
            $import_type = 'customer';
        } elseif ( $import_type === 'coupons' ) {
            $import_type = 'coupon';
        } elseif ( $import_type === 'categories' ) {
            $import_type = 'category';
        } elseif ( $import_type === 'tags' ) {
            $import_type = 'tag';
        } elseif ( $import_type === 'attributes' ) {
            $import_type = 'attribute';
        } elseif ( $import_type === 'variations' ) {
            $import_type = 'variation';
        } elseif ( $import_type === 'reviews' ) {
            $import_type = 'review';
        } elseif ( $import_type === 'downloads' ) {
            $import_type = 'download';
        }

        // Check if this is a platform connector import
        $step2_data = $draft['step_data'][2] ?? array();
        $source = $step2_data['source'] ?? '';
        
        if ( $source === 'platform' ) {
            // Platform connector import
            $connector_name = $step2_data['connector'] ?? '';
            $credentials    = $step2_data['credentials'] ?? array();
            $data_type      = $step2_data['data_type'] ?? 'products';
            
            if ( empty( $connector_name ) || empty( $credentials ) ) {
                $state['status'] = 'error';
                $state['log'][]  = 'Platform connector not configured.';
                self::save_state( $import_id, $state );
                if ( $fast_mode_enabled ) {
                    ImportPerformanceHelper::disable_fast_mode();
                }
                return;
            }

            // Fetch data from platform
            // Shopify uses 'since_id' for pagination, other platforms may use 'page'
            $options = array(
                'limit' => 250,
            );
            
            // For Shopify, use since_id instead of page
            if ( $connector_name === 'shopify' ) {
                $last_item_id = isset( $state['platform_last_id'] ) ? absint( $state['platform_last_id'] ) : 0;
                if ( $last_item_id > 0 ) {
                    $options['since_id'] = $last_item_id;
                }
            } else {
                // For other platforms (Magento, BigCommerce), use page
                $options['page'] = isset( $state['platform_page'] ) ? absint( $state['platform_page'] ) : 1;
            }
            
            $result = PlatformConnectorService::fetch_data( $connector_name, $credentials, $data_type, $options );
            
            if ( ! $result['success'] ) {
                $state['status'] = 'error';
                $state['log'][]  = 'Failed to fetch data from platform: ' . ( $result['error'] ?? 'Unknown error' );
                self::save_state( $import_id, $state );
                if ( $fast_mode_enabled ) {
                    ImportPerformanceHelper::disable_fast_mode();
                }
                return;
            }

            // Convert platform data to rows format
            $platform_items = $result['data'] ?? array();
            $headers = array(); // Platform data doesn't use headers in the same way
            $rows = array();
            
            foreach ( $platform_items as $platform_item ) {
                // Convert to WooCommerce format
                $woo_data = PlatformConnectorService::convert_data( $connector_name, $platform_item, $data_type );
                // Flatten to row format
                $row = self::flatten_woo_data_to_row( $woo_data );
                $rows[] = $row;
                
                // Track last item ID for Shopify pagination
                if ( $connector_name === 'shopify' ) {
                    $item_id = 0;
                    if ( $data_type === 'products' && isset( $platform_item['id'] ) ) {
                        $item_id = absint( $platform_item['id'] );
                    } elseif ( $data_type === 'orders' && isset( $platform_item['id'] ) ) {
                        $item_id = absint( $platform_item['id'] );
                    } elseif ( $data_type === 'customers' && isset( $platform_item['id'] ) ) {
                        $item_id = absint( $platform_item['id'] );
                    }
                    if ( $item_id > 0 ) {
                        $state['platform_last_id'] = $item_id;
                    }
                }
            }
            
            $total = $result['total'] ?? count( $rows );
            if ( $state['total'] < 1 ) {
                $state['total'] = $total;
            }
            
            // Store platform page for pagination (for non-Shopify platforms)
            if ( $connector_name !== 'shopify' ) {
                if ( ! isset( $state['platform_page'] ) ) {
                    $state['platform_page'] = 1;
                }
                // Increment page for next iteration if there's more data
                if ( ! empty( $result['has_more'] ) && ! empty( $rows ) ) {
                    $state['platform_page']++;
                }
            } else {
                // For Shopify, pagination is handled via since_id (already set above)
                // If no more items, we're done
                if ( empty( $rows ) ) {
                    $state['status'] = 'finished';
                }
            }
            
        } else {
            // File-based import (existing logic)
            $file = self::extract_file_from_draft( $draft );
            if ( empty( $file ) || empty( $file['path'] ) ) {
                $state['status'] = 'error';
                $state['log'][]  = 'No file found for import runner.';
                $state['log'][]  = 'Step2 data snapshot: ' . wp_json_encode( $draft['step_data'][2] ?? array() );
                self::save_state( $import_id, $state );
                if ( $fast_mode_enabled ) {
                    ImportPerformanceHelper::disable_fast_mode();
                }
                return;
            }

            $file   = $draft['step_data'][2]['file'];
            $parser = ParserFactory::make( $file['type'] );
            $filters = $draft['step_data'][4]['filters'] ?? array();
            $filters = apply_filters( 'primelink_import_filters', $filters, $import_id, $file );
            $parser_args = array(
                'xpath'    => $draft['step_data'][4]['xpath'] ?? '',
                'max_rows' => 0,
                'filters'  => $filters,
            );
            $parser_args = apply_filters( 'primelink_import_parser_args', $parser_args, $import_id, $file );
            $parsed = $parser->preview( $file['path'], $parser_args );
            $parsed = apply_filters( 'primelink_import_parser_result', $parsed, $import_id, $file, $parser_args );
            $headers = $parsed['headers'] ?? array();
            $rows    = $parsed['rows'] ?? array();
        }
        
        $filters = $draft['step_data'][4]['filters'] ?? array();
        $filters = self::normalize_filters( $filters );
        $rows    = self::apply_filters_to_rows( $headers, $rows, $filters );
        $total   = count( $rows );
        if ( $state['total'] < 1 ) {
            $state['total'] = $total;
        }

        // Step configs
        $settings = get_option( 'mt_primelink_settings', array() );
        $settings_iter = isset( $settings['default_iteration_size'] ) ? max( 1, min( 500, absint( $settings['default_iteration_size'] ) ) ) : 20;
        $settings_speed_opt = ! empty( $settings['default_speed_opt'] );
        $mapping_state      = $draft['step_data'][5] ?? array();
        $mapping            = $mapping_state['mapping'] ?? $mapping_state;
        $custom_fields      = $mapping_state['custom_fields'] ?? array();
        $row_filters        = $mapping_state['row_filters'] ?? array();
        $import_mode = $draft['step_data'][3]['handle_mode'] ?? 'new_update';
        $mode = self::normalize_mode( $import_mode );
        // Default match_by based on import type
        if ( $import_type === 'customer' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'email';
        } elseif ( $import_type === 'order' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'order_id';
        } elseif ( $import_type === 'category' || $import_type === 'tag' || $import_type === 'attribute' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'slug';
        } elseif ( $import_type === 'variation' || $import_type === 'variations' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'sku';
        } elseif ( $import_type === 'review' || $import_type === 'reviews' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'product_author';
        } elseif ( $import_type === 'download' || $import_type === 'downloads' ) {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'file_name';
        } else {
            $match_by = $draft['step_data'][6]['match_existing'] ?? 'title';
        }
        $update_mode   = $draft['step_data'][6]['update_mode'] ?? 'all';
        $update_fields = $draft['step_data'][6]['update_fields'] ?? array();
        $start = absint( $state['current'] );
        $end   = min( $start + $settings_iter, $total );
        
        // OPTIMIZATION: Batch lookup existing items before processing
        $batch_lookups = self::prepare_batch_lookups( $rows, $headers, $mapping, $custom_fields, $import_type, $match_by, $start, $end );

        for ( $i = $start; $i < $end; $i++ ) {
            $row = $rows[ $i ];
            $item = self::row_to_item( $headers, $row );

            try {
                if ( ! self::row_matches_filters( $item, $row_filters ) ) {
                    $state['skipped']++;
                    $state['log'][] = self::log_line( sprintf('Row %d skipped (filters)', $i + 1 ) );
                    continue;
                }
                $mapped = self::apply_mapping_to_item( $item, $mapping, $custom_fields, $import_type );
                
                // ANOMALY DETECTION - Check for anomalies before processing
                $previous_data = null;
                if ( $import_type === 'product' ) {
                    $existing_id = self::find_existing_product_id( $mapped, $match_by, $batch_lookups );
                    if ( $existing_id > 0 ) {
                        // Get previous data for comparison
                        $product = wc_get_product( $existing_id );
                        if ( $product ) {
                            // CRITICAL: Normalize prices to float values, handling empty strings and formatted values
                            // WooCommerce may return empty string '' or formatted strings, which would cause false positives
                            $prev_regular = $product->get_regular_price();
                            $prev_sale = $product->get_sale_price();
                            
                            // Convert to float, handling empty strings and formatted values
                            $prev_regular = $prev_regular === '' || $prev_regular === null ? 0 : floatval( $prev_regular );
                            $prev_sale = $prev_sale === '' || $prev_sale === null ? 0 : floatval( $prev_sale );
                            
                            $previous_data = array(
                                'regular_price' => $prev_regular > 0 ? $prev_regular : '',
                                'sale_price' => $prev_sale > 0 ? $prev_sale : '',
                                'stock_quantity' => $product->get_stock_quantity(),
                                'stock_status' => $product->get_stock_status(),
                                'post_content' => $product->get_description(),
                                'post_excerpt' => $product->get_short_description(),
                                'post_title' => $product->get_name(),
                            );
                        }
                    }
                }
                
                $anomalies = AnomalyDetectionService::analyze_item( $mapped, $previous_data, $import_type, $import_id );
                
                // Store anomalies for review.
                // Only SKIP the row when there is at least one CRITICAL anomaly
                // and "pause on critical" is enabled. Warnings/info just log but allow import.
                if ( ! empty( $anomalies ) ) {
                    AnomalyDetectionService::store_anomaly( $import_id, $i, $mapped, $anomalies );
                    
                    // Log all anomalies
                    foreach ( $anomalies as $anomaly ) {
                        $state['log'][] = self::log_line( sprintf( 
                            'Row %d [%s]: %s - %s',
                            $i + 1,
                            strtoupper( $anomaly['level'] ),
                            $anomaly['type'],
                            $anomaly['message']
                        ) );
                    }
                    
                    // Determine if we should pause/skip on critical anomalies
                    $has_critical = false;
                    foreach ( $anomalies as $anomaly ) {
                        if ( isset( $anomaly['level'] ) && 'critical' === $anomaly['level'] ) {
                            $has_critical = true;
                            break;
                        }
                    }
                    
                    if ( $has_critical ) {
                        $settings           = get_option( 'mt_primelink_settings', array() );
                        $pause_on_critical  = ! empty( $settings['anomaly_detection_config']['pause_on_critical'] );
                        
                        // Import-specific override (if configured)
                        $import_config = get_post_meta( $import_id, '_mt_primelink_anomaly_detection', true );
                        if ( is_array( $import_config ) && isset( $import_config['config']['pause_on_critical'] ) ) {
                            $pause_on_critical = ! empty( $import_config['config']['pause_on_critical'] );
                        }
                        
                        if ( $pause_on_critical ) {
                            $state['skipped']++;
                            $state['log'][] = self::log_line( sprintf( 
                                'Row %d skipped due to critical anomalies detected (pause on critical is enabled).',
                                $i + 1
                            ) );
                            continue; // Skip to next row
                        }
                    }
                }

                if ( $import_type === 'order' ) {
                    // --- ORDER IMPORT LOGIC ---
                    $order_data = $mapped['order'];
                    $existing_id = OrderImportService::find_existing_order_id( $order_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing order match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        // Track before update
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'order' );
                        OrderImportService::update_order( $existing_id, $order_data, $update_fields, $import_id );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated order #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = OrderImportService::create_order( $order_data, $import_id );
                        ImportTrackingService::track_created( $import_id, $new_id, 'order' );
                        $state['created']++;
                        $state['log'][] = self::log_line( sprintf('Row %d created order #%d.', $i + 1, $new_id ) );
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (order logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'customer' ) {
                    // --- CUSTOMER IMPORT LOGIC ---
                    $customer_data = $mapped['customer'] ?? array();
                    
                    if ( empty($customer_data['email']) && !empty($customer_data['billing_email']) ) {
                        $customer_data['email'] = $customer_data['billing_email'];
                    }
                    
                    // Also check if email is in billing_email but not in email field
                    if ( empty($customer_data['email']) ) {
                        // Try to get from billing_email
                        if ( !empty($customer_data['billing_email']) ) {
                            $customer_data['email'] = $customer_data['billing_email'];
                        }
                    }
                    
                    // If customer data is empty, skip
                    if ( empty($customer_data) || (empty($customer_data['email']) && empty($customer_data['customer_id']) && empty($customer_data['username'])) ) {
                        $state['skipped']++;
                        $debug_info = 'Data keys: ' . implode(', ', array_keys($customer_data));
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no customer data mapped - %s)', $i + 1, $debug_info ) );
                        continue;
                    }
                    $existing_id = CustomerImportService::find_existing_customer_id( $customer_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing customer match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'customer' );
                        CustomerImportService::update_customer( $existing_id, $customer_data, $update_fields );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated customer #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = CustomerImportService::create_customer( $customer_data, $import_id );
                        if ( $new_id > 0 ) {
                            ImportTrackingService::track_created( $import_id, $new_id, 'customer' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created customer #%d.', $i + 1, $new_id ) );
                        } else {
                            $state['errors']++;
                            $email = $customer_data['email'] ?? 'N/A';
                            $username = $customer_data['username'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create customer (Email: %s, Username: %s)', $i + 1, $email, $username ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (customer logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'coupon' ) {
                    // --- COUPON IMPORT LOGIC ---
                    $coupon_data = $mapped['coupon'] ?? array();
                    
                    // If coupon data is empty, skip
                    if ( empty($coupon_data) || empty($coupon_data['code']) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no coupon data mapped)', $i + 1 ) );
                        continue;
                    }
                    $existing_id = CouponImportService::find_existing_coupon_id( $coupon_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing coupon match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'coupon' );
                        CouponImportService::update_coupon( $existing_id, $coupon_data, $update_fields );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated coupon #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = CouponImportService::create_coupon( $coupon_data, $import_id );
                        if ( $new_id > 0 ) {
                            ImportTrackingService::track_created( $import_id, $new_id, 'coupon' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created coupon #%d.', $i + 1, $new_id ) );
                        } else {
                            $state['errors']++;
                            $code = $coupon_data['code'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create coupon (Code: %s)', $i + 1, $code ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (coupon logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'category' ) {
                    // --- CATEGORY IMPORT LOGIC ---
                    $category_data = $mapped['category'] ?? array();
                    
                    // If category data is empty, skip
                    if ( empty($category_data) || empty($category_data['name']) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no category data mapped)', $i + 1 ) );
                        continue;
                    }
                    $existing_id = CategoryImportService::find_existing_category_id( $category_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing category match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'category' );
                        CategoryImportService::update_category( $existing_id, $category_data, $update_fields );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated category #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = CategoryImportService::create_category( $category_data, $import_id );
                        if ( $new_id > 0 ) {
                            ImportTrackingService::track_created( $import_id, $new_id, 'category' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created category #%d.', $i + 1, $new_id ) );
                        } else {
                            $state['errors']++;
                            $name = $category_data['name'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create category (Name: %s)', $i + 1, $name ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (category logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'tag' ) {
                    // --- TAG IMPORT LOGIC ---
                    $tag_data = $mapped['tag'] ?? array();
                    
                    // If tag data is empty, skip
                    if ( empty($tag_data) || empty($tag_data['name']) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no tag data mapped)', $i + 1 ) );
                        continue;
                    }
                    $existing_id = TagImportService::find_existing_tag_id( $tag_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing tag match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'tag' );
                        TagImportService::update_tag( $existing_id, $tag_data );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated tag #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $result = TagImportService::create_tag( $tag_data );
                        if ( $result['success'] && $result['term_id'] > 0 ) {
                            ImportTrackingService::track_created( $import_id, $result['term_id'], 'tag' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created tag #%d.', $i + 1, $result['term_id'] ) );
                        } else {
                            $state['errors']++;
                            $name = $tag_data['name'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create tag (Name: %s)', $i + 1, $name ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (tag logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'attribute' ) {
                    // --- ATTRIBUTE IMPORT LOGIC ---
                    $attribute_data = $mapped['attribute'] ?? array();
                    
                    // If attribute data is empty, skip
                    if ( empty($attribute_data) || empty($attribute_data['name']) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no attribute data mapped)', $i + 1 ) );
                        continue;
                    }
                    $existing_id = AttributeImportService::find_existing_attribute_id( $attribute_data, $match_by, $batch_lookups );
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing attribute match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'attribute' );
                        AttributeImportService::update_attribute( $existing_id, $attribute_data, $update_fields );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated attribute #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = AttributeImportService::create_attribute( $attribute_data, $import_id );
                        if ( $new_id > 0 ) {
                            ImportTrackingService::track_created( $import_id, $new_id, 'attribute' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created attribute #%d.', $i + 1, $new_id ) );
                        } else {
                            $state['errors']++;
                            $name = $attribute_data['name'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create attribute (Name: %s)', $i + 1, $name ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (attribute logic)', $i + 1 ) );
                    }
                } elseif ( $import_type === 'variation' || $import_type === 'variations' ) {
                    // --- VARIATION IMPORT LOGIC ---
                    $variation_data = $mapped['variation'] ?? array();
                    
                    // Parent ID or Parent SKU is required
                    if ( empty( $variation_data['parent_id'] ) && empty( $variation_data['parent_sku'] ) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no parent product specified)', $i + 1 ) );
                        continue;
                    }
                    
                    // Resolve parent ID from SKU if needed
                    if ( empty( $variation_data['parent_id'] ) && ! empty( $variation_data['parent_sku'] ) ) {
                        $parent_product = wc_get_product_id_by_sku( $variation_data['parent_sku'] );
                        if ( $parent_product ) {
                            $variation_data['parent_id'] = $parent_product;
                        } else {
                            $state['skipped']++;
                            $state['log'][] = self::log_line( sprintf('Row %d skipped (parent product not found by SKU: %s)', $i + 1, $variation_data['parent_sku'] ) );
                            continue;
                        }
                    }
                    
                    $existing_id = VariationImportService::find_existing_variation_id( $variation_data, $match_by, $batch_lookups );
                    
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing variation match)', $i + 1 ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'variation' );
                        VariationImportService::update_variation( $existing_id, $variation_data, $update_fields );
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated variation #%d.', $i + 1, $existing_id ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        $new_id = VariationImportService::create_variation( $variation_data, $import_id );
                        if ( $new_id > 0 ) {
                            ImportTrackingService::track_created( $import_id, $new_id, 'variation' );
                            $state['created']++;
                            $state['log'][] = self::log_line( sprintf('Row %d created variation #%d.', $i + 1, $new_id ) );
                        } else {
                            $state['errors']++;
                            $sku = $variation_data['sku'] ?? 'N/A';
                            $state['log'][] = self::log_line( sprintf('Row %d error: Failed to create variation (SKU: %s)', $i + 1, $sku ) );
                        }
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (variation logic)', $i + 1 ) );
                    }
                } else {
                    // --- PRODUCT IMPORT LOGIC ---
                    // Safety check: don't create products if we have customer data but no product data
                    if ( !empty($mapped['customer']) && empty($mapped['post']['post_title']) && empty($mapped['product']['sku']) ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (customer data found but no product data)', $i + 1 ) );
                        continue;
                    }
                    // Advanced duplicate detection
                    $duplicate_config = $draft['step_data'][6]['duplicate_detection'] ?? array();
                    if ( ! empty( $duplicate_config['enabled'] ) && ! empty( $duplicate_config['strategy'] ) ) {
                        $existing_id = AdvancedDuplicateDetectionService::find_existing_product( $mapped, $duplicate_config );
                        // If advanced returns array (multiple matches), take first
                        if ( is_array( $existing_id ) && ! empty( $existing_id ) ) {
                            $existing_id = absint( array_key_first( $existing_id ) );
                        } elseif ( is_array( $existing_id ) ) {
                            $existing_id = 0;
                        }
                    } else {
                        // Fallback to simple matching (with batch lookup optimization)
                        $existing_id = self::find_existing_product_id( $mapped, $match_by, $batch_lookups );
                    }
                    
                    // Incremental import check
                    $incremental_settings = $draft['step_data'][6]['incremental'] ?? array();
                    if ( ! empty( $incremental_settings['enabled'] ) && $existing_id > 0 ) {
                        $should_update = IncrementalImportService::should_update( $existing_id, $mapped, $incremental_settings );
                        if ( ! $should_update['should_update'] ) {
                            if ( ! empty( $incremental_settings['skip_unchanged'] ) ) {
                                // Skip completely (don't log as skipped, just continue)
                                continue;
                            } else {
                                $state['skipped']++;
                                $state['log'][] = self::log_line( sprintf('Row %d skipped (no changes detected) %s.', $i + 1, self::format_product_label( $mapped, $existing_id ) ) );
                                continue;
                            }
                        }
                    }
                    
                    if ( ! $existing_id && $mode === 'update_only' ) {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (no existing match) %s.', $i + 1, self::format_product_label( $mapped, null ) ) );
                    } elseif ( $existing_id && $mode !== 'new_only' ) {
                        // Track before update
                        ImportTrackingService::track_updated( $import_id, $existing_id, 'product' );
                        self::update_product( $existing_id, $mapped, $update_mode, $update_fields, $import_id );
                        // Store hash after update
                        if ( ! empty( $incremental_settings['enabled'] ) ) {
                            $fields = ! empty( $incremental_settings['fields'] ) ? (array) $incremental_settings['fields'] : array();
                            $hash = IncrementalImportService::generate_hash( $mapped, $fields );
                            IncrementalImportService::store_hash( $existing_id, $hash, $import_id );
                        }
                        $state['updated']++;
                        $state['log'][] = self::log_line( sprintf('Row %d updated %s.', $i + 1, self::format_product_label( $mapped, $existing_id ) ) );
                    } elseif ( ! $existing_id && $mode !== 'update_only' ) {
                        // CRITICAL: When creating a new product, always use ALL mapped fields
                        // regardless of update_mode. This ensures new products get all data
                        // (description, images, etc.) even if update_mode is 'choose' or 'price_stock'.
                        $new_id = self::create_product( $mapped, $import_id, true );
                        ImportTrackingService::track_created( $import_id, $new_id, 'product' );
                        // Store hash after create
                        if ( ! empty( $incremental_settings['enabled'] ) ) {
                            $fields = ! empty( $incremental_settings['fields'] ) ? (array) $incremental_settings['fields'] : array();
                            $hash = IncrementalImportService::generate_hash( $mapped, $fields );
                            IncrementalImportService::store_hash( $new_id, $hash, $import_id );
                        }
                        $state['created']++;
                        $state['log'][] = self::log_line( sprintf('Row %d created %s.', $i + 1, self::format_product_label( $mapped, $new_id ) ) );
                    } else {
                        $state['skipped']++;
                        $state['log'][] = self::log_line( sprintf('Row %d skipped (product logic)', $i + 1 ) );
                    }
                }
            } catch ( \Throwable $e ) {
                $state['errors']++;
                if ( $import_type === 'order' ) {
                    $state['log'][] = self::log_line( sprintf( 'Row %d error: %s', $i + 1, $e->getMessage() ) );
                } else {
                    $state['log'][] = self::log_line( sprintf( 'Row %d error: %s', $i + 1, $e->getMessage() ) );
                }
            }

            // Update progress after each row for fluid UI updates
            $state['current'] = $i + 1;
            $state['progress'] = $total > 0 ? round( ( $state['current'] / $total ) * 100 ) : 0;
            
            // Save state every 5 rows for fluid progress updates (instead of only at batch end)
            // This ensures frontend polling sees progress updates more frequently
            if ( ( $i + 1 ) % 5 === 0 || $i + 1 === $end ) {
                self::save_state( $import_id, $state );
            }
        }

        // Disable Fast Mode if it was enabled (before final progress update)
        if ( $fast_mode_enabled ) {
            ImportPerformanceHelper::disable_fast_mode();
        }
        
        // Safety net: if we processed rows but "current" somehow stayed 0,
        // force it to total so UI shows X/X instead of 0/X when everything was skipped.
        if ( $total > 0 && (int) $state['current'] < 1 ) {
            $state['current'] = $total;
        }
        
        // Final progress update (in case batch ended without hitting the modulo check)
        $state['progress'] = $total > 0 ? round( ( $state['current'] / $total ) * 100 ) : 0;
        
        if ( $state['current'] >= $total ) {
            $state['status']     = 'finished';
            $state['finished']   = true;
            $state['finished_at'] = current_time( 'mysql' ); // For statistics compatibility
            
            // Check if there are anomalies to review
            $anomalies_data = get_post_meta( $import_id, '_mt_primelink_anomalies', true );
            $anomalies_count = is_array( $anomalies_data ) ? count( $anomalies_data ) : 0;
            if ( $anomalies_count > 0 ) {
                $state['has_anomalies'] = true;
                $state['anomalies_count'] = $anomalies_count;
                $state['log'][] = self::log_line( sprintf(
                    'Finished. Created: %d, Updated: %d, Skipped: %d, Errors: %d. %d items with anomalies detected - review available.',
                    $state['created'], $state['updated'], $state['skipped'], $state['errors'], $anomalies_count
                ) );
            } else {
                $state['log'][] = self::log_line( sprintf(
                    'Finished. Created: %d, Updated: %d, Skipped: %d, Errors: %d',
                    $state['created'], $state['updated'], $state['skipped'], $state['errors']
                ) );
            }
            self::save_state( $import_id, $state );

            // Get notification settings
            $settings = get_option( 'mt_primelink_settings', array() );
            $notify_email = isset( $settings['notify_email'] ) ? sanitize_email( $settings['notify_email'] ) : '';
            $notify_finish = ! empty( $settings['notify_finish'] );
            $notify_error = ! empty( $settings['notify_error'] );
            $tpl_finish = isset( $settings['notify_template_finish'] ) ? $settings['notify_template_finish'] : '';
            $tpl_error = isset( $settings['notify_template_error'] ) ? $settings['notify_template_error'] : '';
            $builder_blocks = isset( $settings['notify_builder_data'] ) ? json_decode( $settings['notify_builder_data'], true ) : null;
            $notify_phone = isset( $settings['notify_phone'] ) ? $settings['notify_phone'] : '';
            $notify_sms = ! empty( $settings['notify_sms'] );
            $notify_whatsapp = ! empty( $settings['notify_whatsapp'] );

            // Notify if enabled
            if ( $notify_finish || ( $notify_error && $state['errors'] > 0 ) ) {
                $subject = sprintf( 'PrimeLink import #%d finished', $import_id );
                $tpl     = ( $state['errors'] > 0 && $notify_error ) ? $tpl_error : $tpl_finish;
                $body_text = strtr( $tpl, array(
                    '{import_id}' => $import_id,
                    '{status}'    => $state['status'],
                    '{created}'   => $state['created'],
                    '{updated}'   => $state['updated'],
                    '{skipped}'   => $state['skipped'],
                    '{errors}'    => $state['errors'],
                ));
                $body_html = $builder_blocks ? self::render_email_builder( $builder_blocks, array(
                    'import_id' => $import_id,
                    'status'    => $state['status'],
                    'created'   => $state['created'],
                    'updated'   => $state['updated'],
                    'skipped'   => $state['skipped'],
                    'errors'    => $state['errors'],
                ) ) : nl2br( esc_html( $body_text ) );
                $headers = array( 'Content-Type: text/html; charset=UTF-8' );

                if ( $notify_email ) {
                    wp_mail( $notify_email, $subject, $body_html, $headers );
                }
                if ( $notify_phone && ( $notify_sms || $notify_whatsapp ) ) {
                    /**
                     * Hook for SMS/WhatsApp providers.
                     */
                    do_action( 'primelink_notify_phone', $notify_phone, $body_text, $state, $import_id, array(
                        'sms'      => $notify_sms,
                        'whatsapp' => $notify_whatsapp,
                    ) );
                }
            }

            // Update next_run for scheduled imports (especially important for external cron)
            $drafts = get_option( 'mt_primelink_import_drafts', array() );
            if ( isset( $drafts[ $import_id ] ) ) {
                $schedule = $drafts[ $import_id ]['step_data'][6]['schedule'] ?? array();
                $enabled = ! empty( $schedule['enabled'] );
                if ( $enabled ) {
                    // Update next_run when import finishes to ensure proper scheduling
                    $drafts[ $import_id ]['step_data'][6]['schedule'] = self::bump_next_run( $schedule );
                    update_option( 'mt_primelink_import_drafts', $drafts, false );
                }
            }

            // Disable Fast Mode before return
            if ( $fast_mode_enabled ) {
                ImportPerformanceHelper::disable_fast_mode();
            }
            return;
        }

        } catch ( \Throwable $e ) {
            // Handle any unexpected errors
            $state['status'] = 'error';
            $state['errors']++;
            $state['log'][] = self::log_line( 'Unexpected error: ' . $e->getMessage() );
            self::save_state( $import_id, $state );
            return;
        } finally {
            // Ensure Fast Mode is disabled in all cases
            if ( $fast_mode_enabled ) {
                ImportPerformanceHelper::disable_fast_mode();
            }
        }

        // Final save and scheduling (outside try-catch)
        self::save_state( $import_id, $state );

        // Schedule next tick only if not in external cron mode
        $settings = self::get_settings();
        if ( ( $settings['cron_mode'] ?? 'wp' ) !== 'external' ) {
            $current_state = get_option( self::state_key( $import_id ), array() );
            if ( ( $current_state['status'] ?? '' ) === 'running' ) {
                if ( ! wp_next_scheduled( 'mt_primelink_import_tick', array( $import_id ) ) ) {
                    wp_schedule_single_event( time() + 1, 'mt_primelink_import_tick', array( $import_id ) );
                }
            }
        }
    }

    /* ============================================================
     * Core helpers
     * ============================================================
     */

    private static function normalize_mode( $raw ) {
        switch ( $raw ) {
            case 'new_only':
            case 'import_new_only':
                return 'new_only';
            case 'update_only':
            case 'update_existing_only':
                return 'update_only';
            default:
                return 'new_update'; // new + update
        }
    }

    public static function row_to_item( $headers, $row ) {
        $item = array();
        foreach ( $headers as $idx => $h ) {
            $key = is_string( $h ) ? $h : (string) $idx;
            $item[ $key ] = $row[ $idx ] ?? '';
        }
        return $item;
    }

    private static function format_product_label( $mapped, $product_id = null ) {
        // Prefer mapped post title (nested under 'post').
        $title = '';
        if ( ! empty( $mapped['post']['post_title'] ) ) {
            $title = trim( (string) $mapped['post']['post_title'] );
        } elseif ( ! empty( $mapped['product']['name'] ) ) {
            // Fallback if someone mapped a custom name field into product bucket.
            $title = trim( (string) $mapped['product']['name'] );
        }

        $slug = '';
        if ( $title === '' && ! empty( $mapped['post']['post_name'] ) ) {
            $slug = trim( (string) $mapped['post']['post_name'] );
        }

        $sku = '';
        if ( ! empty( $mapped['product']['sku'] ) ) {
            $sku = trim( (string) $mapped['product']['sku'] );
        }

        $parts = array();
        if ( $title !== '' ) {
            $parts[] = '"' . $title . '"';
        } elseif ( $slug !== '' ) {
            $parts[] = 'slug ' . $slug;
        }
        if ( $sku !== '' ) {
            $parts[] = 'SKU ' . $sku;
        }
        if ( $product_id ) {
            $parts[] = '#' . (int) $product_id;
        }

        if ( empty( $parts ) ) {
            return $product_id ? 'product #' . (int) $product_id : 'product';
        }

        return implode( ' / ', $parts );
    }

    private static function log_line( $message ) {
        $ts = current_time( 'mysql' );
        return sprintf( '[%s] %s', $ts, $message );
    }

    /**
     * Flatten WooCommerce data to row format for processing.
     *
     * @param array $woo_data WooCommerce-formatted data.
     * @return array
     */
    public static function flatten_woo_data_to_row( $woo_data ) {
        $row = array();
        
        // Flatten post data
        if ( ! empty( $woo_data['post'] ) ) {
            foreach ( $woo_data['post'] as $key => $value ) {
                $row[ 'post_' . $key ] = $value;
            }
        }
        
        // Flatten product/order/customer data
        foreach ( array( 'product', 'order', 'customer', 'coupon' ) as $type ) {
            if ( ! empty( $woo_data[ $type ] ) ) {
                foreach ( $woo_data[ $type ] as $key => $value ) {
                    $row[ $type . '_' . $key ] = $value;
                }
            }
        }
        
        // Flatten images
        if ( ! empty( $woo_data['images']['images'] ) ) {
            $row['images'] = is_array( $woo_data['images']['images'] ) 
                ? implode( ',', $woo_data['images']['images'] ) 
                : $woo_data['images']['images'];
        }
        
        // Flatten taxonomies
        if ( ! empty( $woo_data['tax'] ) ) {
            foreach ( $woo_data['tax'] as $tax => $terms ) {
                $row[ 'tax_' . $tax ] = is_array( $terms ) ? implode( ',', $terms ) : $terms;
            }
        }
        
        // Flatten attributes
        if ( ! empty( $woo_data['attrs'] ) ) {
            foreach ( $woo_data['attrs'] as $key => $value ) {
                $row[ 'attr_' . $key ] = is_array( $value ) ? wp_json_encode( $value ) : $value;
            }
        }
        
        return $row;
    }

    public static function extract_file_from_draft( $draft ) {
    $s2 = $draft['step_data'][2] ?? array();

    // Case A: saved by handle_source -> step_data[2]['file'] = array(...)
    if ( is_array( $s2 ) && ! empty( $s2['file'] ) && is_array( $s2['file'] ) ) {
        return $s2['file'];
    }

    // Case B: step_data[2]['data']['file']
    if ( is_array( $s2 ) && ! empty( $s2['data']['file'] ) && is_array( $s2['data']['file'] ) ) {
        return $s2['data']['file'];
    }

    // Case C: step_data[2] itself is file array
    if ( is_array( $s2 ) && isset( $s2['path'] ) ) {
        return $s2;
    }

    // Nothing usable
    return array();
}


    public static function apply_mapping_to_item( $item, $mapping, $custom_fields, $import_type = 'product' ) {
        $out = array(
            'post'    => array(),
            'product' => array(),
            'order'   => array(), // ensure order bucket exists
            'customer' => array(), // ensure customer bucket exists
            'coupon'  => array(), // ensure coupon bucket exists
            'category' => array(), // ensure category bucket exists
            'tag'     => array(), // ensure tag bucket exists
            'attribute' => array(), // ensure attribute bucket exists
            'variation' => array(),
            'tax'     => array(),
            'images'  => array(),
            'attrs'   => array(),
            'yoast'   => array(),
            'meta'    => array(),
            'linked'  => array(),
        );

        // Standard mapping targets => value
        foreach ( (array) $mapping as $target => $cfg ) {
            if ( is_string( $cfg ) ) {
                $cfg = array( 'source' => $cfg );
            }
            $value = ImportMapper::compute_mapped_value( $item, (array) $cfg );

            // Route target to buckets:
            $known_tax = array(
    'product_cat',
    'product_tag',
    'product_brand',
    'product_type',
);

if ( strpos( $target, 'tax:' ) === 0 ) {
    $tax_name = substr( $target, 4 );
    $out['tax'][ $tax_name ] = $value;

} elseif ( in_array( $target, $known_tax, true ) ) {
    // Accept plain taxonomy targets too (saved by older UI / different prefix).
    $out['tax'][ $target ] = $value;
            } elseif ( strpos( $target, 'attribute:' ) === 0 ) {
                $attr_name = substr( $target, 10 );
                $out['attrs'][ $attr_name ] = array(
                    'values'    => $value,
                    'variation' => ! empty( $cfg['variation'] ),
                    'visible'   => array_key_exists( 'visible', (array) $cfg ) ? (bool) $cfg['visible'] : true,
                );
            } elseif ( strpos( $target, 'yoast_' ) === 0 ) {
                $out['yoast'][ $target ] = $value;
            } elseif ( $target === 'featured_image' ) {
                $out['images'][ $target ] = $value;
            } elseif ( $target === 'images' || strpos( $target, 'images_' ) === 0 ) {
                // Support multiple gallery slots: images, images_1, images_2, etc.
                if ( ! isset( $out['images']['images'] ) ) {
                    $out['images']['images'] = array();
                }
                // If value is array, merge it; otherwise add to array
                if ( is_array( $value ) ) {
                    $out['images']['images'] = array_merge( $out['images']['images'], $value );
                } else {
                    $list = ImportMapper::normalize_list( $value );
                    $out['images']['images'] = array_merge( $out['images']['images'], $list );
                }
            } elseif ( in_array( $target, array( 'post_title', 'post_content', 'post_excerpt', 'post_name', 'post_status' ), true ) ) {
                $out['post'][ $target ] = $value;
            } elseif ( in_array( $target, array( 'variation_regular_price', 'variation_sale_price', 'variation_content', 'variation_excerpt' ), true ) ) {
                $out['variation'][ $target ] = $value;
            } elseif ( in_array( $target, array( 'upsell_refs', 'cross_sell_refs', 'grouped_children' ), true ) ) {
                $out['linked'][ $target ] = $value;
            } elseif ( strpos( $target, 'order_' ) === 0 ) {
                // For orders, map fields like order_id, customer_email, etc.
                $order_field = substr( $target, 6 );
                $out['order'][ $order_field ] = $value;
            } elseif ( ($import_type === 'coupon' || $import_type === 'coupons') && in_array( $target, array( 'code', 'discount_type', 'amount', 'description', 'date_expires', 'usage_limit', 'usage_limit_per_user', 'minimum_amount', 'maximum_amount', 'individual_use', 'exclude_sale_items', 'free_shipping', 'product_ids', 'excluded_product_ids', 'product_categories', 'excluded_product_categories', 'customer_emails', 'limit_usage_to_x_items', 'coupon_id' ), true ) ) {
                // Coupon-specific fields - CHECK THESE BEFORE other fields!
                $out['coupon'][ $target ] = $value;
            } elseif ( ($import_type === 'customer' || $import_type === 'customers') && in_array( $target, array( 'email', 'username', 'password', 'first_name', 'last_name', 'display_name', 'role', 'user_registered', 'customer_id', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_email', 'billing_phone', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country' ), true ) ) {
                // Customer-specific fields - CHECK THESE BEFORE order fields!
                // If billing_email is mapped but email is not, use billing_email as email
                if ( $target === 'billing_email' && empty($out['customer']['email']) ) {
                    $out['customer']['email'] = $value;
                }
                $out['customer'][ $target ] = $value;
            } elseif ( ($import_type === 'category' || $import_type === 'categories') && in_array( $target, array( 'name', 'slug', 'description', 'parent', 'thumbnail', 'display_type', 'category_id' ), true ) ) {
                // Category-specific fields
                $out['category'][ $target ] = $value;
            } elseif ( ($import_type === 'tag' || $import_type === 'tags') && in_array( $target, array( 'name', 'slug', 'description', 'thumbnail', 'tag_id' ), true ) ) {
                // Tag-specific fields
                $out['tag'][ $target ] = $value;
            } elseif ( ($import_type === 'attribute' || $import_type === 'attributes') && in_array( $target, array( 'name', 'slug', 'type', 'order_by', 'has_archives', 'terms', 'attribute_id' ), true ) ) {
                // Attribute-specific fields
                if ( $target === 'terms' && is_string( $value ) ) {
                    // Try to parse as JSON first, otherwise treat as comma-separated
                    $decoded = json_decode( $value, true );
                    if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
                        $out['attribute']['terms'] = $decoded;
                    } else {
                        // Comma-separated term names
                        $term_names = array_map( 'trim', explode( ',', $value ) );
                        $out['attribute']['terms'] = array_map( function( $name ) {
                            return array( 'name' => $name );
                        }, array_filter( $term_names ) );
                    }
                } else {
                    $out['attribute'][ $target ] = $value;
                }
            } elseif ( ($import_type === 'variation' || $import_type === 'variations') && in_array( $target, array( 'variation_id', 'parent_id', 'parent_sku', 'sku', 'regular_price', 'sale_price', 'stock_quantity', 'stock_status', 'manage_stock', 'backorders', 'weight', 'length', 'width', 'height', 'image', 'gallery', 'description', 'short_description', 'attributes', 'tax_class', 'status' ), true ) ) {
                // Variation-specific fields
                if ( $target === 'attributes' && is_string( $value ) ) {
                    // Try to parse as JSON first
                    $decoded = json_decode( $value, true );
                    if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
                        $out['variation']['attributes'] = $decoded;
                    } else {
                        // If not JSON, store as-is (will be handled later)
                        $out['variation']['attributes'] = $value;
                    }
                } elseif ( $target === 'parent_sku' ) {
                    // Store parent_sku separately, will be resolved to parent_id later
                    $out['variation']['parent_sku'] = $value;
                } else {
                    $out['variation'][ $target ] = $value;
                }
            } elseif ( ($import_type === 'variation' || $import_type === 'variations') && strpos( $target, 'attribute:' ) === 0 ) {
                // Variation attribute mapping (e.g., attribute:pa_color)
                $attr_name = substr( $target, 10 );
                if ( ! isset( $out['variation']['attributes'] ) || ! is_array( $out['variation']['attributes'] ) ) {
                    $out['variation']['attributes'] = array();
                }
                $out['variation']['attributes'][ $attr_name ] = $value;
            } elseif ( ($import_type === 'review' || $import_type === 'reviews') && in_array( $target, array( 'review_id', 'product_id', 'product_sku', 'author_name', 'author_email', 'author_url', 'rating', 'title', 'content', 'status', 'verified_purchase', 'helpful_votes', 'parent_id', 'date_created', 'date_modified' ), true ) ) {
                // Review-specific fields
                if ( $target === 'product_sku' ) {
                    // Store product_sku separately, will be resolved to product_id later
                    $out['review']['product_sku'] = $value;
                } else {
                    $out['review'][ $target ] = $value;
                }
            } elseif ( ($import_type === 'download' || $import_type === 'downloads') && in_array( $target, array( 'product_id', 'product_sku', 'file_url', 'file_name', 'download_id', 'download_limit', 'download_expiry' ), true ) ) {
                // Download-specific fields
                if ( $target === 'product_sku' ) {
                    // Store product_sku separately, will be resolved to product_id later
                    $out['download']['product_sku'] = $value;
                } else {
                    $out['download'][ $target ] = $value;
                }
            } elseif ( in_array( $target, array( 'status', 'customer_email', 'customer_id', 'customer_name', 'customer_phone', 'order_id', 'order_key', 'order_date', 'order_total', 'total', 'currency', 'created_via', 'cart_hash', 'parent_id', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_email', 'billing_phone', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country', 'payment_method', 'payment_method_title', 'transaction_id', 'date_paid', 'date_completed', 'customer_note', 'admin_note', 'order_notes', 'items', 'item_sku', 'item_name', 'item_qty', 'item_total', 'item_subtotal', 'item_tax', 'item_discount', 'item_meta' ), true ) ) {
                // Order-specific fields without order_ prefix
                $out['order'][ $target ] = $value;
            } elseif ( in_array( $target, array( 'created_date', 'creation_date', 'date_created', 'created_at' ), true ) ) {
                // Alternative names for order_date
                $out['order']['order_date'] = $value;
            } elseif ( in_array( $target, array( 'completion_date', 'completed_date', 'date_completed', 'completed_at' ), true ) ) {
                // Alternative names for date_completed
                $out['order']['date_completed'] = $value;
            } elseif ( in_array( $target, array( 'paid_date', 'payment_date', 'date_paid', 'paid_at' ), true ) ) {
                // Alternative names for date_paid
                $out['order']['date_paid'] = $value;
            } else {
                $out['product'][ $target ] = $value;
            }
        }

        // Custom fields
        foreach ( (array) $custom_fields as $cf ) {
            $key = sanitize_key( $cf['key'] ?? '' );
            if ( ! $key ) {
                continue;
            }
            $value = ImportMapper::compute_mapped_value( $item, (array) $cf );
            if ( $import_type === 'customer' ) {
                // For customers, add custom fields to customer meta
                if ( !isset($out['customer']['meta']) ) {
                    $out['customer']['meta'] = array();
                }
                $out['customer']['meta'][ $key ] = $value;
            } else {
                $out['meta'][ $key ] = $value;
            }
        }

        return $out;
    }

    /**
     * Prepare batch lookups for existing items to reduce database queries
     * 
     * @param array $rows Rows to process
     * @param array $headers Headers
     * @param array $mapping Mapping configuration
     * @param array $custom_fields Custom fields
     * @param string $import_type Import type
     * @param string $match_by Match by field
     * @param int $start Start index
     * @param int $end End index
     * @return array Batch lookup maps
     */
    private static function prepare_batch_lookups( $rows, $headers, $mapping, $custom_fields, $import_type, $match_by, $start, $end ) {
        $lookups = array(
            'products_by_sku' => array(),
            'products_by_title' => array(),
            'users_by_email' => array(),
            'coupons_by_code' => array(),
            'categories_by_slug' => array(),
            'categories_by_name' => array(),
            'tags_by_slug' => array(),
            'tags_by_name' => array(),
            'attributes_by_slug' => array(),
            'attributes_by_name' => array(),
            'variations_by_sku' => array(),
            'variations_by_id' => array(),
            'reviews_by_product_author' => array(),
            'downloads_by_product_file_name' => array(),
            'downloads_by_product_file_url' => array(),
        );
        
        // Only prepare batch lookups for the current batch
        $batch_rows = array_slice( $rows, $start, $end - $start );
        
        foreach ( $batch_rows as $i => $row ) {
            $item = self::row_to_item( $headers, $row );
            $mapped = self::apply_mapping_to_item( $item, $mapping, $custom_fields, $import_type );
            
            if ( $import_type === 'product' ) {
                if ( $match_by === 'sku' && ! empty( $mapped['product']['sku'] ) ) {
                    $lookups['products_by_sku'][] = (string) $mapped['product']['sku'];
                } elseif ( ! empty( $mapped['post']['post_title'] ) ) {
                    $lookups['products_by_title'][] = (string) $mapped['post']['post_title'];
                }
            } elseif ( $import_type === 'customer' ) {
                $email = $mapped['customer']['email'] ?? $mapped['customer']['billing_email'] ?? '';
                if ( ! empty( $email ) ) {
                    // Normalize to lowercase for batch lookup (matching batch_lookup_users_by_email behavior)
                    $lookups['users_by_email'][] = strtolower( sanitize_email( $email ) );
                }
            } elseif ( $import_type === 'coupon' ) {
                if ( ! empty( $mapped['coupon']['code'] ) ) {
                    $lookups['coupons_by_code'][] = sanitize_text_field( $mapped['coupon']['code'] );
                }
            } elseif ( $import_type === 'category' ) {
                if ( $match_by === 'slug' && ! empty( $mapped['category']['slug'] ) ) {
                    $lookups['categories_by_slug'][] = sanitize_title( $mapped['category']['slug'] );
                } elseif ( ! empty( $mapped['category']['name'] ) ) {
                    $lookups['categories_by_name'][] = sanitize_text_field( $mapped['category']['name'] );
                }
            } elseif ( $import_type === 'tag' ) {
                if ( $match_by === 'slug' && ! empty( $mapped['tag']['slug'] ) ) {
                    $lookups['tags_by_slug'][] = sanitize_title( $mapped['tag']['slug'] );
                } elseif ( ! empty( $mapped['tag']['name'] ) ) {
                    $lookups['tags_by_name'][] = sanitize_text_field( $mapped['tag']['name'] );
                }
            } elseif ( $import_type === 'attribute' ) {
                if ( $match_by === 'slug' && ! empty( $mapped['attribute']['slug'] ) ) {
                    $lookups['attributes_by_slug'][] = sanitize_title( $mapped['attribute']['slug'] );
                } elseif ( ! empty( $mapped['attribute']['name'] ) ) {
                    $lookups['attributes_by_name'][] = sanitize_text_field( $mapped['attribute']['name'] );
                }
            } elseif ( $import_type === 'variation' || $import_type === 'variations' ) {
                if ( $match_by === 'sku' && ! empty( $mapped['variation']['sku'] ) ) {
                    $lookups['variations_by_sku'][] = sanitize_text_field( $mapped['variation']['sku'] );
                } elseif ( $match_by === 'variation_id' && ! empty( $mapped['variation']['variation_id'] ) ) {
                    $lookups['variations_by_id'][] = absint( $mapped['variation']['variation_id'] );
                }
            } elseif ( $import_type === 'review' || $import_type === 'reviews' ) {
                if ( $match_by === 'product_author' && ! empty( $mapped['review']['product_id'] ) && ! empty( $mapped['review']['author_email'] ) ) {
                    $product_id = absint( $mapped['review']['product_id'] );
                    $author_email = strtolower( sanitize_email( $mapped['review']['author_email'] ) );
                    if ( ! isset( $lookups['reviews_by_product_author'][ $product_id ] ) ) {
                        $lookups['reviews_by_product_author'][ $product_id ] = array();
                    }
                    $lookups['reviews_by_product_author'][ $product_id ][] = $author_email;
                }
            } elseif ( $import_type === 'download' || $import_type === 'downloads' ) {
                if ( $match_by === 'file_name' && ! empty( $mapped['download']['product_id'] ) && ! empty( $mapped['download']['file_name'] ) ) {
                    $product_id = absint( $mapped['download']['product_id'] );
                    $file_name = sanitize_text_field( $mapped['download']['file_name'] );
                    if ( ! isset( $lookups['downloads_by_product_file_name'][ $product_id ] ) ) {
                        $lookups['downloads_by_product_file_name'][ $product_id ] = array();
                    }
                    $lookups['downloads_by_product_file_name'][ $product_id ][] = $file_name;
                } elseif ( $match_by === 'file_url' && ! empty( $mapped['download']['product_id'] ) && ! empty( $mapped['download']['file_url'] ) ) {
                    $product_id = absint( $mapped['download']['product_id'] );
                    $file_url = esc_url_raw( $mapped['download']['file_url'] );
                    if ( ! isset( $lookups['downloads_by_product_file_url'][ $product_id ] ) ) {
                        $lookups['downloads_by_product_file_url'][ $product_id ] = array();
                    }
                    $lookups['downloads_by_product_file_url'][ $product_id ][] = $file_url;
                }
            }
        }
        
        // Execute batch lookups
        $results = array();
        
        if ( ! empty( $lookups['products_by_sku'] ) ) {
            $results['products_by_sku'] = \PrimeLink\Helpers\DatabaseOptimizationHelper::batch_lookup_products_by_sku(
                array_unique( $lookups['products_by_sku'] )
            );
        }
        
        if ( ! empty( $lookups['products_by_title'] ) ) {
            $results['products_by_title'] = \PrimeLink\Helpers\DatabaseOptimizationHelper::batch_lookup_products_by_title(
                array_unique( $lookups['products_by_title'] )
            );
        }
        
        if ( ! empty( $lookups['users_by_email'] ) ) {
            $results['users_by_email'] = \PrimeLink\Helpers\DatabaseOptimizationHelper::batch_lookup_users_by_email(
                array_unique( $lookups['users_by_email'] )
            );
        }
        
        if ( ! empty( $lookups['coupons_by_code'] ) ) {
            $results['coupons_by_code'] = \PrimeLink\Helpers\DatabaseOptimizationHelper::batch_lookup_coupons_by_code(
                array_unique( $lookups['coupons_by_code'] )
            );
        }
        
        if ( ! empty( $lookups['categories_by_slug'] ) ) {
            $results['categories_by_slug'] = self::batch_lookup_categories_by_slug(
                array_unique( $lookups['categories_by_slug'] )
            );
        }
        
        if ( ! empty( $lookups['categories_by_name'] ) ) {
            $results['categories_by_name'] = self::batch_lookup_categories_by_name(
                array_unique( $lookups['categories_by_name'] )
            );
        }
        
        if ( ! empty( $lookups['attributes_by_slug'] ) || ! empty( $lookups['attributes_by_name'] ) ) {
            $attr_lookups = AttributeImportService::prepare_batch_lookups( $rows, $headers, $mapping, $match_by );
            if ( isset( $attr_lookups['attributes_by_slug'] ) ) {
                $results['attributes_by_slug'] = $attr_lookups['attributes_by_slug'];
            }
            if ( isset( $attr_lookups['attributes_by_name'] ) ) {
                $results['attributes_by_name'] = $attr_lookups['attributes_by_name'];
            }
        }
        
        if ( ! empty( $lookups['variations_by_sku'] ) || ! empty( $lookups['variations_by_id'] ) ) {
            $var_lookups = VariationImportService::prepare_batch_lookups( $rows, $headers, $mapping, $match_by );
            if ( isset( $var_lookups['variations_by_sku'] ) ) {
                $results['variations_by_sku'] = $var_lookups['variations_by_sku'];
            }
            if ( isset( $var_lookups['variations_by_id'] ) ) {
                $results['variations_by_id'] = $var_lookups['variations_by_id'];
            }
        }
        
        if ( ! empty( $lookups['reviews_by_product_author'] ) ) {
            $review_lookups = ReviewImportService::prepare_batch_lookups( $rows, $headers, $mapping, $match_by );
            if ( isset( $review_lookups['reviews_by_product_author'] ) ) {
                $results['reviews_by_product_author'] = $review_lookups['reviews_by_product_author'];
            }
        }
        
        if ( ! empty( $lookups['downloads_by_product_file_name'] ) || ! empty( $lookups['downloads_by_product_file_url'] ) ) {
            $download_lookups = DownloadImportService::prepare_batch_lookups( $rows, $headers, $mapping, $match_by );
            if ( isset( $download_lookups['downloads_by_product_file_name'] ) ) {
                $results['downloads_by_product_file_name'] = $download_lookups['downloads_by_product_file_name'];
            }
            if ( isset( $download_lookups['downloads_by_product_file_url'] ) ) {
                $results['downloads_by_product_file_url'] = $download_lookups['downloads_by_product_file_url'];
            }
        }
        
        return $results;
    }
    
    /**
     * Batch lookup categories by slug
     */
    private static function batch_lookup_categories_by_slug( $slugs ) {
        global $wpdb;
        $slugs = array_map( 'sanitize_title', $slugs );
        $placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) );
        $query = $wpdb->prepare(
            "SELECT t.term_id, t.slug FROM {$wpdb->terms} t
            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
            WHERE tt.taxonomy = 'product_cat' AND t.slug IN ($placeholders)",
            $slugs
        );
        $results = $wpdb->get_results( $query, OBJECT_K );
        $map = array();
        if ( $results ) {
            foreach ( $results as $term ) {
                $map[ $term->slug ] = $term->term_id;
            }
        }
        return $map;
    }
    
    /**
     * Batch lookup categories by name
     */
    private static function batch_lookup_categories_by_name( $names ) {
        global $wpdb;
        $names = array_map( 'sanitize_text_field', $names );
        $placeholders = implode( ',', array_fill( 0, count( $names ), '%s' ) );
        $query = $wpdb->prepare(
            "SELECT t.term_id, t.name FROM {$wpdb->terms} t
            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
            WHERE tt.taxonomy = 'product_cat' AND t.name IN ($placeholders)",
            $names
        );
        $results = $wpdb->get_results( $query, OBJECT_K );
        
        $mapped = array();
        foreach ( $results as $row ) {
            $mapped[ $row->name ] = absint( $row->term_id );
        }
        return $mapped;
    }
    
    /**
     * Batch lookup tags by slug
     */
    private static function batch_lookup_tags_by_slug( $slugs ) {
        global $wpdb;
        $slugs = array_map( 'sanitize_title', $slugs );
        $placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) );
        $query = $wpdb->prepare(
            "SELECT t.term_id, t.slug FROM {$wpdb->terms} t
            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
            WHERE tt.taxonomy = 'product_tag' AND t.slug IN ($placeholders)",
            $slugs
        );
        $results = $wpdb->get_results( $query, OBJECT_K );
        $map = array();
        if ( $results ) {
            foreach ( $results as $term ) {
                $map[ $term->slug ] = $term->term_id;
            }
        }
        return $map;
    }
    
    /**
     * Batch lookup tags by name
     */
    private static function batch_lookup_tags_by_name( $names ) {
        global $wpdb;
        $names = array_map( 'sanitize_text_field', $names );
        $placeholders = implode( ',', array_fill( 0, count( $names ), '%s' ) );
        $query = $wpdb->prepare(
            "SELECT t.term_id, t.name FROM {$wpdb->terms} t
            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
            WHERE tt.taxonomy = 'product_tag' AND t.name IN ($placeholders)",
            $names
        );
        $results = $wpdb->get_results( $query, OBJECT_K );
        $map = array();
        if ( $results ) {
            foreach ( $results as $term ) {
                $map[ $term->name ] = $term->term_id;
            }
        }
        return $map;
    }
    
    public static function find_existing_product_id( $mapped, $match_by, $batch_lookups = null ) {
        $match_by = (string) $match_by;

        // SKU is the best match for WC.
        if ( $match_by === 'sku' && ! empty( $mapped['product']['sku'] ) ) {
            $sku = (string) $mapped['product']['sku'];
            
            // Use batch lookup if available
            if ( $batch_lookups && isset( $batch_lookups['products_by_sku'][ $sku ] ) ) {
                return absint( $batch_lookups['products_by_sku'][ $sku ] );
            }
            
            // Fallback to individual lookup
            $id = wc_get_product_id_by_sku( $sku );
            return $id ? absint( $id ) : 0;
        }

        // Post ID direct
        if ( $match_by === 'post_id' && ! empty( $mapped['product']['post_id'] ) ) {
            $id = absint( $mapped['product']['post_id'] );
            return get_post( $id ) ? $id : 0;
        }

        // Slug
        if ( $match_by === 'slug' && ! empty( $mapped['post']['post_name'] ) ) {
            $slug = sanitize_title( $mapped['post']['post_name'] );
            $p = get_page_by_path( $slug, OBJECT, 'product' );
            return $p ? $p->ID : 0;
        }

        // Title fallback
        if ( ! empty( $mapped['post']['post_title'] ) ) {
            $title = (string) $mapped['post']['post_title'];
            
            // Use batch lookup if available
            if ( $batch_lookups && isset( $batch_lookups['products_by_title'][ $title ] ) ) {
                return absint( $batch_lookups['products_by_title'][ $title ] );
            }
            
            // Fallback to individual lookup
            $q = new \WP_Query( array(
                'post_type'      => 'product',
                'post_status'    => array( 'publish', 'draft', 'pending', 'private' ),
                'title'          => $title,
                'posts_per_page' => 1,
                'fields'         => 'ids',
            ) );
            if ( ! empty( $q->posts[0] ) ) {
                return absint( $q->posts[0] );
            }
        }

        return 0;
    }

    /**
     * Create a new product.
     * 
     * @param array $mapped Mapped product data.
     * @param int   $import_id Import ID.
     * @param bool  $force_all_fields If true, always use 'all' mode regardless of update_mode.
     *                                This ensures new products get all mapped fields (description, images, etc.)
     *                                even when update_mode is 'choose' or 'price_stock'.
     * @return int Product ID.
     */
    private static function create_product( $mapped, $import_id = 0, $force_all_fields = false ) {
        $post_data = array(
            'post_type'    => 'product',
            'post_title'   => $mapped['post']['post_title'] ?? '',
            'post_content' => $mapped['post']['post_content'] ?? '',
            'post_excerpt' => $mapped['post']['post_excerpt'] ?? '',
            'post_status'  => self::normalize_status( $mapped['post']['post_status'] ?? '' ),
            'post_name'    => ! empty( $mapped['post']['post_name'] ) ? sanitize_title( $mapped['post']['post_name'] ) : '',
        );

        $product_id = wp_insert_post( $post_data, true );
        if ( is_wp_error( $product_id ) ) {
            throw new \Exception( $product_id->get_error_message() );
        }

        // CRITICAL: When creating a new product, always use 'all' mode to ensure
        // all mapped fields are applied (description, images, taxonomies, attributes, etc.)
        // even if update_mode is set to 'choose' or 'price_stock' for updates.
        // This ensures new products are created with complete data.
        self::apply_product_data( $product_id, $mapped, 'all', array(), $import_id );
        if ( $import_id > 0 ) {
            update_post_meta( $product_id, '_mt_primelink_import_id', absint( $import_id ) );
        }
        return absint( $product_id );
    }

    private static function update_product( $product_id, $mapped, $update_mode, $update_fields, $import_id = 0 ) {
        $product_id = absint( $product_id );

        if ( $update_mode === 'price_stock' ) {
            self::apply_product_data( $product_id, $mapped, 'price_stock', array(), $import_id );
            return;
        }

        if ( $update_mode === 'choose' ) {
            self::apply_product_data( $product_id, $mapped, 'choose', (array) $update_fields, $import_id );
            return;
        }

        self::apply_product_data( $product_id, $mapped, 'all', array(), $import_id );
    }

    private static function apply_product_data( $product_id, $mapped, $mode, $fields, $import_id = 0 ) {
        $mode   = (string) $mode;
        $fields = (array) $fields;

        // --- Post fields ---
        $post_update = array( 'ID' => $product_id );
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'title', $fields, true ) ) ) {
            foreach ( array( 'post_title', 'post_content', 'post_excerpt', 'post_name', 'post_status' ) as $pf ) {
                if ( isset( $mapped['post'][ $pf ] ) && $mapped['post'][ $pf ] !== '' ) {
                    $post_update[ $pf ] = $mapped['post'][ $pf ];
                }
            }
            wp_update_post( $post_update );
        }

        $product = wc_get_product( $product_id );
        if ( ! $product ) {
            throw new \Exception( 'Invalid product object.' );
        }

        // --- Product data ---
        if ( $mode === 'all'
            || $mode === 'price_stock'
            || ( $mode === 'choose' && in_array( 'product_data', $fields, true ) )
        ) {
            if ( isset( $mapped['product']['sku'] ) ) {
                $product->set_sku( (string) $mapped['product']['sku'] );
            }
            if ( isset( $mapped['product']['regular_price'] ) ) {
                $product->set_regular_price( (string) $mapped['product']['regular_price'] );
            }
            if ( isset( $mapped['product']['sale_price'] ) ) {
                $product->set_sale_price( (string) $mapped['product']['sale_price'] );
            }
            if ( isset( $mapped['product']['stock_status'] ) ) {
                $product->set_stock_status( (string) $mapped['product']['stock_status'] );
            }
            if ( isset( $mapped['product']['stock'] ) && $mapped['product']['stock'] !== '' ) {
                $product->set_manage_stock( true );
                $product->set_stock_quantity( (int) $mapped['product']['stock'] );
            }
            if ( isset( $mapped['product']['weight'] ) ) {
                $product->set_weight( (string) $mapped['product']['weight'] );
            }
            if ( isset( $mapped['product']['length'] ) ) {
                $product->set_length( (string) $mapped['product']['length'] );
            }
            if ( isset( $mapped['product']['width'] ) ) {
                $product->set_width( (string) $mapped['product']['width'] );
            }
            if ( isset( $mapped['product']['height'] ) ) {
                $product->set_height( (string) $mapped['product']['height'] );
            }
            if ( isset( $mapped['product']['tax_status'] ) ) {
                $product->set_tax_status( (string) $mapped['product']['tax_status'] );
            }
            if ( isset( $mapped['product']['tax_class'] ) ) {
                $product->set_tax_class( (string) $mapped['product']['tax_class'] );
            }

            $product->save();
        }

        // --- Taxonomies ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'taxonomies', $fields, true ) ) ) {
            foreach ( (array) $mapped['tax'] as $tax => $val ) {
                if ( $val === '' || $val === null ) continue;
                $terms = ImportMapper::normalize_terms( $val );
                wp_set_object_terms( $product_id, $terms, $tax, false );
            }
        }

        // --- Attributes ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'product_data', $fields, true ) ) ) {
            if ( ! empty( $mapped['attrs'] ) ) {
                $attrs = array();

                foreach ( $mapped['attrs'] as $name => $data ) {
                    $variation = false;
                    $visible   = true;
                    $values    = $data;

                    if ( is_array( $data ) && array_key_exists( 'values', $data ) ) {
                        $variation = ! empty( $data['variation'] );
                        $visible   = array_key_exists( 'visible', $data ) ? (bool) $data['visible'] : true;
                        $values    = $data['values'];
                    } elseif ( is_array( $data ) && array_key_exists( 'variation', $data ) ) {
                        $variation = ! empty( $data['variation'] );
                    }

                    $values = ImportMapper::normalize_terms( $values );
                    if ( empty( $values ) ) continue;

                    $attr = new \WC_Product_Attribute();
                    $attr_key = ( strpos( $name, 'pa_' ) === 0 ) ? $name : sanitize_title( $name );
                    $attr->set_name( $attr_key );
                    $attr->set_options( $values );
                    $attr->set_visible( $visible );
                    $attr->set_variation( $variation );
                    $attrs[] = $attr;
                }

                if ( $attrs ) {
                    $product->set_attributes( $attrs );
                    $product->save();

                    if ( ! empty( $create_variations ) ) {
                        self::generate_variations_from_attributes( $product_id, $attrs, $mapped['product'] ?? array(), true, $mapped['variation'] ?? array() );
                    }
                }
            }
        }

        // --- Images ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'images', $fields, true ) ) ) {
            self::apply_images( $product_id, $mapped['images'] ?? array(), $import_id );
        }

        // --- Linked products (upsell / cross-sell / grouped) ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'product_data', $fields, true ) ) ) {
            if ( ! empty( $mapped['linked'] ) ) {
                $linked = $mapped['linked'];

                if ( isset( $linked['upsell_refs'] ) ) {
                    $ids = self::normalize_product_refs( $linked['upsell_refs'] );
                    if ( ! empty( $ids ) ) {
                        $product->set_upsell_ids( $ids );
                    }
                }
                if ( isset( $linked['cross_sell_refs'] ) ) {
                    $ids = self::normalize_product_refs( $linked['cross_sell_refs'] );
                    if ( ! empty( $ids ) ) {
                        $product->set_cross_sell_ids( $ids );
                    }
                }
                if ( isset( $linked['grouped_children'] ) ) {
                    $ids = self::normalize_product_refs( $linked['grouped_children'] );
                    if ( ! empty( $ids ) ) {
                        // group children use meta key on parent
                        update_post_meta( $product_id, '_children', $ids );
                        $product->set_type( 'grouped' );
                    }
                }
                $product->save();
            }
        }

        // --- Yoast meta ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'yoast', $fields, true ) ) ) {
            self::apply_yoast( $product_id, $mapped['yoast'] ?? array() );
        }

        // --- Custom meta ---
        if ( $mode === 'all' || ( $mode === 'choose' && in_array( 'custom_fields', $fields, true ) ) ) {
            foreach ( (array) $mapped['meta'] as $k => $v ) {
                update_post_meta( $product_id, $k, $v );
            }
        }
    }

    private static function apply_images( $product_id, $images, $import_id = 0 ) {
        $gallery = array();
        $feat_id = 0;

        // Get option: use first gallery image as featured
        $draft = self::get_draft( $import_id );
        $use_first_gallery_as_featured = ! empty( $draft['step_data'][6]['use_first_gallery_as_featured'] ?? false );

        // Featured image
        if ( ! empty( $images['featured_image'] ) ) {
            $feat_id = ImportMapper::maybe_attach_image( $images['featured_image'], $product_id );
            if ( $feat_id ) {
                set_post_thumbnail( $product_id, $feat_id );
            }
        }

        // Gallery images - collect from all gallery slots (images, images_1, images_2, etc.)
        $all_gallery_images = array();
        
        // Main images slot
        if ( ! empty( $images['images'] ) ) {
            $list = ImportMapper::normalize_list( $images['images'] );
            $all_gallery_images = array_merge( $all_gallery_images, $list );
        }
        
        // Additional gallery slots (images_1, images_2, etc.)
        foreach ( $images as $key => $value ) {
            if ( strpos( $key, 'images_' ) === 0 && $key !== 'images' ) {
                $list = ImportMapper::normalize_list( $value );
                $all_gallery_images = array_merge( $all_gallery_images, $list );
            }
        }
        
        // Remove duplicates while preserving order
        $all_gallery_images = array_values( array_unique( $all_gallery_images, SORT_STRING ) );

        foreach ( $all_gallery_images as $index => $url ) {
            $att_id = ImportMapper::maybe_attach_image( $url, $product_id );
            if ( $att_id ) {
                // If no featured image and option is enabled, use first gallery image as featured
                if ( $feat_id === 0 && $use_first_gallery_as_featured && $index === 0 ) {
                    set_post_thumbnail( $product_id, $att_id );
                    $feat_id = $att_id;
                } else {
                    $gallery[] = $att_id;
                }
            }
        }

        if ( $gallery ) {
            update_post_meta( $product_id, '_product_image_gallery', implode( ',', $gallery ) );
        }
    }

    private static function apply_yoast( $product_id, $yoast ) {
        if ( isset( $yoast['yoast_title'] ) ) {
            update_post_meta( $product_id, '_yoast_wpseo_title', $yoast['yoast_title'] );
        }
        if ( isset( $yoast['yoast_description'] ) ) {
            update_post_meta( $product_id, '_yoast_wpseo_metadesc', $yoast['yoast_description'] );
        }
        if ( isset( $yoast['yoast_focuskw'] ) ) {
            update_post_meta( $product_id, '_yoast_wpseo_focuskw', $yoast['yoast_focuskw'] );
        }
    }

    private static function normalize_status( $status ) {
        $status = strtolower( trim( (string) $status ) );
        if ( $status === '' || $status === 'draft' ) {
            return 'publish';
        }
        return $status;
    }

    /**
     * Turn a delimited list of IDs/SKUs into product IDs.
     *
     * @param mixed $value Comma/semicolon/pipe-separated IDs or SKUs.
     * @return array
     */
    private static function normalize_product_refs( $value ) {
        $vals = ImportMapper::normalize_list( $value );
        $ids  = array();
        foreach ( $vals as $val ) {
            $val = trim( (string) $val );
            if ( $val === '' ) {
                continue;
            }
            if ( is_numeric( $val ) ) {
                $ids[] = absint( $val );
                continue;
            }
            $sku_id = wc_get_product_id_by_sku( $val );
            if ( $sku_id ) {
                $ids[] = absint( $sku_id );
            }
        }
        return array_values( array_filter( array_unique( $ids ) ) );
    }

    /**
     * Generate product variations based on variation-enabled attributes.
     *
     * @param int   $product_id           Parent product ID.
     * @param array $attrs                Array of WC_Product_Attribute objects.
     * @param array $product_values       Current mapped product values (prices/content).
     * @param bool  $use_all_attrs        Whether to create variations from all attributes (even if not marked variation).
     * @param array $variation_overrides  Variation-specific mapped values.
     */
    private static function generate_variations_from_attributes( $product_id, $attrs, $product_values, $use_all_attrs = false, $variation_overrides = array() ) {
        if ( empty( $attrs ) ) {
            return;
        }

        $variation_map = array();
        foreach ( (array) $attrs as $attr ) {
            if ( ! $attr instanceof \WC_Product_Attribute ) {
                continue;
            }
            if ( ! $use_all_attrs && ! $attr->get_variation() ) {
                continue;
            }
            $options = $attr->get_options();
            if ( empty( $options ) ) {
                continue;
            }
            $variation_map[ $attr->get_name() ] = $options;
        }

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

        // Ensure product is variable type.
        wp_set_object_terms( $product_id, 'variable', 'product_type', false );
        $product = wc_get_product( $product_id );
        if ( ! $product instanceof \WC_Product_Variable ) {
            $product = new \WC_Product_Variable( $product_id );
        }

        $existing_signatures = array();
        foreach ( $product->get_children() as $child_id ) {
            $child = wc_get_product( $child_id );
            if ( ! $child ) {
                continue;
            }
            $sig = md5( wp_json_encode( $child->get_attributes() ) );
            $existing_signatures[ $sig ] = true;
        }

        $combinations = self::build_attribute_combinations( $variation_map );
        foreach ( $combinations as $combo ) {
            $sig = md5( wp_json_encode( $combo ) );
            if ( isset( $existing_signatures[ $sig ] ) ) {
                continue;
            }

            $variation = new \WC_Product_Variation();
            $variation->set_parent_id( $product_id );
            $variation->set_attributes( $combo );

            if ( isset( $product_values['regular_price'] ) ) {
                $variation->set_regular_price( (string) $product_values['regular_price'] );
            }
            if ( isset( $product_values['sale_price'] ) ) {
                $variation->set_sale_price( (string) $product_values['sale_price'] );
            }
            if ( isset( $variation_overrides['variation_regular_price'] ) ) {
                $variation->set_regular_price( (string) $variation_overrides['variation_regular_price'] );
            }
            if ( isset( $variation_overrides['variation_sale_price'] ) ) {
                $variation->set_sale_price( (string) $variation_overrides['variation_sale_price'] );
            }

            $var_desc = $variation_overrides['variation_content'] ?? $product_values['post_content'] ?? '';
            $var_short = $variation_overrides['variation_excerpt'] ?? $product_values['post_excerpt'] ?? '';
            if ( $var_desc !== '' ) {
                $variation->set_description( (string) $var_desc );
            }
            if ( $var_short !== '' ) {
                $variation->set_short_description( (string) $var_short );
            }

            // Stock overrides
            $var_manage_stock = $variation_overrides['variation_manage_stock'] ?? '';
            $var_stock_qty    = $variation_overrides['variation_stock'] ?? null;
            $var_stock_status = $variation_overrides['variation_stock_status'] ?? '';
            if ( $var_stock_qty !== null && $var_stock_qty !== '' ) {
                $variation->set_manage_stock( true );
                $variation->set_stock_quantity( (int) $var_stock_qty );
            } elseif ( $var_manage_stock !== '' ) {
                $variation->set_manage_stock( (bool) $var_manage_stock );
            }
            if ( $var_stock_status !== '' ) {
                $variation->set_stock_status( (string) $var_stock_status );
            }

            // Images per variation
            if ( ! empty( $variation_overrides['variation_image'] ) ) {
                $img_id = ImportMapper::maybe_attach_image( $variation_overrides['variation_image'], $variation->get_id() );
                if ( $img_id ) {
                    $variation->set_image_id( $img_id );
                }
            }
            if ( ! empty( $variation_overrides['variation_gallery'] ) ) {
                $gallery_ids = array();
                $urls = ImportMapper::normalize_list( $variation_overrides['variation_gallery'] );
                foreach ( $urls as $u ) {
                    $aid = ImportMapper::maybe_attach_image( $u, $variation->get_parent_id() );
                    if ( $aid ) {
                        $gallery_ids[] = $aid;
                    }
                }
                if ( ! empty( $gallery_ids ) ) {
                    update_post_meta( $variation->get_id(), '_variation_gallery', implode( ',', $gallery_ids ) );
                }
            }

            // Derive SKU if parent has one
            if ( ! empty( $product_values['sku'] ) ) {
                $variation->set_sku( self::compose_variation_sku( $product_values['sku'], $combo ) );
            }

            $variation->set_status( 'publish' );
            $variation->save();
        }
    }

    private static function compose_variation_sku( $parent_sku, $combo ) {
        $suffix_parts = array();
        foreach ( (array) $combo as $val ) {
            $slug = sanitize_title( (string) $val );
            if ( $slug !== '' ) {
                $suffix_parts[] = $slug;
            }
        }
        if ( empty( $suffix_parts ) ) {
            return $parent_sku;
        }
        return $parent_sku . '-' . implode( '-', $suffix_parts );
    }

    /**
     * Build all combinations for variation attributes.
     *
     * @param array $variation_map [attr_name => [options]].
     * @return array[] Array of combinations [attr_name => option].
     */
    private static function build_attribute_combinations( $variation_map ) {
        $variation_map = array_filter( (array) $variation_map );
        if ( empty( $variation_map ) ) {
            return array();
        }

        $result = array( array() );
        foreach ( $variation_map as $name => $options ) {
            $next = array();
            foreach ( $result as $combo ) {
                foreach ( (array) $options as $opt ) {
                    $combo2 = $combo;
                    $combo2[ $name ] = $opt;
                    $next[] = $combo2;
                }
            }
            $result = $next;
        }

        return $result;
    }

    /**
     * Filters helper (supports regex, in, contains, numeric comparisons).
     */
    public static function normalize_filters( $filters ) {
        $out = array();
        foreach ( (array) $filters as $f ) {
            $field = isset( $f['field'] ) ? sanitize_text_field( $f['field'] ) : '';
            $op    = isset( $f['op'] ) ? sanitize_text_field( $f['op'] ) : '=';
            $val   = $f['value'] ?? '';
            if ( is_array( $val ) ) {
                $val = implode( ',', $val );
            }
            $val = (string) $val;
            if ( $field === '' && $val === '' ) {
                continue;
            }
            $out[] = array(
                'field' => $field,
                'op'    => $op,
                'value' => $val,
            );
        }
        return $out;
    }

    public static function apply_filters_to_rows( $headers, $rows, $filters ) {
        if ( empty( $filters ) ) {
            return $rows;
        }

        $headers = array_values( (array) $headers );
        $filtered = array();

        foreach ( (array) $rows as $row ) {
            $assoc = array();
            foreach ( $headers as $idx => $h ) {
                $assoc[ $h ] = isset( $row[ $idx ] ) ? (string) $row[ $idx ] : '';
            }
            if ( self::row_matches_filters( $assoc, $filters ) ) {
                $filtered[] = $row;
            }
        }

        return $filtered;
    }

    public static function row_matches_filters( $assoc, $filters ) {
        foreach ( (array) $filters as $f ) {
            $field = $f['field'] ?? '';
            $op    = $f['op'] ?? '=';
            $value = isset( $f['value'] ) ? (string) $f['value'] : '';

            $left = isset( $assoc[ $field ] ) ? trim( (string) $assoc[ $field ] ) : '';
            $list = self::split_values( $value );

            $match = true;
            switch ( $op ) {
                case '!=':
                    $match = ( $left !== $value );
                    break;
                case 'contains':
                    $match = ( stripos( $left, $value ) !== false );
                    break;
                case 'not_contains':
                    $match = ( stripos( $left, $value ) === false );
                    break;
                case 'in':
                    $match = empty( $list ) ? false : in_array( strtolower( $left ), array_map( 'strtolower', $list ), true );
                    break;
                case 'not_in':
                    $match = empty( $list ) ? true : ! in_array( strtolower( $left ), array_map( 'strtolower', $list ), true );
                    break;
                case 'regex':
                case 'not_regex':
                    $pattern = '/' . str_replace( '/', '\/', $value ) . '/i';
                    $ok = @preg_match( $pattern, $left );
                    $match = $op === 'regex' ? ( $ok === 1 ) : ( $ok === 0 || $ok === false );
                    break;
                case '>':
                    $match = floatval( $left ) > floatval( $value );
                    break;
                case '<':
                    $match = floatval( $left ) < floatval( $value );
                    break;
                case '>=':
                    $match = floatval( $left ) >= floatval( $value );
                    break;
                case '<=':
                    $match = floatval( $left ) <= floatval( $value );
                    break;
                case '=':
                default:
                    $match = ( $left === $value );
                    break;
            }

            if ( ! $match ) {
                return false;
            }
        }
        return true;
    }

    private static function split_values( $value ) {
        $parts = preg_split( '/\s*[,|;\n\r]+\s*/', (string) $value );
        $parts = array_values( array_filter( array_map( 'trim', $parts ), function( $v ) { return $v !== ''; } ) );
        return $parts;
    }

    // --- ORDER IMPORT HELPERS ---
private static function find_existing_order_id( $mapped, $match_by ) {
    // Example: match by order_id, customer_email, etc.
    if ( $match_by === 'order_id' && !empty($mapped['order']['order_id']) ) {
        $order = wc_get_order( absint($mapped['order']['order_id']) );
        return $order ? $order->get_id() : 0;
    }
    if ( $match_by === 'customer_email' && !empty($mapped['order']['customer_email']) ) {
        $query = new \WC_Order_Query([
            'limit' => 1,
            'customer_email' => $mapped['order']['customer_email'],
        ]);
        $orders = $query->get_orders();
        return !empty($orders) ? $orders[0]->get_id() : 0;
    }
    return 0;
}
private static function create_order( $mapped, $import_id ) {
    $order_data = $mapped['order'] ?? array();
    $order = wc_create_order();
    // Example: set order fields
    if ( !empty($order_data['customer_id']) ) {
        $order->set_customer_id( absint($order_data['customer_id']) );
    }
    if ( !empty($order_data['customer_email']) ) {
        update_post_meta( $order->get_id(), '_customer_email', sanitize_email($order_data['customer_email']) );
    }
    if ( !empty($order_data['status']) ) {
        $order->set_status( sanitize_text_field($order_data['status']) );
    }
    if ( !empty($order_data['total']) ) {
        $order->set_total( floatval($order_data['total']) );
    }
    // Add line items if present
    if ( !empty($order_data['line_items']) && is_array($order_data['line_items']) ) {
        foreach ( $order_data['line_items'] as $item ) {
            $order->add_product( wc_get_product( absint($item['product_id']) ), absint($item['quantity']) );
        }
    }
    $order->save();
    update_post_meta( $order->get_id(), '_mt_primelink_import_id', absint($import_id) );
    return $order->get_id();
}
private static function update_order( $order_id, $mapped, $update_mode, $update_fields, $import_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order ) return;
    $order_data = $mapped['order'] ?? array();
    // Example: update fields
    if ( in_array('customer_email', $update_fields) && !empty($order_data['customer_email']) ) {
        update_post_meta( $order_id, '_customer_email', sanitize_email($order_data['customer_email']) );
    }
    if ( in_array('status', $update_fields) && !empty($order_data['status']) ) {
        $order->set_status( sanitize_text_field($order_data['status']) );
    }
    if ( in_array('total', $update_fields) && !empty($order_data['total']) ) {
        $order->set_total( floatval($order_data['total']) );
    }
    // Update line items if needed
    $order->save();
}
}

/* ============================================================
 * ImportMapper helper (advanced mapping rules)
 * ============================================================
 */

final class ImportMapper {

    public static function compute_mapped_value( $item, $cfg ) {
        $cfg = (array) $cfg;
        $source = $cfg['source'] ?? '';
        if ( ! $source ) {
            return '';
        }

        $raw_values = array();
        
        // Special handling for images fields - collect all images.image.* fields into array
        if ( $source === 'images' || $source === 'images.image[]' || strpos( $source, 'images.image' ) === 0 ) {
            // Collect all images.image.0, images.image.1, etc. into an array
            $image_array = array();
            $index = 0;
            $max_index = 50; // Safety limit
            while ( $index < $max_index ) {
                $key = 'images.image.' . $index;
                if ( isset( $item[ $key ] ) ) {
                    $img_val = trim( (string) $item[ $key ] );
                    if ( $img_val !== '' ) {
                        $image_array[] = $img_val;
                    }
                } else {
                    // No more indexed fields, break
                    if ( $index > 0 ) {
                        break;
                    }
                }
                $index++;
            }
            
            // If we found indexed images, use the array
            if ( ! empty( $image_array ) ) {
                $raw_values[] = $image_array;
            } elseif ( isset( $item['images'] ) ) {
                // Fallback: use 'images' field if it exists (might be comma-separated string)
                $images_val = $item['images'];
                if ( is_array( $images_val ) ) {
                    $raw_values[] = $images_val;
                } else {
                    $images_str = trim( (string) $images_val );
                    if ( $images_str !== '' ) {
                        $raw_values[] = $images_str;
                    }
                }
            }
        } else {
            $raw_values[] = $item[ $source ] ?? '';
        }
        
        if ( ! empty( $cfg['extra_sources'] ) && is_array( $cfg['extra_sources'] ) ) {
            foreach ( $cfg['extra_sources'] as $ex ) {
                $raw_values[] = $item[ $ex ] ?? '';
            }
        }

        // Conditions (all must pass)
        if ( ! empty( $cfg['conditions'] ) && is_array( $cfg['conditions'] ) ) {
            foreach ( $cfg['conditions'] as $cond ) {
                if ( ! self::check_condition( $item, $cond ) ) {
                    return '';
                }
            }
        }

        $vals = array();
        foreach ( $raw_values as $rv ) {
            // If already an array (e.g., from images.image.* collection), handle it specially
            if ( is_array( $rv ) && ! empty( $rv ) ) {
                // Apply transforms to each array element
                $transformed_array = array();
                foreach ( $rv as $arr_item ) {
                    $transformed = self::apply_transforms( $arr_item, $cfg['transforms'] ?? array() );
                    if ( $transformed !== '' && $transformed !== null ) {
                        $transformed_array[] = $transformed;
                    }
                }
                if ( ! empty( $transformed_array ) ) {
                    $vals[] = $transformed_array;
                }
                continue;
            }
            
            $val = self::apply_transforms( $rv, $cfg['transforms'] ?? array() );

            // Default
            if ( ( $val === '' || $val === null ) && isset( $cfg['default'] ) && $cfg['default'] !== '' ) {
                $val = $cfg['default'];
            }

            // Price adjustment
            if ( ! empty( $cfg['price_adjust'] ) && is_array( $cfg['price_adjust'] ) ) {
                $val = self::apply_price_adjust( $val, $cfg['price_adjust'] );
            }

            // Separator -> array
            if ( ! empty( $cfg['separator'] ) ) {
                $sep = (string) $cfg['separator'];
                $parts = array_map( 'trim', explode( $sep, (string) $val ) );
                foreach ( $parts as $p ) {
                    if ( $p !== '' ) {
                        $vals[] = $p;
                    }
                }
            } else {
                $vals[] = $val;
            }
        }

        // Single or array
        if ( count( $vals ) === 1 ) {
            $single = $vals[0];
            // If it's an array with one element, return the array (for images)
            if ( is_array( $single ) ) {
                return $single;
            }
            return $single;
        }
        // Multiple values - flatten if all are arrays
        $all_arrays = true;
        foreach ( $vals as $v ) {
            if ( ! is_array( $v ) ) {
                $all_arrays = false;
                break;
            }
        }
        if ( $all_arrays ) {
            $flattened = array();
            foreach ( $vals as $v ) {
                $flattened = array_merge( $flattened, $v );
            }
            return $flattened;
        }
        return $vals;
    }

    private static function check_condition( $item, $cond ) {
        $field = $cond['field'] ?? '';
        $op    = $cond['op'] ?? '=';
        $value = (string) ( $cond['value'] ?? '' );

        $left = isset( $item[ $field ] ) ? (string) $item[ $field ] : '';

        switch ( $op ) {
            case '=':
                return $left === $value;
            case '!=':
                return $left !== $value;
            case '>':
                return floatval( $left ) > floatval( $value );
            case '<':
                return floatval( $left ) < floatval( $value );
            case 'contains':
                return strpos( $left, $value ) !== false;
            default:
                return true;
        }
    }

    private static function apply_transforms( $raw, $transforms ) {
        $v = is_array( $raw ) ? implode( '|', $raw ) : (string) $raw;
        $t = (array) $transforms;

        if ( ! empty( $t['trim'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::trim( $v );
        }

        if ( ! empty( $t['lowercase'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::lower( $v );
        }

        if ( ! empty( $t['uppercase'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::upper( $v );
        }

        if ( ! empty( $t['capitalize'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::capitalize( $v );
        }

        if ( ! empty( $t['title_case'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::title( $v );
        }

        if ( ! empty( $t['normalize_html'] ) ) {
            $v = wp_strip_all_tags( $v );
            $v = html_entity_decode( $v, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        }

        if ( ! empty( $t['remove_accents'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::remove_accents( $v );
        }

        if ( ! empty( $t['slugify'] ) ) {
            $v = \PrimeLink\Helpers\StringHelper::slugify( $v );
        }

        if ( ! empty( $t['extract_numbers'] ) ) {
            $v = preg_replace( '/[^\d.-]/', '', $v );
        }

        if ( ! empty( $t['extract_text'] ) ) {
            $v = preg_replace( '/[\d.-]/', '', $v );
        }

        if ( ! empty( $t['url_encode'] ) ) {
            $v = urlencode( $v );
        }

        if ( ! empty( $t['url_decode'] ) ) {
            $v = urldecode( $v );
        }

        // Number transforms
        if ( ! empty( $t['round'] ) || ! empty( $t['ceil'] ) || ! empty( $t['floor'] ) ) {
            $num = floatval( $v );
            if ( is_numeric( $v ) ) {
                if ( ! empty( $t['round'] ) ) {
                    $decimals = isset( $t['round_decimals'] ) ? absint( $t['round_decimals'] ) : 2;
                    $v = number_format( $num, $decimals, '.', '' );
                } elseif ( ! empty( $t['ceil'] ) ) {
                    $v = (string) ceil( $num );
                } elseif ( ! empty( $t['floor'] ) ) {
                    $v = (string) floor( $num );
                }
            }
        }

        if ( ! empty( $t['number_format'] ) && $t['number_format'] !== '' ) {
            $num = floatval( $v );
            if ( is_numeric( $v ) ) {
                $parts = explode( '.', number_format( $num, 2, '.', '' ) );
                if ( $t['number_format'] === 'comma' ) {
                    $v = number_format( $num, 2, '.', ',' );
                } elseif ( $t['number_format'] === 'dot' ) {
                    $v = number_format( $num, 2, ',', '.' );
                } elseif ( $t['number_format'] === 'space' ) {
                    $v = number_format( $num, 2, '.', ' ' );
                }
            }
        }

        // String operations
        if ( isset( $t['substring_start'] ) && $t['substring_start'] !== '' ) {
            $start = absint( $t['substring_start'] );
            $length = isset( $t['substring_length'] ) && $t['substring_length'] !== '' ? absint( $t['substring_length'] ) : null;
            $v = \PrimeLink\Helpers\StringHelper::slice( $v, $start, $length );
        }

        if ( ! empty( $t['pad_side'] ) && ! empty( $t['pad_length'] ) ) {
            $length = absint( $t['pad_length'] );
            $char = isset( $t['pad_char'] ) && $t['pad_char'] !== '' ? $t['pad_char'][0] : ' ';
            $pad_type = ( $t['pad_side'] === 'left' ) ? STR_PAD_LEFT : ( ( $t['pad_side'] === 'both' ) ? STR_PAD_BOTH : STR_PAD_RIGHT );
            $v = \PrimeLink\Helpers\StringHelper::pad( $v, $length, $char, $pad_type );
        }

        // Date transforms
        if ( ! empty( $t['date_format'] ) && ! empty( $t['date_output'] ) ) {
            $parsed = self::parse_date( $v, $t['date_format'] );
            if ( $parsed ) {
                $v = self::format_date( $parsed, $t['date_output'] );
            }
        }

        // Regex replace
        if ( ! empty( $t['regex_pattern'] ) && $t['regex_pattern'] !== '' ) {
            $pattern = $t['regex_pattern'];
            $replacement = isset( $t['regex_replacement'] ) ? $t['regex_replacement'] : '';
            // Validate regex pattern
            if ( @preg_match( $pattern, '' ) !== false ) {
                $v = preg_replace( $pattern, $replacement, $v );
            }
        }

        // Replace rules (after other transforms)
        if ( ! empty( $t['replace'] ) && is_array( $t['replace'] ) ) {
            foreach ( $t['replace'] as $r ) {
                $from = isset( $r['from'] ) ? (string) $r['from'] : '';
                $to   = isset( $r['to'] ) ? (string) $r['to'] : '';
                if ( $from === '' ) continue;
                $v = str_replace( $from, $to, $v );
            }
        }

        return $v;
    }

    /**
     * Parse date string with given format.
     *
     * @param string $date_string Date string.
     * @param string $format Format (e.g., 'Y-m-d', 'd/m/Y').
     * @return \DateTime|\Carbon\Carbon|false
     */
    private static function parse_date( $date_string, $format ) {
        return \PrimeLink\Helpers\DateHelper::parse( $date_string, $format, false );
    }

    /**
     * Format DateTime object with given format.
     *
     * @param \DateTime|\Carbon\Carbon|int|string $date Date object, timestamp, or string.
     * @param string                              $format Format (e.g., 'Y-m-d', 'd/m/Y').
     * @return string
     */
    private static function format_date( $date, $format ) {
        return \PrimeLink\Helpers\DateHelper::format( $date, $format );
    }

    private static function apply_price_adjust( $value, $cfg ) {
        $num = floatval( $value );
        $amt = isset( $cfg['amount'] ) ? floatval( $cfg['amount'] ) : 0;
        $type = isset( $cfg['type'] ) ? $cfg['type'] : '';
        if ( ! is_numeric( $value ) || $type === '' ) {
            return $value;
        }
        switch ( $type ) {
            case 'percent_plus':
                return $num + ( $num * ( $amt / 100 ) );
            case 'percent_minus':
                return $num - ( $num * ( $amt / 100 ) );
            case 'fixed_plus':
                return $num + $amt;
            case 'fixed_minus':
                return $num - $amt;
            default:
                return $value;
        }
    }

    public static function normalize_terms( $val ) {
        if ( is_array( $val ) ) {
            return array_map( 'sanitize_text_field', $val );
        }

        $str = (string) $val;

        // Support "A | B | C" and "A,B,C" and "A > B"
        $parts = preg_split( '/\s*[\|,>]\s*/', $str );
        $parts = array_values( array_filter( array_map( 'trim', $parts ) ) );

        return array_map( 'sanitize_text_field', $parts );
    }

    public static function normalize_list( $val ) {
    if ( is_array( $val ) ) {
        $vals = array_values( array_filter( array_map( 'trim', $val ) ) );
    } else {

        $str = trim( (string) $val );
        if ( $str === '' ) {
            return array();
        }

        // If user didn't set separator, fallback to common delimiters.
        $parts = preg_split( '/\s*[\|,;\n\r]+\s*/', $str );
        $vals = array_values( array_filter( array_map( 'trim', $parts ) ) );
    }

    // Sanitize URLs if present
    $vals = array_map( array( __CLASS__, 'sanitize_url' ), $vals );

    return $vals;
}



    /**
     * Download/attach image for a product.
     * MVP: supports URL; local file path later.
     * 
     * @param string $url Image URL.
     * @param int    $post_id Post ID to attach to.
     * @param array  $options Optional optimization options.
     * @return int Attachment ID or 0 on failure.
     */
    public static function maybe_attach_image( $url, $post_id, $options = array() ) {
        $url = self::sanitize_url( $url );
        if ( ! $url ) return 0;

        // If already attachment URL, try to find existing.
        $att_id = attachment_url_to_postid( $url );
        if ( $att_id ) {
            return absint( $att_id );
        }

        // Download sideload
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $tmp = download_url( $url );
        if ( is_wp_error( $tmp ) ) {
            return 0;
        }

        // Validate image before processing
        $validation = \PrimeLink\Helpers\ImageHelper::validate( $tmp );
        if ( ! $validation['valid'] ) {
            @unlink( $tmp );
            LoggerHelper::warning( 'Invalid image file', array(
                'url'   => $url,
                'error' => $validation['error'] ?? 'Unknown error',
            ) );
            return 0;
        }

        // Optimize image if options provided and Intervention Image is available
        $file_to_upload = $tmp;
        $optimized_path = null;
        
        if ( ! empty( $options ) && \PrimeLink\Helpers\ImageHelper::has_intervention_image() ) {
            $optimize_options = wp_parse_args( $options, array(
                'max_width'      => 0, // 0 = no resize
                'max_height'     => 0,
                'quality'        => 85,
                'format'         => null, // Keep original
                'strip_metadata' => true,
            ) );

            // Only optimize if resize or quality options are set
            if ( $optimize_options['max_width'] > 0 || $optimize_options['max_height'] > 0 || $optimize_options['quality'] < 100 ) {
                $upload_dir = wp_upload_dir();
                $optimized_dir = trailingslashit( $upload_dir['basedir'] ) . 'primelink/temp/';
                if ( ! \PrimeLink\Helpers\FilesystemHelper::directory_exists( $optimized_dir ) ) {
                    \PrimeLink\Helpers\FilesystemHelper::create_directory( $optimized_dir );
                }

                $file_extension = pathinfo( parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION );
                $optimized_path = $optimized_dir . uniqid( 'img_', true ) . '.' . ( $file_extension ?: 'jpg' );

                $optimize_result = \PrimeLink\Helpers\ImageHelper::optimize( $tmp, $optimized_path, $optimize_options );
                
                if ( $optimize_result['success'] ) {
                    $file_to_upload = $optimized_path;
                    
                    // Log optimization results
                    if ( isset( $optimize_result['original_size'] ) && isset( $optimize_result['optimized_size'] ) ) {
                        $savings = $optimize_result['original_size'] - $optimize_result['optimized_size'];
                        $savings_percent = $optimize_result['original_size'] > 0 
                            ? round( ( $savings / $optimize_result['original_size'] ) * 100, 1 ) 
                            : 0;
                        
                        if ( $savings > 0 ) {
                            LoggerHelper::debug( 'Image optimized', array(
                                'url'            => $url,
                                'original_size'  => size_format( $optimize_result['original_size'] ),
                                'optimized_size' => size_format( $optimize_result['optimized_size'] ),
                                'savings'        => size_format( $savings ) . " ({$savings_percent}%)",
                            ) );
                        }
                    }
                } else {
                    // Optimization failed, use original
                    LoggerHelper::warning( 'Image optimization failed, using original', array(
                        'url'   => $url,
                        'error' => $optimize_result['error'] ?? 'Unknown error',
                    ) );
                }
            }
        }

        $file_array = array(
            'name'     => basename( parse_url( $url, PHP_URL_PATH ) ),
            'tmp_name' => $file_to_upload,
        );

        $id = media_handle_sideload( $file_array, $post_id );
        
        // Cleanup temp files
        @unlink( $tmp );
        if ( $optimized_path && \PrimeLink\Helpers\FilesystemHelper::file_exists( $optimized_path ) ) {
            \PrimeLink\Helpers\FilesystemHelper::delete_file( $optimized_path );
        }

        if ( is_wp_error( $id ) ) {
            return 0;
        }

        return absint( $id );
    }

    private static function sanitize_url( $url ) {
        $url = trim( (string) $url );
        if ( $url === '' ) {
            return '';
        }

        // Replace backslashes and collapse duplicate slashes (except after scheme).
        $url = str_replace( '\\', '/', $url );

        if ( preg_match( '#^https?://#i', $url ) ) {
            // Preserve scheme//host, collapse the rest.
            $parts = parse_url( $url );
            if ( ! $parts || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
                return $url;
            }

            $path = isset( $parts['path'] ) ? preg_replace( '#/{2,}#', '/', $parts['path'] ) : '';
            $path = '/' . ltrim( $path, '/' );

            $url = $parts['scheme'] . '://' . $parts['host']
                . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' )
                . $path
                . ( isset( $parts['query'] ) ? '?' . $parts['query'] : '' )
                . ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' );
        } else {
            $url = preg_replace( '#/{2,}#', '/', $url );
        }

        return $url;
    }

    /**
     * Render email builder blocks to HTML.
     *
     * @param array $blocks   Builder blocks.
     * @param array $context  Placeholder context.
     * @return string
     */
    private static function render_email_builder( $blocks, $context = array() ) {
        $ctx = wp_parse_args( $context, array(
            'import_id' => '',
            'status'    => '',
            'created'   => '',
            'updated'   => '',
            'skipped'   => '',
            'errors'    => '',
        ) );

        $replace = function( $text ) use ( $ctx ) {
            $map = array(
                '{import_id}' => $ctx['import_id'],
                '{status}'    => $ctx['status'],
                '{created}'   => $ctx['created'],
                '{updated}'   => $ctx['updated'],
                '{skipped}'   => $ctx['skipped'],
                '{errors}'    => $ctx['errors'],
            );
            return strtr( (string) $text, $map );
        };

        $html  = '<div style="font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto;padding:24px;background:#f7f9fb;border-radius:12px;">';

        foreach ( (array) $blocks as $block ) {
            $type = $block['type'] ?? '';
            switch ( $type ) {
                case 'heading':
                    $level = max( 1, min( 4, absint( $block['level'] ?? 2 ) ) );
                    $text  = esc_html( $replace( $block['text'] ?? '' ) );
                    $html .= sprintf(
                        '<h%d style="margin:0 0 12px;font-size:%dpx;color:#111;">%s</h%d>',
                        $level,
                        $level === 1 ? 24 : ( $level === 2 ? 22 : ( $level === 3 ? 20 : 18 ) ),
                        $text,
                        $level
                    );
                    break;
                case 'paragraph':
                    $text  = nl2br( esc_html( $replace( $block['text'] ?? '' ) ) );
                    $html .= '<p style="margin:0 0 14px;line-height:1.6;color:#1f2933;">' . $text . '</p>';
                    break;
                case 'stats':
                    $html .= '<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;margin:0 0 16px;border-collapse:collapse;">';
                    $stats = array(
                        'Created' => $ctx['created'],
                        'Updated' => $ctx['updated'],
                        'Skipped' => $ctx['skipped'],
                        'Errors'  => $ctx['errors'],
                        'Status'  => $ctx['status'],
                    );
                    foreach ( $stats as $label => $value ) {
                        $html .= '<tr>';
                        $html .= '<td style="padding:6px 0;color:#6c7680;font-weight:600;">' . esc_html( $label ) . '</td>';
                        $html .= '<td style="padding:6px 0;text-align:right;color:#111;">' . esc_html( (string) $value ) . '</td>';
                        $html .= '</tr>';
                    }
                    $html .= '</table>';
                    break;
                case 'button':
                    $text = esc_html( $replace( $block['text'] ?? __( 'View import', 'primelink' ) ) );
                    $url  = esc_url( $block['url'] ?? admin_url( 'admin.php?page=mt-primelink-manage-import' ) );
                    $html .= sprintf(
                        '<div style="margin:12px 0 18px;"><a href="%s" style="display:inline-block;padding:12px 18px;background:#111827;color:#fff;text-decoration:none;border-radius:10px;font-weight:600;">%s</a></div>',
                        $url,
                        $text
                    );
                    break;
                case 'html':
                    $allowed = wp_kses_allowed_html( 'post' );
                    // Allow inline style and class on common tags.
                    foreach ( array( 'p','div','span','table','thead','tbody','tr','td','th','ul','ol','li','a','img','h1','h2','h3','h4','strong','em' ) as $tag ) {
                        if ( ! isset( $allowed[ $tag ] ) ) {
                            $allowed[ $tag ] = array();
                        }
                        $allowed[ $tag ]['style'] = true;
                        $allowed[ $tag ]['class'] = true;
                        if ( $tag === 'a' ) {
                            $allowed[ $tag ]['href']   = true;
                            $allowed[ $tag ]['target'] = true;
                            $allowed[ $tag ]['rel']    = true;
                        }
                        if ( $tag === 'img' ) {
                            $allowed[ $tag ]['src'] = true;
                            $allowed[ $tag ]['alt'] = true;
                            $allowed[ $tag ]['width'] = true;
                            $allowed[ $tag ]['height'] = true;
                        }
                    }
                    $raw_html = $replace( $block['html'] ?? $block['text'] ?? '' );
                    $html    .= wp_kses( $raw_html, $allowed );
                    break;
                default:
                    // Unknown block, skip.
                    break;
            }
        }

        $html .= '</div>';
        return $html;
    }
}

