<?php
/**
 * Abstract Product importer
 *
 * @package  WooCommerce\Import
 * @version  3.1.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

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

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_Importer_Interface', false ) ) {
    // Find WooCommerce's main directory and include the interface.
    $wc_plugin_dir = trailingslashit( WP_PLUGIN_DIR ) . 'woocommerce/';
    if ( file_exists( $wc_plugin_dir . 'includes/interfaces/class-wc-importer-interface.php' ) ) {
        include_once $wc_plugin_dir . 'includes/interfaces/class-wc-importer-interface.php';
    }
}

/**
 * WC_Product_Importer Class.
 */
abstract class WC_Product_Importer implements WC_Importer_Interface {

    /**
     * CSV file.
     *
     * @var string
     */
    protected $file = '';

    /**
     * The file position after the last read.
     *
     * @var int
     */
    protected $file_position = 0;

    /**
     * Importer parameters.
     *
     * @var array
     */
    protected $params = array();

    /**
     * Logger instance.
     *
     * @var WC_Logger
     */
    protected $logger = null;

    /**
     * Whether logging is enabled.
     *
     * @var bool
     */
    protected $logging_enabled = false;

    /**
     * Raw keys - CSV raw headers.
     *
     * @var array
     */
    protected $raw_keys = array();

    /**
     * Mapped keys - CSV headers.
     *
     * @var array
     */
    protected $mapped_keys = array();

    /**
     * Raw data.
     *
     * @var array
     */
    protected $raw_data = array();

    /**
     * Raw data.
     *
     * @var array
     */
    protected $file_positions = array();

    /**
     * Parsed data.
     *
     * @var array
     */
    protected $parsed_data = array();

    /**
     * Start time of current import.
     *
     * (default value: 0)
     *
     * @var int
     */
    protected $start_time = 0;

    /**
     * Get file raw headers.
     *
     * @return array
     */
    public function get_raw_keys() {
        return $this->raw_keys;
    }

    /**
     * Get file mapped headers.
     *
     * @return array
     */
    public function get_mapped_keys() {
        return ! empty( $this->mapped_keys ) ? $this->mapped_keys : $this->raw_keys;
    }

    /**
     * Get raw data.
     *
     * @return array
     */
    public function get_raw_data() {
        return $this->raw_data;
    }

    /**
     * Get parsed data.
     *
     * @return array
     */
    public function get_parsed_data() {
        /**
         * Filter product importer parsed data.
         *
         * @param array $parsed_data Parsed data.
         * @param WC_Product_Importer $importer Importer instance.
         */
        return apply_filters( 'wcvendors_product_importer_parsed_data', $this->parsed_data, $this );
    }

    /**
     * Get importer parameters.
     *
     * @return array
     */
    public function get_params() {
        return $this->params;
    }

    /**
     * Get file pointer position from the last read.
     *
     * @return int
     */
    public function get_file_position() {
        return $this->file_position;
    }

    /**
     * Get file pointer position as a percentage of file size.
     *
     * @return int
     */
    public function get_percent_complete() {
        $size = filesize( $this->file );
        if ( ! $size ) {
            return 0;
        }

        return absint( min( NumberUtil::round( ( $this->file_position / $size ) * 100 ), 100 ) );
    }

