<?php
 
 /**
 * =============================================================================
 * @package         Blue Raven (Free Version)
 * @Plugin URI      https://enterrahost.com/plugins/blue-raven
 * 
 * @Author          EnterraHost
 * @Author URI      https://enterrahost.com
 * @License         GPL-2.0-or-later
 *  
 * @Copyright       (c) 2025 EnterraHost
 * @Contents        Import Blue Raven Products to Woo
 *
 * =============================================================================
 * 
 * || INDEX:
 * ||   - UPDATE PRODUCT
 * ||   - SIMPLE PRODUCT
 * ||   - GROUPED PRODUCT
 * ||   - VARIABLE PRODUCT
 * ||   - OTHER PRODUCT UPDATES
 * ||   - PROCESS ATTRIBUTES
 * ||   - PROCESS TAGS AND CATEGORIES
 * ||   - PROCESS IMAGE
 * ||   - MEDIA UPLOAD
 * ||   - PROCESS CSV
 * ||   - VARIABLE CHILD PRODUCTS 
 * ||   - EXTRA FUNCTIONS 
 * || 
 * =============================================================================
 */

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

/*
============================================================
   UPDATE PRODUCT
============================================================
*/

    ## =========== Function to update an existing WooCommerce product =========== ##

function update_woo_product($product_id, $variations_CSV, $attibuteJSON, $product_data, $draft, $slug) {
    
    $product = wc_get_product( $product_id );
    if ( ! $product ) return new WP_Error( 'product_not_found', __( 'Product not found.', 'blue-raven' ) );
    $newType = strtolower( sanitize_key($product_data["Type"]));
    
    ### =========== Case dependent =========== ##

/*
============================================================
   SIMPLE PRODUCT
============================================================
*/  

    //BlueRaven_SendError("LOG", "blue-raven/includes/third-party/class-blue-raven-woo-updater.php", "product_data", json_encode($product_data));

    switch ( $newType ) {
        case 'simple':
            
            $product = new WC_Product_Simple( $product_id );
            $product->save();
            
            $manageStock = isset($product_data['Stock']) && $product_data['Stock'] != '';
            
            $product->set_price($product_data['Regular price']);
            $product->set_regular_price($product_data['Regular price']);
            $product->set_sale_price($product_data['Sale price']);
            $product->set_date_on_sale_from($product_data['Date sale price starts']);
            $product->set_date_on_sale_to($product_data['Date sale price ends']);
            $product->set_manage_stock($manageStock);
            $product->set_stock_quantity($product_data['Stock']);
            $product->set_stock_status($product_data['In stock?'] ? 'instock' : 'outofstock');
            $product->set_low_stock_amount($product_data['Low stock amount']);
            $product->set_backorders($product_data['Backorders allowed?']);
            $product->set_sold_individually((bool)$product_data['Sold individually?']);
            $product->set_weight($product_data['Weight (kg)']);
            $product->set_length($product_data['Length (cm)']);
            $product->set_width($product_data['Width (cm)']);
            $product->set_height($product_data['Height (cm)']);
    
            $product->set_reviews_allowed((bool)$product_data['Allow customer reviews?']);
            $product->set_purchase_note($product_data['Purchase note']);
            $product->set_virtual($product_data['Virtual'] == '1');
            
    ## =========== Shipping Class =========== ##
    
            if($product_data['Shipping Class'] != ''){
                $shipping_class_slug = sanitize_title($product_data['Shipping Class']); 
                $shipping_class = get_term_by('slug', $shipping_class_slug, 'product_shipping_class');

                if ( $shipping_class && ! is_wp_error($shipping_class) ) {
                    $product->set_shipping_class_id( $shipping_class->term_id );
                } else {
                    BlueRaven_SendError("ERROR", "blue-raven/includes/third-party/class-blue-raven-woo-updater.php", "Shipping class not found:", $product_data['Shipping Class']);
                }
            }
           
    ## =========== Sub Products =========== ##
    
            $raw_upsells = isset($product_data['Upsells']) && is_string($product_data['Upsells']) ? explode(',', $product_data['Upsells']) : [];
            
            if (!empty($raw_upsells)) {
                $upsell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_upsells)));
                $product->set_upsell_ids($upsell_ids);
            }
                        
            $raw_cross_sells = isset($product_data['Cross-sells']) && is_string($product_data['Cross-sells']) ? explode(',', $product_data['Cross-sells']) : [];
            
            if (!empty($raw_cross_sells)) {
                $cross_sell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_cross_sells)));
                $product->set_cross_sell_ids($cross_sell_ids);
            }
            
            $product->set_menu_order( (int) $product_data['Position'] );

    ## ========== Process attributes for variable products =========== ##
            
            $attribute_array = json_decode($attibuteJSON, true);
            if (!is_array($attribute_array)) return false;

            $attributes = [];

            foreach ($attribute_array as $attr) {
                if (empty($attr['name']) || empty($attr['values']) || !is_array($attr['values'])) {continue; }

                $label = trim($attr['name']); 
                $slug  = sanitize_title($label); 
                $options = array_map('trim', $attr['values']); 

                $attribute = new WC_Product_Attribute();
                $attribute->set_name($label); 
                $attribute->set_options($options); 
                $attribute->set_visible(!empty($attr['visible']));
                $attribute->set_variation(!empty($attr['variations']));
                $attributes[$label] = $attribute;
            }

            $product->set_attributes($attributes);
            $product->save();            

/*
============================================================
   GROUPED PRODUCT
============================================================
*/

            break;
        case 'grouped':
            
            $product = new WC_Product_Grouped( $product_id );
            $product->save();
            $product->set_manage_stock( false ); 
            $product->set_sold_individually( false );
            
    ## =========== Sub Products =========== ##
    
            $grouped_products_raw = isset($product_data['Grouped products']) && is_string($product_data['Grouped products']) ? explode(',', $product_data['Grouped products']) : [];
                
            if (!empty($grouped_products_raw)) {
                $grouped_product_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $grouped_products_raw)));
                $product->set_children($grouped_product_ids);
            }

            $raw_upsells = isset($product_data['Upsells']) && is_string($product_data['Upsells']) ? explode(',', $product_data['Upsells']) : [];
            
            if (!empty($raw_upsells)) {
                $upsell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_upsells)));
                $product->set_upsell_ids($upsell_ids);
            }
            
            $product->set_menu_order( (int) $product_data['Position'] );
    
    ## ========== Process attributes for variable products =========== ##
            
            $attribute_array = json_decode($attibuteJSON, true);
            if (!is_array($attribute_array)) return false;

            $attributes = [];

            foreach ($attribute_array as $attr) {
                if (empty($attr['name']) || empty($attr['values']) || !is_array($attr['values'])) {continue; }

                $label = trim($attr['name']); 
                $slug  = sanitize_title($label); 
                $options = array_map('trim', $attr['values']); 

                $attribute = new WC_Product_Attribute();
                $attribute->set_name($label); 
                $attribute->set_options($options); 
                $attribute->set_visible(!empty($attr['visible']));
                $attribute->set_variation(!empty($attr['variations']));
                $attributes[$label] = $attribute;
            }

            $product->set_attributes($attributes);
            $product->save();

/*
============================================================
   EXTERNAL PRODUCT
============================================================
*/

            break;
        case 'external':
            
            $product = new WC_Product_External( $product_id );
            $product->save();
            $product->set_stock_status( 'instock' );
            $product->set_virtual( true );
            $product->set_reviews_allowed( true );
            $product->set_product_url($product_data['External URL']); 
            $product->set_button_text($product_data['Button text']); 

    ## =========== Sub Products =========== ##
    
            $raw_upsells = isset($product_data['Upsells']) && is_string($product_data['Upsells']) ? explode(',', $product_data['Upsells']) : [];
            
            if (!empty($raw_upsells)) {
                $upsell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_upsells)));
                $product->set_upsell_ids($upsell_ids);
            }
            
            $product->set_menu_order( (int) $product_data['Position'] );

    ## ========== Process attributes for variable products =========== ##
            
            $attribute_array = json_decode($attibuteJSON, true);
            if (!is_array($attribute_array)) return false;

            $attributes = [];

            foreach ($attribute_array as $attr) {
                if (empty($attr['name']) || empty($attr['values']) || !is_array($attr['values'])) {continue; }

                $label = trim($attr['name']); 
                $slug  = sanitize_title($label); 
                $options = array_map('trim', $attr['values']); 

                $attribute = new WC_Product_Attribute();
                $attribute->set_name($label); 
                $attribute->set_options($options); 
                $attribute->set_visible(!empty($attr['visible']));
                $attribute->set_variation(!empty($attr['variations']));
                $attributes[$label] = $attribute;
            }

            $product->set_attributes($attributes);
            $product->save();