    /**
     * Prepare a single product for create or update.
     *
     * @param  array $data     Item data.
     * @return WC_Product|WP_Error
     */
    protected function get_product_object( $data ) {
        $id                    = isset( $data['id'] ) ? absint( $data['id'] ) : 0;
        $product_type_settings = get_option( 'wcvendors_capability_product_types', array() );

        // Type is the most important part here because we need to be using the correct class and methods.
        if ( isset( $data['type'] ) ) {

            // Check that the vendor has permission for this kind of product type.
            if ( in_array( $data['type'], $product_type_settings, true ) ) {
                return new WP_Error( 'wcvendors_product_importer_no_permission', __( 'You do not have permission to import this product type.', 'wcvendors-pro' ), array( 'status' => 401 ) );
            }

            // Check if the product type exists in WooCommerce.
            if ( ! array_key_exists( $data['type'], WC_Admin_Exporters::get_product_types() ) ) {
                return new WP_Error( 'wcvendors_product_importer_invalid_type', __( 'Invalid product type.', 'wcvendors-pro' ), array( 'status' => 401 ) );
            }

            try {
                // Prevent getting "variation_invalid_id" error message from Variation Data Store.
                if ( 'variation' === $data['type'] ) {
                    $id = wp_update_post(
                        array(
                            'ID'        => $id,
                            'post_type' => 'product_variation',
                        )
                    );
                }

                $product = wc_get_product_object( $data['type'], $id );
            } catch ( WC_Data_Exception $e ) {
                return new WP_Error( 'wcvendors_product_csv_importer_' . $e->getErrorCode(), $e->getMessage(), array( 'status' => 401 ) );
            }
        } elseif ( ! empty( $data['id'] ) ) {
            $product = wc_get_product( $id );

            if ( ! $product ) {
                return new WP_Error(
                    'wcvendors_product_csv_importer_invalid_id',
                    /* translators: %d: product ID */
                    sprintf( __( 'Invalid product ID %d.', 'wcvendors-pro' ), $id ),
                    array(
                        'id'     => $id,
                        'status' => 401,
                    )
                );
            }
        } else {
            $product = wc_get_product_object( 'simple', $id );
        }

        return apply_filters( 'wcvendors_product_import_get_product_object', $product, $data );
    }