/*
============================================================
   VARIABLE PRODUCT
============================================================
*/

            break;
        case 'variable':

            $product = new WC_Product_Variable( $product_id );
            $product->save();

            $product->set_weight($product_data['Weight (kg)']);
            $product->set_length($product_data['Length (cm)']);
            $product->set_width($product_data['Width (cm)']);
            $product->set_height($product_data['Height (cm)']);

            $product->set_reviews_allowed((bool)$product_data['Allow customer reviews?']);
            $product->set_purchase_note($product_data['Purchase note']);
            $product->set_menu_order( (int) $product_data['Position'] );
            
            $product->set_manage_stock(isset($product_data['Stock']) && $product_data['Stock'] != '');
            $product->set_stock_quantity($product_data['Stock']);
            $product->set_stock_status($product_data['In stock?'] ? 'instock' : 'outofstock');
            $product->set_low_stock_amount($product_data['Low stock amount']);
            $product->set_backorders($product_data['Backorders allowed?']);
            $product->set_sold_individually((bool)$product_data['Sold individually?']);
            
    ## =========== Sub Products =========== ##
    
            $raw_upsells = isset($product_data['Upsells']) && is_string($product_data['Upsells']) ? explode(',', $product_data['Upsells']) : [];
            
            if (!empty($raw_upsells)) {
                $upsell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_upsells)));
                $product->set_upsell_ids($upsell_ids);
            }
                        
            $raw_cross_sells = isset($product_data['Cross-sells']) && is_string($product_data['Cross-sells']) ? explode(',', $product_data['Cross-sells']) : [];
            
            if (!empty($raw_cross_sells)) {
                $cross_sell_ids = array_values(array_filter(array_map(function($item) {$id = (int) preg_replace('/\D/', '', $item);return $id > 0 ? $id : null;}, $raw_cross_sells)));
                $product->set_cross_sell_ids($cross_sell_ids);
            }
                        
    ## ========== Process attributes for variable products =========== ##
            
            $attribute_array = json_decode($attibuteJSON, true);
            if (!is_array($attribute_array)) return false; 

            $attributes = [];

            foreach ($attribute_array as $attr) {
                if (empty($attr['name']) || empty($attr['values']) || !is_array($attr['values'])) {continue; }

                $label = trim($attr['name']); 
                $slug  = sanitize_title($label); 
                $options = array_map('trim', $attr['values']); 

                $attribute = new WC_Product_Attribute();
                $attribute->set_name($label); 
                $attribute->set_options($options); 
                $attribute->set_visible(!empty($attr['visible']));
                $attribute->set_variation(!empty($attr['variations']));
                $attributes[$label] = $attribute;
            }

            $product->set_attributes($attributes);
            $product->save();
            
    ## =========== Shipping Class =========== ##
            
            if($product_data['Shipping Class'] != ''){
                $shipping_class_slug = sanitize_title($product_data['Shipping Class']); 
                $shipping_class = get_term_by('slug', $shipping_class_slug, 'product_shipping_class');

                if ( $shipping_class && ! is_wp_error($shipping_class) ) {
                    $product->set_shipping_class_id( $shipping_class->term_id );
                } else {
                    BlueRaven_SendError("ERROR", "blue-raven/includes/third-party/class-blue-raven-woo-updater.php", "Shipping class not found:", $product_data['Shipping Class']);
                }
            }

    ## =========== Variable Products =========== ##      

             
            setVariableProduct($product, $variations_CSV, $attribute_array, $draft);
        
/*  
============================================================
   OTHER PRODUCT UPDATES
============================================================
*/
            
            break;
        default:
            BlueRaven_SendError("ERROR", "blue-raven/includes/third-party/class-blue-raven-woo-updater.php", "SWITCH", 'default');
            break;
    }
        
    $product->set_name($product_data['Name']);
    
    $desired_slug = sanitize_title($slug);
    if ($product->get_slug() !== $desired_slug) {
        $existing = get_page_by_path($slug, OBJECT, 'product');
        if ($existing) $desired_slug .= '-' . time(); 
        $product->set_slug($desired_slug);
    }

    $product->set_sku($product_data['SKU']);
    $product->set_description($product_data['Description']);
    $product->set_short_description($product_data['Short description']);
    $product->set_status($draft ? 'publish' : 'draft');
    
    ## =========== Downloads =========== ##
    
    if (isset($product_data['Download limit']) && isset($product_data['Download 1 name'])) {
        
        $download_limit = trim($product_data['Download limit']) === '' ? -1 : (int) $product_data['Download limit'];
        $download_expiry = trim($product_data['Download expiry days']) === '' ? -1 : (int) $product_data['Download expiry days'];

        $product->set_download_limit($download_limit);
        $product->set_download_expiry($download_expiry);
        $product->set_downloadable(true);

        $downloads = array();
        
        $i = 1;
        while (isset($product_data["Download {$i} ID"]) || isset($product_data["Download {$i} URL"])) {
            if (!empty($product_data["Download {$i} ID"]) && !empty($product_data["Download {$i} URL"])) $downloads[] = array('id' => $product_data["Download {$i} ID"],'name' => $product_data["Download {$i} name"] ?? "Download {$i}",'file'=> $product_data["Download {$i} URL"]);
            $i++;
        }
        
        if (!empty($downloads)) {
            $product->set_downloadable(true);
            $product->set_downloads($downloads);
            $product->save();
        } else {
            $product->set_downloadable(false);
            $product->save();
        }
    } else {

        $product->set_downloadable(false);
        $product->save();    
    }

    if (!empty($product_data['Meta: blue_raven_meta_title'])) update_post_meta($product_id, '_blue_raven_meta_title', sanitize_text_field($product_data['Meta: blue_raven_meta_title']));
    if (!empty($product_data['Meta: blue_raven_meta_description'])) update_post_meta($product_id, '_blue_raven_meta_description', sanitize_text_field($product_data['Meta: blue_raven_meta_description']));
    if (!empty($product_data['Meta: blue_raven_meta_keywords'])) update_post_meta($product_id, '_blue_raven_meta_tags', sanitize_text_field($product_data['Meta: blue_raven_meta_keywords']));
    
    ## =========== After updating the type, it's good practice to clear product caches =========== ## 
    
    $product->save();
    return true; 
}

/*
============================================================
   PROCESS ATTRIBUTES
============================================================
*/

    ## =========== Function to process attributes for variable products =========== ##
    
function process_attributes($product_data) {
    $attributes = [];

    foreach ($product_data as $key => $value) {
        if (preg_match('/^Attribute (\d+) Name$/i', $key, $matches)) {
            $i = $matches[1];

            $attr_label = trim($product_data["Attribute {$i} Name"] ?? '');
            $attr_values = trim($product_data["Attribute {$i} value(s)"] ?? '');
            $attr_visible = !empty($product_data["Attribute {$i} visible"]);
            $attr_variation = !empty($product_data["Attribute {$i} used_for_variations"]);

            if (!empty($attr_label) && !empty($attr_values)) {
                $attr_slug = sanitize_title($attr_label);
                $options = array_map('sanitize_title', array_map('trim', explode(',', $attr_values)));

                $attribute = new WC_Product_Attribute();
                $attribute->set_name($attr_slug);
                $attribute->set_options($options);
                $attribute->set_visible($attr_visible);
                $attribute->set_variation($attr_variation);

                $attributes[$attr_slug] = $attribute; 
            }
        }
    }

    return $attributes;
}

/*
============================================================
   PROCESS TAGS AND CATEGORIES
============================================================
*/

    ## =========== Function to process categories and tags =========== ##
    