    /**
     * Find existing variation by parent ID and attributes
     *
     * @param int   $parent_id Parent product ID.
     * @param array $attributes Variation attributes.
     * @return int|false Variation ID if found, false otherwise.
     */
    protected function find_existing_variation( $parent_id, $attributes ) {
        global $wpdb;

        if ( empty( $attributes ) ) {
            return false;
        }

        $this->log(
            sprintf(
                'Checking for existing variation of parent ID %d with attributes: %s',
                $parent_id,
                print_r( $attributes, true ) // phpcs:ignore
            )
        );

        // Get all variations for this parent.
        $variation_ids = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT ID FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation'",
                $parent_id
            )
        );

        if ( empty( $variation_ids ) ) {
            $this->log( 'No existing variations found for this parent product' );
            return false;
        }

        $this->log( sprintf( 'Found %d variations to check for parent ID %d', count( $variation_ids ), $parent_id ) );

        // Check each variation to see if attributes match.
        foreach ( $variation_ids as $variation_id ) {
            $variation = wc_get_product( $variation_id );
            if ( ! $variation ) {
                continue;
            }

            $existing_attributes = $variation->get_attributes();

            // If all attributes match, we found our variation.
            if ( $this->attributes_match( $attributes, $existing_attributes ) ) {
                $this->log( sprintf( 'Found matching variation ID %d with attributes', $variation_id ) );
                return $variation_id;
            }
        }

        $this->log( 'No matching variation found with these attributes' );
        return false;
    }

    /**
     * Check if two sets of attributes match
     *
     * @param array $attrs1 First set of attributes.
     * @param array $attrs2 Second set of attributes.
     * @return bool True if attributes match.
     */
    protected function attributes_match( $attrs1, $attrs2 ) {
        // If attribute count doesn't match, they're different.
        if ( count( $attrs1 ) !== count( $attrs2 ) ) {
            return false;
        }

        foreach ( $attrs1 as $key => $value ) {
            if ( ! isset( $attrs2[ $key ] ) || $attrs2[ $key ] !== $value ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Extract variation attributes from raw attributes
     *
     * @param array      $raw_attributes Raw attributes from import data.
     * @param WC_Product $parent_product Parent product.
     * @return array Prepared variation attributes.
     */
    protected function prepare_variation_attributes( $raw_attributes, $parent_product ) {
        $variation_attributes = array();
        $parent_attributes    = $parent_product->get_attributes();

        foreach ( $raw_attributes as $attribute ) {
            $attribute_id = 0;

            // Get ID if is a global attribute.
            if ( ! empty( $attribute['taxonomy'] ) ) {
                $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
            }

            if ( $attribute_id ) {
                $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
            } else {
                $attribute_name = sanitize_title( $attribute['name'] );
            }

            if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
                continue;
            }

            $attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
            $attribute_value = isset( $attribute['value'] ) ? current( $attribute['value'] ) : '';

            if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
                // If dealing with a taxonomy, we need to get the slug from the name posted to the API.
                $term = get_term_by( 'name', $attribute_value, $attribute_name );

                if ( $term && ! is_wp_error( $term ) ) {
                    $attribute_value = $term->slug;
                } else {
                    $attribute_value = sanitize_title( $attribute_value );
                }
            }

            $variation_attributes[ $attribute_key ] = $attribute_value;
        }

        return $variation_attributes;
    }

    /**
     * Process a single item and save.
     *
     * @throws Exception If item cannot be processed.
     * @param  array $data Raw CSV data.
     * @return array|WP_Error
     */
    protected function process_item( $data ) {
        try {

            $this->log( sprintf( 'Starting to process item: %s', isset( $data['name'] ) ? $data['name'] : 'Unknown name' ) );

            do_action( 'wcvendors_product_import_before_process_item', $data );
            $data = apply_filters( 'wcvendors_product_import_process_item_data', $data );

            // Get product ID from SKU if created during the importation.
            if ( empty( $data['id'] ) && ! empty( $data['sku'] ) ) {
                $product_id = $this->get_product_id_by_sku( $data['sku'] );

                if ( $product_id ) {
                    $data['id'] = $product_id;
                    $this->log( sprintf( 'Found existing product ID %d for SKU %s', $product_id, $data['sku'] ) );
                }
            }

            // Check if this is a variation and if so, try to find an existing one.
            if ( isset( $data['type'] ) && 'variation' === $data['type'] && isset( $data['parent_id'] ) ) {

                $parent = wc_get_product( $data['parent_id'] );

                if ( $parent && isset( $data['raw_attributes'] ) ) {
                    // Extract variation attributes.
                    $variation_attributes = $this->prepare_variation_attributes( $data['raw_attributes'], $parent );

                    // Check if variation already exists.
                    $existing_variation_id = $this->find_existing_variation( $data['parent_id'], $variation_attributes );

                    if ( $existing_variation_id ) {
                        $this->log(
                            sprintf(
                                'Found existing variation ID %d for parent %d with matching attributes - updating instead of creating new',
                                $existing_variation_id,
                                $data['parent_id']
                            )
                        );

                        // Set the ID so we update instead of create.
                        $data['id'] = $existing_variation_id;
                    }
                }
            }

            $object   = $this->get_product_object( $data );
            $updating = false;
            $type     = $object->get_type();

            if ( is_wp_error( $object ) ) {
                $this->log( sprintf( 'Error getting product object: %s', $object->get_error_message() ), 'error' );
                return $object;
            }

            if ( $object->get_id() && 'importing' !== $object->get_status() ) {
                $updating = true;
                $this->log( sprintf( 'Updating existing product ID %d', $object->get_id() ) );
            } else {
                $this->log( 'Creating new product with name: ' . ( isset( $data['name'] ) ? $data['name'] : 'Unknown' ) );
            }

            if ( 'external' === $object->get_type() ) {
                unset( $data['manage_stock'], $data['stock_status'], $data['backorders'], $data['low_stock_amount'] );
            }
            $is_variation = false;
            if ( 'variation' === $type ) {
                if ( isset( $data['status'] ) && -1 === $data['status'] ) {
                    $data['status'] = 0; // Variations cannot be drafts - set to private.
                }
                $is_variation = true;
                $this->log( 'Processing product variation' );
            }

            if ( 'importing' === $object->get_status() ) {
                $object->set_status( 'publish' );
                $object->set_slug( '' );
            }

            $result = $object->set_props( array_diff_key( $data, array_flip( array( 'meta_data', 'raw_image_id', 'raw_gallery_image_ids', 'raw_attributes' ) ) ) );

            if ( is_wp_error( $result ) ) {
                $this->log( sprintf( 'Error setting product properties: %s', $result->get_error_message() ), 'error' );
                throw new Exception( $result->get_error_message() );
            }

            if ( 'variation' === $type ) {
                $this->set_variation_data( $object, $data );
            } else {
                $this->set_product_data( $object, $data );
            }

            $this->set_image_data( $object, $data );
            $this->set_meta_data( $object, $data );

            $object = apply_filters( 'wcvendors_product_import_pre_insert_product_object', $object, $data );
            $object->save();

            do_action( 'wcvendors_product_import_inserted_product_object', $object, $data );

            $this->log( sprintf( 'Successfully processed %s ID %d - %s', 'variation' === $type ? 'variation' : 'product', $object->get_id(), $object->get_name() ) );

            return array(
                'id'           => $object->get_id(),
                'updated'      => $updating,
                'is_variation' => $is_variation,
            );
        } catch ( Exception $e ) {
            $this->log( sprintf( 'Error processing product: %s', $e->getMessage() ), 'error' );
            return new WP_Error( 'wcvendors_product_importer_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
        }
    }

    /**
     * Convert raw image URLs to IDs and set.
     *
     * @param WC_Product $product Product instance.
     * @param array      $data    Item data.
     */
    protected function set_image_data( &$product, $data ) {
        // Image URLs need converting to IDs before inserting.
        if ( isset( $data['raw_image_id'] ) ) {
            $product->set_image_id( $this->get_attachment_id_from_url( $data['raw_image_id'], $product->get_id() ) );
        }

        // Gallery image URLs need converting to IDs before inserting.
        if ( isset( $data['raw_gallery_image_ids'] ) ) {
            $gallery_image_ids = array();

            foreach ( $data['raw_gallery_image_ids'] as $image_id ) {
                $gallery_image_ids[] = $this->get_attachment_id_from_url( $image_id, $product->get_id() );
            }
            $product->set_gallery_image_ids( $gallery_image_ids );
        }
    }

    /**
     * Append meta data.
     *
     * @param WC_Product $product Product instance.
     * @param array      $data    Item data.
     */
    protected function set_meta_data( &$product, $data ) {
        if ( isset( $data['meta_data'] ) ) {
            foreach ( $data['meta_data'] as $meta ) {
                $product->update_meta_data( $meta['key'], $meta['value'] );
            }
        }
    }

    /**
     * Set product data.
     *
     * @param WC_Product $product Product instance.
     * @param array      $data    Item data.
     * @throws Exception If data cannot be set.
     */
    protected function set_product_data( &$product, $data ) {
        if ( isset( $data['raw_attributes'] ) ) {
            $attributes          = array();
            $default_attributes  = array();
            $existing_attributes = $product->get_attributes();

            foreach ( $data['raw_attributes'] as $position => $attribute ) {
                $attribute_id = 0;

                // Get ID if is a global attribute.
                if ( ! empty( $attribute['taxonomy'] ) ) {
                    $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
                }

                // Set attribute visibility.
                if ( isset( $attribute['visible'] ) ) {
                    $is_visible = $attribute['visible'];
                } else {
                    $is_visible = 1;
                }

                // Get name.
                $attribute_name = $attribute_id ? wc_attribute_taxonomy_name_by_id( $attribute_id ) : $attribute['name'];

                // Set if is a variation attribute based on existing attributes if possible so updates via CSV do not change this.
                $is_variation = 0;

                if ( $existing_attributes ) {
                    foreach ( $existing_attributes as $existing_attribute ) {
                        if ( $existing_attribute->get_name() === $attribute_name ) {
                            $is_variation = $existing_attribute->get_variation();
                            break;
                        }
                    }
                }

                if ( $attribute_id ) {
                    if ( isset( $attribute['value'] ) ) {
                        $options = array_map( 'wc_sanitize_term_text_based', $attribute['value'] );
                        $options = array_filter( $options, 'strlen' );
                    } else {
                        $options = array();
                    }

                    // Check for default attributes and set "is_variation".
                    if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $options, true ) ) {
                        $default_term = get_term_by( 'name', $attribute['default'], $attribute_name );

                        if ( $default_term && ! is_wp_error( $default_term ) ) {
                            $default = $default_term->slug;
                        } else {
                            $default = sanitize_title( $attribute['default'] );
                        }

                        $default_attributes[ $attribute_name ] = $default;
                        $is_variation                          = 1;
                    }

                    if ( ! empty( $options ) ) {
                        $attribute_object = new WC_Product_Attribute();
                        $attribute_object->set_id( $attribute_id );
                        $attribute_object->set_name( $attribute_name );
                        $attribute_object->set_options( $options );
                        $attribute_object->set_position( $position );
                        $attribute_object->set_visible( $is_visible );
                        $attribute_object->set_variation( $is_variation );
                        $attributes[] = $attribute_object;
                    }
                } elseif ( isset( $attribute['value'] ) ) {
                    // Check for default attributes and set "is_variation".
                    if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $attribute['value'], true ) ) {
                        $default_attributes[ sanitize_title( $attribute['name'] ) ] = $attribute['default'];
                        $is_variation = 1;
                    }

                    $attribute_object = new WC_Product_Attribute();
                    $attribute_object->set_name( $attribute['name'] );
                    $attribute_object->set_options( $attribute['value'] );
                    $attribute_object->set_position( $position );
                    $attribute_object->set_visible( $is_visible );
                    $attribute_object->set_variation( $is_variation );
                    $attributes[] = $attribute_object;
                }
            }

            $product->set_attributes( $attributes );

            // Set variable default attributes.
            if ( $product->is_type( 'variable' ) ) {
                $product->set_default_attributes( $default_attributes );
            }
        }
    }

    /**
     * Set variation data.
     *
     * @param WC_Product $variation Product instance.
     * @param array      $data    Item data.
     * @return WC_Product|WP_Error
     * @throws Exception If data cannot be set.
     */
    protected function set_variation_data( &$variation, $data ) {
        $parent = false;

        // Check if parent exist.
        if ( isset( $data['parent_id'] ) ) {
            $parent = wc_get_product( $data['parent_id'] );

            if ( $parent ) {
                $variation->set_parent_id( $parent->get_id() );
            }
        }

        // Stop if parent does not exists.
        if ( ! $parent ) {
            $this->log( 'Variation cannot be imported: Missing parent ID or parent does not exist yet.', 'error' );
            return new WP_Error( 'wcvendors_product_importer_missing_variation_parent_id', __( 'Variation cannot be imported: Missing parent ID or parent does not exist yet.', 'wcvendors-pro' ), array( 'status' => 401 ) );
        }

        // Stop if parent is a product variation.
        if ( $parent->is_type( 'variation' ) ) {
            $this->log( 'Variation cannot be imported: Parent product cannot be a product variation', 'error' );
            return new WP_Error( 'wcvendors_product_importer_parent_set_as_variation', __( 'Variation cannot be imported: Parent product cannot be a product variation', 'wcvendors-pro' ), array( 'status' => 401 ) );
        }

        if ( isset( $data['raw_attributes'] ) ) {
            $attributes        = array();
            $parent_attributes = $this->get_variation_parent_attributes( $data['raw_attributes'], $parent );

            foreach ( $data['raw_attributes'] as $attribute ) {
                $attribute_id = 0;

                // Get ID if is a global attribute.
                if ( ! empty( $attribute['taxonomy'] ) ) {
                    $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
                }

                if ( $attribute_id ) {
                    $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
                } else {
                    $attribute_name = sanitize_title( $attribute['name'] );
                }

                if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
                    continue;
                }

                $attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
                $attribute_value = isset( $attribute['value'] ) ? current( $attribute['value'] ) : '';

                if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
                    // If dealing with a taxonomy, we need to get the slug from the name posted to the API.
                    $term = get_term_by( 'name', $attribute_value, $attribute_name );

                    if ( $term && ! is_wp_error( $term ) ) {
                        $attribute_value = $term->slug;
                    } else {
                        $attribute_value = sanitize_title( $attribute_value );
                    }
                }

                $attributes[ $attribute_key ] = $attribute_value;
            }

            $variation->set_attributes( $attributes );
        }
    }

    /**
     * Get variation parent attributes and set "is_variation".
     *
     * @param  array      $attributes Attributes list.
     * @param  WC_Product $parent_product     Parent product data.
     * @return array
     */
    protected function get_variation_parent_attributes( $attributes, $parent_product ) {
        $parent_attributes = $parent_product->get_attributes();
        $require_save      = false;

        foreach ( $attributes as $attribute ) {
            $attribute_id = 0;

            // Get ID if is a global attribute.
            if ( ! empty( $attribute['taxonomy'] ) ) {
                $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
            }

            if ( $attribute_id ) {
                $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
            } else {
                $attribute_name = sanitize_title( $attribute['name'] );
            }

            // Check if attribute handle variations.
            if ( isset( $parent_attributes[ $attribute_name ] ) && ! $parent_attributes[ $attribute_name ]->get_variation() ) {
                // Re-create the attribute to CRUD save and generate again.
                $parent_attributes[ $attribute_name ] = clone $parent_attributes[ $attribute_name ];
                $parent_attributes[ $attribute_name ]->set_variation( 1 );

                $require_save = true;
            }
        }

        // Save variation attributes.
        if ( $require_save ) {
            $parent_product->set_attributes( array_values( $parent_attributes ) );
            $parent_product->save();
        }

        return $parent_attributes;
    }

    /**
     * Get attachment ID.
     *
     * @param  string $url        Attachment URL.
     * @param  int    $product_id Product ID.
     * @return int
     * @throws Exception If attachment cannot be loaded.
     */
    public function get_attachment_id_from_url( $url, $product_id ) {
        if ( empty( $url ) ) {
            return 0;
        }

        $this->log( sprintf( 'Processing image URL: %s for product ID: %d', $url, $product_id ) );

        $id         = 0;
        $upload_dir = wp_upload_dir( null, false );
        $base_url   = $upload_dir['baseurl'] . '/';

        // Check first if attachment is inside the WordPress uploads directory, or we're given a filename only.
        if ( false !== strpos( $url, $base_url ) || false === strpos( $url, '://' ) ) {
            // Search for yyyy/mm/slug.extension or slug.extension - remove the base URL.
            $file = str_replace( $base_url, '', $url );
            $args = array(
                'post_type'   => 'attachment',
                'post_status' => 'any',
                'fields'      => 'ids',
				'meta_query'  => array( // @codingStandardsIgnoreLine.
                    'relation' => 'OR',
                    array(
                        'key'     => '_wp_attached_file',
                        'value'   => '^' . $file,
                        'compare' => 'REGEXP',
                    ),
                    array(
                        'key'     => '_wp_attached_file',
                        'value'   => '/' . $file,
                        'compare' => 'LIKE',
                    ),
                    array(
                        'key'     => '_wc_attachment_source',
                        'value'   => '/' . $file,
                        'compare' => 'LIKE',
                    ),
                ),
            );
        } else {
            // This is an external URL, so compare to source.
            $args = array(
                'post_type'   => 'attachment',
                'post_status' => 'any',
                'fields'      => 'ids',
				'meta_query'  => array( // @codingStandardsIgnoreLine.
                    array(
                        'value' => $url,
                        'key'   => '_wc_attachment_source',
                    ),
                ),
            );
        }

		$ids = get_posts( $args ); // @codingStandardsIgnoreLine.

        if ( $ids ) {
            $id = current( $ids );
            $this->log( sprintf( 'Found existing attachment ID: %d for image: %s', $id, $url ) );
        }

        // Upload if attachment does not exists.
        if ( ! $id && stristr( $url, '://' ) ) {
            $this->log( sprintf( 'Uploading image from URL: %s', $url ) );
            $upload = wc_rest_upload_image_from_url( $url );

            if ( is_wp_error( $upload ) ) {
                $this->log( sprintf( 'Error uploading image: %s', $upload->get_error_message() ), 'error' );
                throw new Exception( esc_html( $upload->get_error_message() ), 400 );
            }

            $id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_id );

            if ( ! wp_attachment_is_image( $id ) ) {
                $this->log( sprintf( 'Uploaded file is not a valid image: %s', $url ), 'error' );
                /* translators: %s: image URL */
                throw new Exception( sprintf( esc_html__( 'Not able to attach "%s".', 'wcvendors-pro' ), esc_html( $url ) ), 400 );
            }

            // Save attachment source for future reference.
            update_post_meta( $id, '_wc_attachment_source', $url );
            $this->log( sprintf( 'Successfully uploaded image and created attachment ID: %d', $id ) );
        }

        if ( ! $id ) {
            $this->log( sprintf( 'Unable to use image: %s', $url ), 'error' );
            /* translators: %s: image URL */
            throw new Exception( sprintf( esc_html__( 'Unable to use image "%s".', 'wcvendors-pro' ), esc_html( $url ) ), 400 );
        }

        return $id;
    }

    /**
     * Get attribute taxonomy ID from the imported data.
     * If does not exists register a new attribute.
     *
     * @param  string $raw_name Attribute name.
     * @return int
     * @throws Exception If taxonomy cannot be loaded.
     */
    public function get_attribute_taxonomy_id( $raw_name ) {
        global $wpdb, $wc_product_attributes;

        $this->log( sprintf( 'Processing attribute: %s', $raw_name ) );

        // These are exported as labels, so convert the label to a name if possible first.
        $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' );
        $attribute_name   = array_search( $raw_name, $attribute_labels, true );

        if ( ! $attribute_name ) {
            $attribute_name = wc_sanitize_taxonomy_name( $raw_name );
            $this->log( sprintf( 'Sanitized attribute name: %s', $attribute_name ) );
        }

        $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );

        // Get the ID from the name.
        if ( $attribute_id ) {
            $this->log( sprintf( 'Found existing attribute ID: %d for %s', $attribute_id, $raw_name ) );
            return $attribute_id;
        }

        $this->log( sprintf( 'Creating new attribute: %s', $raw_name ) );

        // If the attribute does not exist, create it.
        $attribute_id = wc_create_attribute(
            array(
                'name'         => $raw_name,
                'slug'         => $attribute_name,
                'type'         => 'select',
                'order_by'     => 'menu_order',
                'has_archives' => false,
            )
        );

        if ( is_wp_error( $attribute_id ) ) {
            $this->log( sprintf( 'Error creating attribute: %s', $attribute_id->get_error_message() ), 'error' );
            throw new Exception( esc_html( $attribute_id->get_error_message() ), 400 );
        }

        $this->log( sprintf( 'Successfully created attribute ID: %d', $attribute_id ) );

        // Register as taxonomy while importing.
        $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name );
        register_taxonomy(
            $taxonomy_name,
            apply_filters( 'wcvendors_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ),
            apply_filters(
                'wcvendors_taxonomy_args_' . $taxonomy_name,
                array(
                    'labels'       => array(
                        'name' => $raw_name,
                    ),
                    'hierarchical' => true,
                    'show_ui'      => false,
                    'query_var'    => true,
                    'rewrite'      => false,
                )
            )
        );

        // Set product attributes global.
        $wc_product_attributes = array();

        foreach ( wc_get_attribute_taxonomies() as $taxonomy ) {
            $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy;
        }

        return $attribute_id;
    }

    /**
     * Memory exceeded
     *
     * Ensures the batch process never exceeds 90%
     * of the maximum WordPress memory.
     *
     * @return bool
     */
    protected function memory_exceeded() {
        $memory_limit   = $this->get_memory_limit() * 0.9; // 90% of max memory
        $current_memory = memory_get_usage( true );
        $return         = false;
        if ( $current_memory >= $memory_limit ) {
            $return = true;
        }
        return apply_filters( 'wcvendors_product_importer_memory_exceeded', $return );
    }

    /**
     * Get memory limit
     *
     * @return int
     */
    protected function get_memory_limit() {
        if ( function_exists( 'ini_get' ) ) {
            $memory_limit = ini_get( 'memory_limit' );
        } else {
            // Sensible default.
            $memory_limit = '128M';
        }

        if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
            // Unlimited, set to 32GB.
            $memory_limit = '32000M';
        }
        return intval( $memory_limit ) * 1024 * 1024;
    }

    /**
     * Time exceeded.
     *
     * Ensures the batch never exceeds a sensible time limit.
     * A timeout limit of 30s is common on shared hosting.
     *
     * @return bool
     */
    protected function time_exceeded() {
        $finish = $this->start_time + apply_filters( 'wcvendors_product_importer_default_time_limit', 20 ); // 20 seconds
        $return = false;
        if ( time() >= $finish ) {
            $return = true;
        }
        return apply_filters( 'wcvendors_product_importer_time_exceeded', $return );
    }

    /**
     * Explode CSV cell values using commas by default, and handling escaped
     * separators.
     *
     * @since  3.2.0
     * @param  string $value     Value to explode.
     * @param  string $separator Separator separating each value. Defaults to comma.
     * @return array
     */
    protected function explode_values( $value, $separator = ',' ) {
        $value  = str_replace( '\\,', '::separator::', $value );
        $values = explode( $separator, $value );
        $values = array_map( array( $this, 'explode_values_formatter' ), $values );

        return $values;
    }

    /**
     * Remove formatting and trim each value.
     *
     * @since  3.2.0
     * @param  string $value Value to format.
     * @return string
     */
    protected function explode_values_formatter( $value ) {
        return trim( str_replace( '::separator::', ',', $value ) );
    }

    /**
     * The exporter prepends a ' to escape fields that start with =, +, - or @.
     * Remove the prepended ' character preceding those characters.
     *
     * @since 3.5.2
     * @param  string $value A string that may or may not have been escaped with '.
     * @return string
     */
    protected function unescape_data( $value ) {
        $active_content_triggers = array( "'=", "'+", "'-", "'@" );

        if ( in_array( mb_substr( $value, 0, 2 ), $active_content_triggers, true ) ) {
            $value = mb_substr( $value, 1 );
        }

        return $value;
    }

    /**
     * Helper function that safely gets product ID by SKU
     *
     * @param string $sku Product SKU.
     * @return int|bool Product ID if found, false otherwise.
     */
    protected function get_product_id_by_sku( $sku ) {
        // First try the standard WooCommerce function.
        $product_id = wc_get_product_id_by_sku( $sku );

        // If that fails, try a direct database lookup.
        if ( ! $product_id ) {
            global $wpdb;
            $product_id = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_sku' AND meta_value=%s LIMIT 1",
                    $sku
                )
            );
        }

        return $product_id ? absint( $product_id ) : false;
    }

    /**
     * Set logger.
     *
     * @param WC_Logger $logger Logger.
     *
     * @since 1.9.7
     */
    public function set_logger( $logger ) {
        $this->logger = $logger;
    }

    /**
     * Set logging enabled.
     *
     * @param bool $logging_enabled Logging enabled.
     *
     * @since 1.9.7
     */
    public function set_logging_enabled( $logging_enabled ) {
        $this->logging_enabled = $logging_enabled;
    }


    /**
     * Log a message
     *
     * @since 1.9.7
     * @param string $message The message to log.
     * @param string $level Optional. The log level. Default 'info'. Options: emergency|alert|critical|error|warning|notice|info|debug.
     * @return void
     */
    protected function log( $message, $level = 'info' ) {
        if ( ! $this->logging_enabled || ! $this->logger ) {
            return;
        }

        $this->logger->log( $level, $message, array( 'source' => 'wcvendors-pro-product-importer-vendor-' . get_current_user_id() ) );
    }
}