function process_taxonomies($product_data) {
    
    ## =========== Categories =========== ##
        
    $categories = explode(',', $product_data['Categories']);
    $category_ids = array();
    foreach ($categories as $category) {
        $term = get_term_by('name', trim($category), 'product_cat');
        
        if ($term) {
            $category_ids[] = $term->term_id;
        } else {
            
                ///-- Create category if it doesn't exist 
    
            $new_term = wp_insert_term(trim($category), 'product_cat');
            !is_wp_error($new_term) && $category_ids[] = $new_term['term_id'];
        }
    }
    
    ## =========== Tags =========== ##
    
    $brands = explode(',', $product_data['Brands']);
    $brand_ids = array();
    
    foreach ($brands as $brand) {
        $brand = trim($brand);
        $term = get_term_by('name', $brand, 'product_brand');
    
        if ($term) {
            $brand_ids[] = $term->term_id;
        } else {
            
                ///-- Create brand if it doesn't exist
            
            $new_term = wp_insert_term($brand, 'product_brand');
            !is_wp_error($new_term) && $brand_ids[] = $new_term['term_id'];
        }
    }

    ## =========== Tags =========== ##
    
    $tags = explode(',', $product_data['Tags']);
    $tag_ids = array();
    
    foreach ($tags as $tag) {
        $term = get_term_by('name', trim($tag), 'product_tag');
        
        if ($term) {
            $tag_ids[] = $term->term_id;
        } else {
            
                ///-- Create tag if it doesn't exist 
    
            $new_term = wp_insert_term(trim($tag), 'product_tag');
            !is_wp_error($new_term) && $tag_ids[] = $new_term['term_id'];
        }
    }
    
    return array('categories' => $category_ids, 'tags' => $tag_ids, 'brands' => $brand_ids);
}

/*
============================================================
   PROCESS IMAGE
============================================================
*/

    ## =========== Function to process images =========== ##

function process_images($product_data, $product_id) {
    $image_urls = explode(',', $product_data['Images']);
    $gallery_ids = array();

    foreach ($image_urls as $index => $image_url) {
        $image_url = trim($image_url);
        
        if (!empty($image_url)) {
            $attachment_id = get_attachment_id_by_url_basename($image_url);
            $attachment_id = $attachment_id ?: upload_image_to_media_library($image_url);
            $attachment_id && ($index === 0 ? set_post_thumbnail($product_id, $attachment_id) : $gallery_ids[] = $attachment_id);

        }
    }

    return $gallery_ids;
}

/*
============================================================
   MEDIA UPLOAD
============================================================
*/

    ## =========== Helper function to upload image to media library =========== ##
    
function upload_image_to_media_library($image_url) {
    require_once(ABSPATH . 'wp-admin/includes/media.php');
    require_once(ABSPATH . 'wp-admin/includes/file.php');
    require_once(ABSPATH . 'wp-admin/includes/image.php');
    
    $tmp = download_url($image_url);
    
    if (is_wp_error($tmp)) return false;
    
    $file_array = array('name' => basename($image_url),'tmp_name' => $tmp);
    $attachment_id = media_handle_sideload($file_array, 0);
    
    if (is_wp_error($attachment_id)) { wp_delete_file($file_array['tmp_name']); return false; }
    
    return $attachment_id;
}

/*
============================================================
    PROCESS CSV
============================================================
*/

/**
 * Process CSV text to create or update WooCommerce products
 *
 * @param string $csv_text The CSV content as text
 * @param mixed $x Optional parameter (unused in this function)
 * @param bool $draft Whether to create products as drafts
 * @return array Summary of processed products
 */
 
function process_csv_file($csv_text, $variations_CSV, $attibuteJSON, $product_id, $tid, $draft, $slug) {

    if ( ! class_exists( 'WooCommerce' ) ) return new WP_Error( 'woocommerce_not_active', __( 'WooCommerce is not active.', 'blue-raven' ) );
    
    ## ========== Convert CSV text to array =========== ##
    
    $csv_data = [];
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Safe php://temp in-memory stream
	$stream = fopen('php://temp', 'r+');
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Safe fwrite to php://temp
	fwrite($stream, $csv_text);
    rewind($stream);
    
    while (($row = fgetcsv($stream)) !== false) { $csv_data[] = $row; }
    fclose($stream); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Safe fclose of php://temp stream
    
    if (empty($csv_data)) { throw new Exception("No CSV data found in the provided text"); }

    ## ========== Extract headers and remove them from data =========== ##
    
    $headers = array_shift($csv_data);
    $results = [];
    $processed_count = 0;
    $errors = [];

    foreach ($csv_data as $row_index => $row) {
        try {
            if (count($headers) !== count($row)) {
                $c = count($headers);
                $d = count($row);
                throw new Exception("Header/column count mismatch on row " . ($row_index + 1) . "  - $c :: $d - ");
            }

            $product_data = array_combine($headers, $row);
            
    ## ========== Update or Create =========== ##
    
            global $wpdb;
            $table_name = $wpdb->prefix . 'BlueRavenProducts';
            $current = $wpdb->get_var($wpdb->prepare("SELECT woo_product_id FROM $table_name WHERE id = %d", $tid));

            if (empty($current)) {
                
    ## ========== Create new WooCommerce product =========== ##
    
                $product_id = $product_id ?: (new WC_Product())->save();
        
    ## ========== Update the DB with the new product_id =========== ##
    
                $wpdb->update($table_name,['woo_product_id' => $product_id],['id' => $tid],['%d'],['%d']);
            }
            
            update_woo_product($product_id, $variations_CSV, $attibuteJSON, $product_data, $draft, $slug);

    ## ========== Process categories and tags =========== ## 
    
            $taxonomies = process_taxonomies($product_data);
            
            !empty($taxonomies['categories']) && wp_set_object_terms($product_id, $taxonomies['categories'], 'product_cat');
            !empty($taxonomies['tags']) && wp_set_object_terms($product_id, $taxonomies['tags'], 'product_tag');
            !empty($taxonomies['brands']) && wp_set_object_terms($product_id, $taxonomies['brands'], 'product_brand');

    ## ========== Process Images =========== ##  
    
            $images_string = sanitize_text_field( wp_unslash( $product_data['Images'] ?? '' ) );
            $image_urls = array_map('trim', explode(',', $images_string));
            $gallery_ids = [];
            
            foreach ($image_urls as $image_url) {
                $clean_url = str_replace('\\/', '/', $image_url);
                if ($attachment_id = attachment_url_to_postid($clean_url)) $gallery_ids[] = $attachment_id;
            }
            
            if (!empty($gallery_ids)) {
                $thumbnail_id = $gallery_ids[0];
                set_post_thumbnail($product_id, $thumbnail_id);
                array_shift($gallery_ids); 
                update_post_meta($product_id, '_product_image_gallery', implode(',', $gallery_ids));
            }
            
            $processed_count++;
            $results[] = ['product_id' => $product_id,'action' => $action,'status' => 'success','data' => $product_data];
    
        } catch (Exception $e) {
            $errors[] = ['row_index' => $row_index + 1,'error' => $e->getMessage(),'data' => $row];
            continue;
        }
    }

    return [
        'processed_count' => $processed_count,
        'successful' => $results,
        'errors' => $errors,
        'total_rows' => count($csv_data)
    ];
}

/*
============================================================
    ARIABLE CHILD PRODUCTS 
============================================================
*/

function setVariableProduct($product, $variations_CSV, $attribute_array, $draft) {
    $variation_array = parse_variations_csv($variations_CSV);
            
    if (isset($variation_array) && is_array($variation_array)) {
        
        $variationList = [];

        foreach ($variation_array as $variation_data) {
            
            $product_id = $product->get_id();
            $sku        = $variation_data['SKU'];
            $manageStock = isset($variation_data['Stock']) && $variation_data['Stock'] != '';

            // Try to get variation by SKU
            $variation = get_unique_variation_by_sku($sku, $product_id);

            // If variation does not exist, create new
            if (!$variation) {
                $variation = new WC_Product_Variation();
                $variation->set_parent_id($product_id);
                $variation->set_sku($sku); // assign SKU on create
            } else {
                // ✅ Instead of erroring, just update the variation
                $variation = wc_get_product($variation->get_id());
            }

            // No need to duplicate SKU check here, since we reuse existing
            // But keep a safeguard to ensure SKU is not used by another product
            $existing_product_id = wc_get_product_id_by_sku($sku);
            if ($existing_product_id && $existing_product_id != $variation->get_id() && $existing_product_id != $product_id) {
                wp_send_json_error(array(
                    'message' => "Duplicate SKU detected for variation of product ID {$product_id}: {$sku} (already used by product ID {$existing_product_id})"
                ));
                wp_die();
            }
            
                ///-- Set variation attributes

            $variation_attributes = build_variation_attributes($variation_data, $attribute_array);
            $variation->set_attributes($variation_attributes);
            $variation->set_status(!empty($variation_data['Published']) ? 'publish' : 'private');
            $variation->save();

                ///-- Set variation properties
            
            $variation->set_regular_price($variation_data['Regular price']);
            $variation->set_sale_price($variation_data['Sale price']);
            $variation->set_date_on_sale_from($variation_data['Date sale price starts']);
            $variation->set_date_on_sale_to($variation_data['Date sale price ends']);
            $variation->set_sku($variation_data['SKU']);
            $variation->set_manage_stock($manageStock);
            $variation->set_stock_quantity($variation_data['Stock']);
            $variation->set_low_stock_amount($variation_data['Low stock amount']);
            $variation->set_backorders($variation_data['Backorders allowed?']);
            $variation->set_stock_status($variation_data['In stock?']);
            $variation->set_weight($variation_data['Weight (kg)']);
            $variation->set_length($variation_data['Length (cm)']);
            $variation->set_width($variation_data['Width (cm)']);
            $variation->set_height($variation_data['Height (cm)']);
            $variation->set_description($variation_data['Description']);
            $variation->set_virtual($variation_data['Virtual'] == '1');

                ///-- Save Shipping Class

            if ($variation_data['Shipping Class'] != '') {
                $shipping_class_slug = sanitize_title($variation_data['Shipping Class']); 
                $shipping_class = get_term_by('slug', $shipping_class_slug, 'product_shipping_class');

                if ($shipping_class && !is_wp_error($shipping_class)) {
                    $variation->set_shipping_class_id($shipping_class->term_id);
                } else {
                    BlueRaven_SendError("ERROR", "blue-raven/includes/third-party/class-blue-raven-woo-updater.php", "Variation Shipping class not found:", $variation_data['Shipping Class']);
                }
            } else {
                $parent_shipping_class_id = $variation->get_parent_data()['shipping_class_id'] ?? 0;
                $variation->set_shipping_class_id($parent_shipping_class_id);
            }

                ///-- Save variation
            
            $variation_id = $variation->save();

                ///-- Save Downloads
            
            if (isset($variation_data['Download limit']) && isset($variation_data['Download 1 name'])) {

                $download_limit = trim($variation_data['Download limit']) === '' ? -1 : (int) $variation_data['Download limit'];
                $download_expiry = trim($variation_data['Download expiry days']) === '' ? -1 : (int) $variation_data['Download expiry days'];

                $variation->set_download_limit($download_limit);
                $variation->set_download_expiry($download_expiry);
                $variation->set_downloadable(true);

                $downloads = array();
                
                $i = 1;
                while (isset($variation_data["Download {$i} ID"]) || isset($variation_data["Download {$i} URL"])) {
                    if (!empty($variation_data["Download {$i} ID"]) && !empty($variation_data["Download {$i} URL"])) $downloads[] = array('id'   => $variation_data["Download {$i} ID"],'name' => $variation_data["Download {$i} name"] ?? "Download {$i}",'file' => $variation_data["Download {$i} URL"]);
                    $i++;
                }
                
                if (!empty($downloads)) {
                    $variation->set_downloadable(true);
                    $variation->set_downloads($downloads);
                    $variation->save();
                } else {
                    $variation->set_downloadable(false);
                    $variation->save();
                }
            } else {
                $variation->set_downloadable(false);
                $variation->save();
            }             

                ///-- Handle variation image 
            
            if (!empty($variation_data['Images'])) {
                $image_url = $variation_data['Images'];
                $attachment_id = attachment_url_to_postid($image_url);
                
                if (!$attachment_id) {
                    require_once(ABSPATH . 'wp-admin/includes/media.php');
                    require_once(ABSPATH . 'wp-admin/includes/file.php');
                    require_once(ABSPATH . 'wp-admin/includes/image.php');
                    
                    $tmp = download_url($image_url);
                    
                    if (!is_wp_error($tmp)) {
                        $file_array = array('name' => basename($image_url),'tmp_name' => $tmp);
                        $attachment_id = media_handle_sideload($file_array, 0);
                        if (is_wp_error($attachment_id)) wp_delete_file($file_array['tmp_name']);
                    }
                }
                
                if ($attachment_id && !is_wp_error($attachment_id)) {
                    $variation->set_image_id($attachment_id);
                    $variation->save();
                }
            }
            
            $variationList[] = $variation->get_id();
        }
        
        $existing_variations = wc_get_products([
            'status' => ['publish', 'private'],
            'limit' => -1,
            'parent' => $product->get_id(),
            'type' => 'variation',
        ]);
        
        foreach ($existing_variations as $existing_variation) {
            if (!in_array($existing_variation->get_id(), $variationList)) wp_delete_post($existing_variation->get_id(), true);
        }
    }
}

/*
============================================================
    EXTRA FUNCTIONS 
============================================================
*/

    ## =========== Retrieves a WordPress attachment ID by matching the basename of a given image URL to the _wp_attached_file meta field. =========== ##

function get_attachment_id_by_url_basename($image_url) {
    $upload_dir = wp_upload_dir();
    $parsed_url = wp_parse_url($image_url);
    if (!isset($parsed_url['path'])) return false;
    $relative_path = ltrim(str_replace($upload_dir['baseurl'], '', $image_url), '/');
    $query = new WP_Query(['post_type'=> 'attachment','posts_per_page' => 1,'fields' => 'ids','meta_query' => [['key' => '_wp_attached_file','value' => $relative_path,'compare' => '=']]]);
    return (!empty($query->posts)) ? $query->posts[0] : false;
}

    ## =========== Returns a WC_Product_Variation object by SKU if it exists and belongs to the expected parent product ID; otherwise, returns null. =========== ##
    
function get_unique_variation_by_sku($sku, $expected_parent_id) {
    $product_id = wc_get_product_id_by_sku($sku);
    if (!$product_id) return null;
    $product = wc_get_product($product_id);
    if ($product instanceof WC_Product_Variation && $product->get_parent_id() === $expected_parent_id) { return $product; }
    return null; 
}

   ## =========== Parses a CSV string into an array of associative arrays using the first row as headers. =========== ##
    
function parse_variations_csv($csv_string) {
    $rows = [];
    $headers = [];
    
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Safe php://temp for in-memory CSV parsing
    $fp = fopen('php://temp', 'r+');
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Safe fwrite to php://temp memory stream
    fwrite($fp, $csv_string);
    rewind($fp);

    $headers = ($data = fgetcsv($fp)) !== false ? $data : [];
    while (($data = fgetcsv($fp)) !== false) $rows[] = array_combine($headers, $data);
	
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Safe fclose of php://temp memory stream
    fclose($fp);
    return $rows;
}

## =========== Builds variation attributes by matching input to defined attributes, preserving original casing when matched. =========== ##
    
function build_variation_attributes($variation_data, $attribute_array) {
    $variation_attributes = [];

    foreach ($variation_data as $key => $value) {
        if (preg_match('/^Attribute (\d+) name$/i', $key, $matches)) {
            $index = $matches[1];
            $name_key = "Attribute {$index} name";
            $value_key = "Attribute {$index} value(s)";
            $label = trim($variation_data[$name_key] ?? '');
            $input_option = trim($variation_data[$value_key] ?? '');

            if ($label !== '' && $input_option !== '') {
                $slug = sanitize_title($label); 
                $match = null;

                foreach ($attribute_array as $attr_def) {
                    if (sanitize_title($attr_def['name']) === $slug) {
                        
                            ///-- Match value by slug

                        foreach ($attr_def['values'] as $valid_option) {if (sanitize_title($valid_option) === sanitize_title($input_option)) { $match = $valid_option; break 2; }}
                    }
                }
                $variation_attributes["attribute_{$slug}"] = $match ?: $input_option;
            }
        }
    }

    return $variation_attributes;
}