<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 */

namespace PrestaShop\PrestaShop\Adapter\Import\Handler;

use Address;
use Category;
use Doctrine\DBAL\Connection;
use Feature;
use FeatureValue;
use Image;
use Manufacturer;
use Module;
use PrestaShop\PrestaShop\Adapter\Configuration;
use PrestaShop\PrestaShop\Adapter\Database;
use PrestaShop\PrestaShop\Adapter\Import\ImageCopier;
use PrestaShop\PrestaShop\Adapter\Import\ImportDataFormatter;
use PrestaShop\PrestaShop\Adapter\Tools;
use PrestaShop\PrestaShop\Adapter\Validate;
use PrestaShop\PrestaShop\Core\Cache\Clearer\CacheClearerInterface;
use PrestaShop\PrestaShop\Core\Import\Configuration\ImportConfigInterface;
use PrestaShop\PrestaShop\Core\Import\Configuration\ImportRuntimeConfigInterface;
use PrestaShop\PrestaShop\Core\Import\Entity;
use PrestaShop\PrestaShop\Core\Import\File\DataRow\DataRowInterface;
use Product;
use ProductDownload;
use ProductSupplier;
use Psr\Log\LoggerInterface;
use Shop;
use SpecificPrice;
use StockAvailable;
use StockManagerFactory;
use Supplier;
use Symfony\Contracts\Translation\TranslatorInterface;
use Tag;
use TaxManagerFactory;
use TaxRulesGroup;
use Warehouse;
use WarehouseProductLocation;

/**
 * Class ProductImportHandler is responsible for product import.
 */
final class ProductImportHandler extends AbstractImportHandler
{
    /**
     * @var Connection database connection
     */
    private $connection;

    /**
     * @var string product database table name
     */
    private $productTable;

    /**
     * @var string accessory database table name
     */
    private $accessoryTable;

    /**
     * @var Address
     */
    private $shopAddress;

    /**
     * @var Tools
     */
    private $tools;

    /**
     * @var ImageCopier
     */
    private $imageCopier;

    /**
     * @param ImportDataFormatter $dataFormatter
     * @param array $allShopIds
     * @param array $contextShopIds
     * @param int $currentContextShopId
     * @param bool $isMultistoreEnabled
     * @param int $contextLanguageId
     * @param TranslatorInterface $translator
     * @param LoggerInterface $logger
     * @param int $employeeId
     * @param Database $legacyDatabase
     * @param CacheClearerInterface $cacheClearer
     * @param Connection $connection
     * @param string $dbPrefix
     * @param Configuration $configuration
     * @param Address $shopAddress
     * @param Validate $validate
     * @param Tools $tools
     * @param ImageCopier $imageCopier
     */
    public function __construct(
        ImportDataFormatter $dataFormatter,
        array $allShopIds,
        array $contextShopIds,
        $currentContextShopId,
        $isMultistoreEnabled,
        $contextLanguageId,
        TranslatorInterface $translator,
        LoggerInterface $logger,
        $employeeId,
        Database $legacyDatabase,
        CacheClearerInterface $cacheClearer,
        Connection $connection,
        $dbPrefix,
        Configuration $configuration,
        Address $shopAddress,
        Validate $validate,
        Tools $tools,
        ImageCopier $imageCopier
    ) {
        parent::__construct(
            $dataFormatter,
            $allShopIds,
            $contextShopIds,
            $currentContextShopId,
            $isMultistoreEnabled,
            $contextLanguageId,
            $translator,
            $logger,
            $employeeId,
            $legacyDatabase,
            $cacheClearer,
            $configuration,
            $validate
        );

        $this->connection = $connection;
        $this->productTable = $dbPrefix . 'product';
        $this->accessoryTable = $dbPrefix . 'accessory';
        $this->defaultValues = [
            'id_category' => [$this->configuration->getInt('PS_HOME_CATEGORY')],
            'id_category_default' => null,
            'active' => '1',
            'width' => 0.000000,
            'height' => 0.000000,
            'depth' => 0.000000,
            'weight' => 0.000000,
            'visibility' => 'both',
            'additional_shipping_cost' => 0.00,
            'unit_price' => 0,
            'quantity' => 0,
            'minimal_quantity' => 1,
            'low_stock_threshold' => null,
            'low_stock_alert' => false,
            'price' => 0,
            'id_tax_rules_group' => 0,
            'description_short' => [$this->defaultLanguageId => ''],
            'link_rewrite' => [$this->defaultLanguageId => ''],
            'online_only' => 0,
            'condition' => 'new',
            'available_date' => date('Y-m-d'),
            'date_add' => date('Y-m-d H:i:s'),
            'date_upd' => date('Y-m-d H:i:s'),
            'customizable' => 0,
            'uploadable_files' => 0,
            'text_fields' => 0,
            'advanced_stock_management' => 0,
            'depends_on_stock' => 0,
            'is_virtual' => 0,
        ];
        $this->shopAddress = $shopAddress;
        $this->tools = $tools;
        $this->imageCopier = $imageCopier;
        $this->importTypeLabel = $this->translator->trans('Products', [], 'Admin.Global');
    }

    /**
     * {@inheritdoc}
     */
    public function setUp(ImportConfigInterface $importConfig, ImportRuntimeConfigInterface $runtimeConfig)
    {
        parent::setUp($importConfig, $runtimeConfig);

        if (!defined('PS_MASS_PRODUCT_CREATION')) {
            define('PS_MASS_PRODUCT_CREATION', true);
        }

        if (!$runtimeConfig->shouldValidateData()) {
            Module::setBatchMode(true);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function importRow(
        ImportConfigInterface $importConfig,
        ImportRuntimeConfigInterface $runtimeConfig,
        DataRowInterface $dataRow
    ) {
        parent::importRow($importConfig, $runtimeConfig, $dataRow);

        $entityFields = $runtimeConfig->getEntityFields();
        $productId = $this->fetchProductId(
            $dataRow,
            $runtimeConfig->getEntityFields(),
            $importConfig->matchReferences()
        );
        $productName = $this->fetchDataValueByKey($dataRow, $entityFields, 'name');
        $product = new Product($productId);

        $this->loadStock($product);
        $this->setDefaultValues($product);
        $this->fillEntityData($product, $entityFields, $dataRow, $this->languageId);
        $this->loadShops($product, $importConfig, $productName);
        $this->loadTaxes($product);
        $this->loadManufacturer($product, false);
        $this->loadSupplier($product, false);
        $this->loadPrice($product);
        $this->loadCategory($product, false);
        $this->loadMetaData($product, $importConfig);
        $this->fixFloatValues($product);

        $productExistsById = $this->entityExists($product, 'product');
        $productExistsByReference = $importConfig->matchReferences() &&
            $product->reference &&
            $product->existsRefInDatabase($product->reference)
        ;

        if ($productExistsByReference || $productExistsById) {
            $product->date_upd = date('Y-m-d H:i:s');
        }

        $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');
        $fieldsError = $product->validateFields($unfriendlyError, true);
        $langFieldsError = $product->validateFieldsLang($unfriendlyError, true);
        $isValid = true === $fieldsError && true === $langFieldsError;

        if ($isValid) {
            $productSaved = $this->loadProductData(
                $product,
                $importConfig,
                $productExistsById,
                $productExistsByReference,
                $runtimeConfig->shouldValidateData(),
                $dataRow,
                $entityFields
            );

            if (!$productSaved) {
                $productId = $this->fetchDataValueByKey($dataRow, $entityFields, 'id');

                $this->error(sprintf(
                    $this->translator->trans('%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification'),
                    !empty($productName) ? $this->tools->sanitize($productName) : 'No Name',
                    !empty($productId) ? $this->tools->sanitize($productId) : 'No ID'
                ));

                $this->error($fieldsError . $langFieldsError . $this->legacyDatabase->getErrorMessage());
            } else {
                if (!$runtimeConfig->shouldValidateData()) {
                    $this->saveProductSupplier($product);
                    $this->saveProductTags($product, $importConfig, $productName);
                    $this->saveProductImages($product, $importConfig);
                    $this->saveFeatures($product, $importConfig);
                }

                $this->saveSpecificPrice(
                    $product,
                    $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_price'),
                    $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_percent'),
                    $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_from'),
                    $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_to'),
                    $runtimeConfig->shouldValidateData(),
                    $productName
                );
                $this->updateAdditionalData($product, $runtimeConfig->shouldValidateData());
                $this->saveStock(
                    $product,
                    $runtimeConfig->shouldValidateData(),
                    $productExistsById || $productExistsByReference
                );

                $this->linkAccessories($product, $runtimeConfig);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function tearDown(ImportConfigInterface $importConfig, ImportRuntimeConfigInterface $runtimeConfig)
    {
        parent::tearDown($importConfig, $runtimeConfig);

        if ($runtimeConfig->isFinished() && !$runtimeConfig->shouldValidateData()) {
            $this->importAccessories($runtimeConfig);
        }

        if (!$runtimeConfig->shouldValidateData()) {
            Module::processDeferedFuncCall();
            Module::processDeferedClearCache();
            Tag::updateTagCount();
        }
    }

    /**
     * Legacy logic to create category.
     * This method is internally called by legacy Category::searchByPath(), so it has to be public.
     *
     * @param int $defaultLanguageId
     * @param string $categoryName
     * @param int|null $parentCategoryId
     */
    public function createCategory($defaultLanguageId, $categoryName, $parentCategoryId = null)
    {
        $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');
        $homeCategoryId = $this->configuration->getInt('PS_HOME_CATEGORY');
        $category = new Category();

        $category->id_shop_default = $this->isMultistoreEnabled ? (int) $this->currentContextShopId : 1;
        $category->name = $this->dataFormatter->createMultiLangField(trim($categoryName));
        $category->active = true;
        $category->id_parent = (int) ($parentCategoryId ? $parentCategoryId : $homeCategoryId);
        $category->link_rewrite = $this->dataFormatter->createMultiLangField(
            $this->dataFormatter->createFriendlyUrl($category->name[$defaultLanguageId])
        );

        $fieldsError = $category->validateFields($unfriendlyError, true);
        $langFieldsError = $category->validateFieldsLang($unfriendlyError, true);
        $isValid = true === $fieldsError && true === $langFieldsError;

        if (!$isValid || !$category->add()) {
            $this->error(sprintf(
                $this->translator->trans(
                    '%1$s (ID: %2$s) cannot be saved',
                    [],
                    'Admin.Advparameters.Notification'
                ),
                $category->name[$defaultLanguageId],
                !empty($category->id) ? $category->id : 'null'
            ));

            if (!$isValid) {
                $error = true !== $fieldsError ? $fieldsError : '';
                $error .= true !== $langFieldsError ? $langFieldsError : '';

                $this->error($error . $this->legacyDatabase->getErrorMessage());
            }
        }
    }

    /**
     * Fetch the product ID.
     *
     * @param DataRowInterface $dataRow
     * @param array $entityFields
     * @param bool $fetchByReference if true, will fallback to finding the product ID by reference
     *
     * @return int|null
     */
    private function fetchProductId(
        DataRowInterface $dataRow,
        array $entityFields,
        $fetchByReference
    ) {
        $productId = $this->fetchDataValueByKey($dataRow, $entityFields, 'id');

        if (!empty($productId)) {
            return (int) $productId;
        }

        if ($fetchByReference) {
            $productReference = $this->fetchDataValueByKey($dataRow, $entityFields, 'reference');

            if ($productReference) {
                $statement = $this->connection->query(
                    'SELECT p.`id_product`
                    FROM `' . $this->productTable . '` p
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    WHERE p.`reference` = "' . pSQL($productReference) . '"'
                );
                $row = $statement->fetch();

                return isset($row['id_product']) ? $row['id_product'] : null;
            }
        }

        return null;
    }

    /**
     * Load stock data for the product.
     *
     * @param Product $product
     */
    private function loadStock(Product $product)
    {
        if (!Validate::isLoadedObject($product)) {
            return;
        }

        $product->loadStockData();
        $category_data = Product::getProductCategories((int) $product->id);

        if (is_array($category_data)) {
            foreach ($category_data as $tmp) {
                if ($product->category && !is_array($product->category)) {
                    continue;
                }
                $product->category[] = $tmp;
            }
        }
    }

    /**
     * Load shops data into the product object.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     * @param string $productName used for error messages
     */
    private function loadShops(Product $product, ImportConfigInterface $importConfig, $productName)
    {
        $defaultShopId = $this->configuration->getInt('PS_SHOP_DEFAULT');

        if (!$this->isMultistoreEnabled) {
            $product->shop = $defaultShopId;
            $product->id_shop_default = $defaultShopId;
        } elseif (!isset($product->shop) || empty($product->shop)) {
            $product->shop = implode($importConfig->getMultipleValueSeparator(), $this->contextShopIds);
            $product->id_shop_default = $this->currentContextShopId;
        }

        // link product to shops
        $product->id_shop_list = [];

        $multipleValueSeparator = $importConfig->getMultipleValueSeparator();
        if (empty($multipleValueSeparator)) {
            return;
        }
        $productShops = explode($multipleValueSeparator, $product->shop);

        if (is_array($productShops)) {
            foreach ($productShops as $shop) {
                if (!empty($shop)) {
                    $shop = is_numeric($shop) ? $shop : Shop::getIdByName($shop);

                    if (!in_array($shop, $this->allShopIds)) {
                        $this->addEntityWarning(
                            $this->translator->trans('Shop is not valid', [], 'Admin.Advparameters.Notification'),
                            $productName,
                            $product->id
                        );
                    } else {
                        $product->id_shop_list[] = $shop;
                    }
                }
            }
        }
    }

    /**
     * Load taxes data into the product object.
     *
     * @param Product $product
     */
    private function loadTaxes(Product $product)
    {
        if ($product->id_tax_rules_group) {
            if (Validate::isLoadedObject(new TaxRulesGroup($product->id_tax_rules_group))) {
                $taxManager = TaxManagerFactory::getManager($this->shopAddress, $product->id_tax_rules_group);
                $taxCalculator = $taxManager->getTaxCalculator();
                $product->tax_rate = $taxCalculator->getTotalRate();
            } else {
                $this->addEntityWarning(
                    $this->translator->trans(
                        'Unknown tax rule group ID. You need to create a group with this ID first.',
                        [],
                        'Admin.Advparameters.Notification'
                    ),
                    'id_tax_rules_group',
                    $product->id_tax_rules_group
                );
            }
        }

        if (!$this->configuration->getBoolean('PS_USE_ECOTAX')) {
            $product->ecotax = 0;
        }
    }

    /**
     * Load manufacturer data into the product object.
     *
     * @param Product $product
     * @param bool $validateOnly if true, will not create new manufacturer if not exists
     */
    private function loadManufacturer(Product $product, $validateOnly)
    {
        if (!isset($product->manufacturer)) {
            return;
        }

        if (is_numeric($product->manufacturer) && Manufacturer::manufacturerExists($product->manufacturer)) {
            $product->id_manufacturer = (int) $product->manufacturer;
        } elseif (is_string($product->manufacturer) && !empty($product->manufacturer)) {
            if ($manufacturer = Manufacturer::getIdByName($product->manufacturer)) {
                $product->id_manufacturer = (int) $manufacturer;
            } else {
                $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');

                $manufacturer = new Manufacturer();
                $manufacturer->name = $product->manufacturer;
                $manufacturer->active = true;

                $fieldsError = $manufacturer->validateFields($unfriendlyError, true);
                $langFieldsError = $manufacturer->validateFieldsLang($unfriendlyError, true);
                $isValid = true === $fieldsError && true === $langFieldsError;

                // Creating the manufacturer if it's not validation step
                if ($isValid && !$validateOnly && $manufacturer->add()) {
                    $product->id_manufacturer = (int) $manufacturer->id;
                    $manufacturer->associateTo($product->id_shop_list);
                } else {
                    if (!$validateOnly) {
                        $this->error(sprintf(
                            $this->translator->trans(
                                '%1$s (ID: %2$s) cannot be saved',
                                [],
                                'Admin.Advparameters.Notification'
                            ),
                            $manufacturer->name,
                            !empty($manufacturer->id) ? $manufacturer->id : 'null'
                        ));
                    }

                    if (!$isValid) {
                        $error = true !== $fieldsError ? $fieldsError : '';
                        $error .= true !== $langFieldsError ? $langFieldsError : '';

                        $this->error($error . $this->legacyDatabase->getErrorMessage());
                    }
                }
            }
        }
    }

    /**
     * Load supplier data into the product object.
     *
     * @param Product $product
     * @param bool $validateOnly if true, will not create new supplier if not exists
     */
    private function loadSupplier(Product $product, $validateOnly)
    {
        if (!isset($product->supplier)) {
            return;
        }

        if (is_numeric($product->supplier) && Supplier::supplierExists($product->supplier)) {
            $product->id_supplier = (int) $product->supplier;
        } elseif (is_string($product->supplier) && !empty($product->supplier)) {
            if ($supplier = Supplier::getIdByName($product->supplier)) {
                $product->id_supplier = (int) $supplier;
            } else {
                $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');

                $supplier = new Supplier();
                $supplier->name = $product->supplier;
                $supplier->active = true;

                $fieldsError = $supplier->validateFields($unfriendlyError, true);
                $langFieldsError = $supplier->validateFieldsLang($unfriendlyError, true);
                $isValid = true === $fieldsError && true === $langFieldsError;

                // Creating the supplier if it's not validation step
                if ($isValid && !$validateOnly && $supplier->add()) {
                    $product->id_supplier = (int) $supplier->id;
                    $supplier->associateTo($product->id_shop_list);
                } else {
                    if (!$validateOnly) {
                        $this->error(sprintf(
                            $this->translator->trans(
                                '%1$s (ID: %2$s) cannot be saved',
                                [],
                                'Admin.Advparameters.Notification'
                            ),
                            $supplier->name,
                            !empty($supplier->id) ? $supplier->id : 'null'
                        ));
                    }

                    if (!$isValid) {
                        $error = true !== $fieldsError ? $fieldsError : '';
                        $error .= true !== $langFieldsError ? $langFieldsError : '';

                        $this->error($error . $this->legacyDatabase->getErrorMessage());
                    }
                }
            }
        }
    }

    /**
     * Load prices into product object.
     *
     * @param Product $product
     */
    private function loadPrice(Product $product)
    {
        if (isset($product->price_tex) && !isset($product->price_tin)) {
            $product->price = $product->price_tex;
        } elseif (isset($product->price_tin) && !isset($product->price_tex)) {
            $product->price = $product->price_tin;
            // If a tax is already included in price, withdraw it from price
            if ($product->tax_rate) {
                $product->price = (float) number_format($product->price / (1 + $product->tax_rate / 100), 6, '.', '');
            }
        } elseif (isset($product->price_tin, $product->price_tex)) {
            $product->price = $product->price_tex;
        }
    }

    /**
     * Load category data into product object.
     *
     * @param Product $product
     * @param bool $validateOnly
     */
    private function loadCategory(Product $product, $validateOnly)
    {
        if (is_array($product->category) && count($product->category)) {
            $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');
            $defaultLanguageId = $this->configuration->getInt('PS_LANG_DEFAULT');
            $homeCategoryId = $this->configuration->getInt('PS_HOME_CATEGORY');
            $product->id_category = []; // Reset default values array

            foreach ($product->category as $value) {
                if (is_numeric($value)) {
                    if (Category::categoryExists((int) $value)) {
                        $product->id_category[] = (int) $value;
                    } else {
                        $category = new Category();
                        $category->id = (int) $value;
                        $category->name = $this->dataFormatter->createMultiLangField($value);
                        $category->active = true;
                        $category->id_parent = $homeCategoryId;
                        $category->link_rewrite = $this->dataFormatter->createMultiLangField(
                            $this->dataFormatter->createFriendlyUrl($category->name[$defaultLanguageId])
                        );

                        $fieldsError = $category->validateFields($unfriendlyError, true);
                        $langFieldsError = $category->validateFieldsLang($unfriendlyError, true);
                        $isValid = true === $fieldsError && true === $langFieldsError;

                        if ($isValid && !$validateOnly && $category->add()) {
                            $product->id_category[] = (int) $category->id;
                        } else {
                            if (!$validateOnly) {
                                $this->error(sprintf(
                                    $this->translator->trans(
                                        '%1$s (ID: %2$s) cannot be saved',
                                        [],
                                        'Admin.Advparameters.Notification'
                                    ),
                                    $category->name[$defaultLanguageId],
                                    !empty($category->id) ? $category->id : 'null'
                                ));
                            }

                            if (!$isValid) {
                                $error = true !== $fieldsError ? $fieldsError : '';
                                $error .= true !== $langFieldsError ? $langFieldsError : '';

                                $this->error($error . $this->legacyDatabase->getErrorMessage());
                            }
                        }
                    }
                } elseif (!$validateOnly && is_string($value) && !empty($value)) {
                    $category = Category::searchByPath(
                        $defaultLanguageId,
                        trim($value),
                        $this,
                        'createCategory'
                    );
                    if ($category['id_category']) {
                        $product->id_category[] = (int) $category['id_category'];
                    } else {
                        $this->error(
                            $this->translator->trans(
                                '%data% cannot be saved',
                                [
                                    '%data%' => trim($value),
                                ],
                                'Admin.Advparameters.Notification'
                            )
                        );
                    }
                }
            }

            $product->id_category = array_values(array_unique($product->id_category));
        }

        // Category default now takes the value of the first new category during import
        if (isset($product->id_category[0])) {
            $product->id_category_default = (int) $product->id_category[0];
        } elseif (!empty($product->id_category_default)) {
            $defaultProductShop = new Shop($product->id_shop_default);
            $product->id_category_default = Category::getRootCategory(
                null,
                Validate::isLoadedObject($defaultProductShop) ? $defaultProductShop : null
            )->id;
        }
    }

    /**
     * Load meta data into the product object.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     */
    private function loadMetaData(Product $product, ImportConfigInterface $importConfig)
    {
        $linkRewrite = '';

        $linkRewriteExists = is_array($product->link_rewrite) && isset($product->link_rewrite[$this->languageId]);

        if ($linkRewriteExists) {
            $linkRewrite = trim($product->link_rewrite[$this->languageId]);
        }

        $validLink = $this->validate->isLinkRewrite($linkRewrite);

        if (($linkRewriteExists && empty($product->link_rewrite[$this->languageId])) || !$validLink) {
            $linkRewrite = $this->dataFormatter->createFriendlyUrl($product->name[$this->languageId]);

            if ($linkRewrite == '') {
                $linkRewrite = 'friendly-url-autogeneration-failed';
            }
        }

        if (!$validLink) {
            $this->notice($this->translator->trans(
                'Rewrite link for %1$s (ID %2$s): re-written as %3$s.',
                [
                    '%1$s' => $product->name[$this->languageId],
                    '%2$s' => 'null',
                    '%3$s' => $linkRewrite,
                ],
                'Admin.Advparameters.Notification'
            ));
        }

        if (!$validLink || !(is_array($product->link_rewrite) && count($product->link_rewrite))) {
            $product->link_rewrite = $this->dataFormatter->createMultiLangField($linkRewrite);
        } else {
            $product->link_rewrite[(int) $this->languageId] = $linkRewrite;
        }

        $multipleValueSeparator = $importConfig->getMultipleValueSeparator();

        // replace the value of separator by coma
        if ($multipleValueSeparator != ',') {
            if (is_array($product->meta_keywords)) {
                foreach ($product->meta_keywords as &$metaKeyword) {
                    if (!empty($metaKeyword)) {
                        $metaKeyword = str_replace($multipleValueSeparator, ',', $metaKeyword);
                    }
                }
            }
        }
    }

    /**
     * Fix float values.
     *
     * @param Product $product
     */
    private function fixFloatValues(Product $product)
    {
        // Convert comma into dot for all floating values
        foreach (Product::$definition['fields'] as $key => $array) {
            if ($array['type'] == Product::TYPE_FLOAT) {
                $product->{$key} = str_replace(',', '.', $product->{$key});
            }
        }
    }

    /**
     * Load other product data.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     * @param bool $productExistsById
     * @param bool $productExistsByReference
     * @param bool $validateOnly
     * @param DataRowInterface $dataRow
     * @param array $entityFields
     *
     * @return bool
     */
    private function loadProductData(
        Product $product,
        ImportConfigInterface $importConfig,
        $productExistsById,
        $productExistsByReference,
        $validateOnly,
        DataRowInterface $dataRow,
        array $entityFields
    ) {
        if (!$product->quantity) {
            $product->quantity = 0;
        }

        $product->force_id = (bool) $importConfig->forceIds();
        $result = true;

        if ($productExistsById || $productExistsByReference) {
            $sqlPart = 'SELECT product_shop.`date_add`, p.`id_product`
                FROM `' . _DB_PREFIX_ . 'product` p
                ' . Shop::addSqlAssociation('product', 'p') . '
                WHERE ';

            if ($productExistsByReference) {
                $sqlPart .= 'p.`reference` = "' . pSQL($product->reference) . '"';
            } else {
                $sqlPart .= 'p.`id_product` = ' . (int) $product->id;
            }

            $statement = $this->connection->query($sqlPart);
            $row = $statement->fetch();

            if ($productExistsByReference) {
                $product->id = (int) $row['id_product'];
            }

            $product->date_add = $row['date_add'];

            if (!$validateOnly) {
                $result = $product->update();
            }
        } else {
            $result = $product->add($product->date_add == '');
        }

        if (!$validateOnly) {
            if ($product->getType() == Product::PTYPE_VIRTUAL) {
                StockAvailable::setProductOutOfStock((int) $product->id, 1);
            } else {
                StockAvailable::setProductOutOfStock((int) $product->id, (int) $product->out_of_stock);
            }

            if ($productDownload_id = ProductDownload::getIdFromIdProduct((int) $product->id)) {
                $productDownload = new ProductDownload($productDownload_id);
                $productDownload->delete(true);
            }

            if ($product->getType() == Product::PTYPE_VIRTUAL) {
                $downloadDir = $this->configuration->get('_PS_DOWNLOAD_DIR_');

                $productDownload = new ProductDownload();
                $productDownload->filename = ProductDownload::getNewFilename();
                $virtualProductFileUrl = $this->fetchDataValueByKey(
                    $dataRow,
                    $entityFields,
                    'file_url'
                );
                $this->tools->copy($virtualProductFileUrl, $downloadDir . $productDownload->filename);
                $productDownload->id_product = (int) $product->id;
                $productDownload->nb_downloadable = (int) $this->fetchDataValueByKey(
                    $dataRow,
                    $entityFields,
                    'nb_downloadable'
                );
                $productDownload->date_expiration = $this->fetchDataValueByKey(
                    $dataRow,
                    $entityFields,
                    'date_expiration'
                );
                $productDownload->nb_days_accessible = (int) $this->fetchDataValueByKey(
                    $dataRow,
                    $entityFields,
                    'nb_days_accessible'
                );
                $productDownload->display_filename = basename($virtualProductFileUrl);
                $productDownload->add();
            }
        }

        return $result;
    }

    /**
     * Save product supplier data.
     *
     * @param Product $product
     */
    private function saveProductSupplier(Product $product)
    {
        if ($product->id && property_exists($product, 'supplier_reference')) {
            $productSupplierId = (int) ProductSupplier::getIdByProductAndSupplier(
                (int) $product->id,
                0,
                (int) $product->id_supplier
            );

            $productSupplier = new ProductSupplier($productSupplierId);
            $productSupplier->id_product = (int) $product->id;
            $productSupplier->id_product_attribute = 0;
            $productSupplier->id_supplier = (int) $product->id_supplier;
            $productSupplier->product_supplier_price_te = $product->wholesale_price;
            $productSupplier->product_supplier_reference = $product->supplier_reference;
            $productSupplier->save();
        }
    }

    /**
     * Save specific price for a product.
     *
     * @param Product $product
     * @param string $reductionPrice
     * @param string $reductionPercent
     * @param string $reductionFrom
     * @param string $reductionTo
     * @param bool $validateOnly
     * @param string $productName
     */
    private function saveSpecificPrice(
        Product $product,
        $reductionPrice,
        $reductionPercent,
        $reductionFrom,
        $reductionTo,
        $validateOnly,
        $productName
    ) {
        $reductionPercent = (float) $reductionPercent;
        $reductionPrice = (float) $reductionPrice;

        if (!$reductionPrice <= 0 && $reductionPercent <= 0) {
            return;
        }

        foreach ($product->id_shop_list as $shopId) {
            $specificPrice = SpecificPrice::getSpecificPrice($product->id, $shopId, 0, 0, 0, 1, 0, 0, 0, 0);

            if (is_array($specificPrice) && isset($specificPrice['id_specific_price'])) {
                $specificPrice = new SpecificPrice((int) $specificPrice['id_specific_price']);
            } else {
                $specificPrice = new SpecificPrice();
            }
            $specificPrice->id_product = (int) $product->id;
            $specificPrice->id_specific_price_rule = 0;
            $specificPrice->id_shop = $shopId;
            $specificPrice->id_currency = 0;
            $specificPrice->id_country = 0;
            $specificPrice->id_group = 0;
            $specificPrice->price = -1;
            $specificPrice->id_customer = 0;
            $specificPrice->from_quantity = 1;
            $specificPrice->reduction = $reductionPrice ? $reductionPrice : $reductionPercent / 100;
            $specificPrice->reduction_type = $reductionPrice ? 'amount' : 'percentage';
            $specificPrice->from = Validate::isDate($reductionFrom) ? $reductionFrom : '0000-00-00 00:00:00';
            $specificPrice->to = Validate::isDate($reductionTo) ? $reductionTo : '0000-00-00 00:00:00';

            if (!$validateOnly && !$specificPrice->save()) {
                $this->addEntityWarning(
                    $this->translator->trans('Discount is invalid', [], 'Admin.Advparameters.Notification'),
                    $this->tools->sanitize($productName),
                    $product->id
                );
            }
        }
    }

    /**
     * Save product tags data.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     * @param string $productName product name, used for error messages
     */
    private function saveProductTags(Product $product, ImportConfigInterface $importConfig, $productName)
    {
        if (empty($product->tags)) {
            return;
        }

        $multipleValueSeparator = $importConfig->getMultipleValueSeparator();

        if (isset($product->id) && $product->id) {
            $tags = Tag::getProductTags($product->id);
            if (is_array($tags) && count($tags)) {
                if (is_string($product->tags) && !empty($multipleValueSeparator)) {
                    $product->tags = explode($multipleValueSeparator, $product->tags);
                }
                if (is_array($product->tags)) {
                    foreach ($product->tags as $key => $tag) {
                        if (!empty($tag)) {
                            $product->tags[$key] = trim($tag);
                        }
                    }
                    $tags[$this->languageId] = $product->tags;
                    $product->tags = $tags;
                }
            }
        }
        // Delete tags for this id product, for no duplicating error
        Tag::deleteTagsForProduct($product->id);

        if (!is_array($product->tags) && !empty($product->tags)) {
            $product->tags = $this->dataFormatter->createMultiLangField($product->tags);
            foreach ($product->tags as $key => $tags) {
                $isTagAdded = Tag::addTags($key, $product->id, $tags, $multipleValueSeparator);
                if (!$isTagAdded) {
                    $this->addEntityWarning(
                        $this->translator->trans('Tags list is invalid', [], 'Admin.Advparameters.Notification'),
                        $this->tools->sanitize($productName),
                        $product->id
                    );
                    break;
                }
            }
        } else {
            foreach ($product->tags as $key => $tags) {
                $str = '';

                foreach ($tags as $one_tag) {
                    $str .= $one_tag . $multipleValueSeparator;
                }

                $str = rtrim($str, $multipleValueSeparator);
                $isTagAdded = Tag::addTags($key, $product->id, $str, $multipleValueSeparator);

                if (!$isTagAdded) {
                    $this->addEntityWarning(
                        $this->translator->trans(
                            'Invalid tag(s) (%s)',
                            [
                                $str,
                            ],
                            'Admin.Notifications.Error'
                        ),
                        $this->tools->sanitize($productName),
                        (int) $product->id
                    );
                    break;
                }
            }
        }
    }

    /**
     * Save product images.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     */
    private function saveProductImages(Product $product, ImportConfigInterface $importConfig)
    {
        //delete existing images if "delete_existing_images" is set to 1
        if (isset($product->delete_existing_images)) {
            if ((bool) $product->delete_existing_images) {
                $product->deleteImages();
            }
        }

        if (isset($product->image) && is_array($product->image) && count($product->image)) {
            $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR');
            $product_has_images = (bool) Image::getImages($this->languageId, (int) $product->id);

            foreach ($product->image as $key => $url) {
                $url = trim($url);
                $error = false;
                if (!empty($url)) {
                    $url = str_replace(' ', '%20', $url);

                    $image = new Image();
                    $image->id_product = (int) $product->id;
                    $image->position = Image::getHighestPosition($product->id) + 1;
                    $image->cover = (!$key && !$product_has_images) ? true : false;
                    $alt = $product->image_alt[$key];
                    if (strlen($alt) > 0) {
                        $image->legend = $this->dataFormatter->createMultiLangField($alt);
                    }

                    $fieldsError = $image->validateFields($unfriendlyError, true);
                    $langFieldsError = $image->validateFieldsLang($unfriendlyError, true);
                    $isValid = true === $fieldsError && true === $langFieldsError;

                    if ($isValid && $image->add()) {
                        // associate image to selected shops
                        $image->associateTo($product->id_shop_list);
                        $copySucceeded = $this->imageCopier->copyImg(
                            $product->id,
                            $image->id,
                            $url,
                            'products',
                            !$importConfig->skipThumbnailRegeneration()
                        );

                        if (!$copySucceeded) {
                            $image->delete();
                            $this->warning(
                                $this->translator->trans(
                                    'Error copying image: %url%',
                                    [
                                        '%url%' => $url,
                                    ],
                                    'Admin.Advparameters.Notification'
                                )
                            );
                        }
                    } else {
                        $error = true;
                    }
                } else {
                    $error = true;
                }

                if ($error) {
                    $this->warning(
                        $this->translator->trans(
                            'Product #%id%: the picture (%url%) cannot be saved.',
                            [
                                '%id%' => isset($image) ? $image->id_product : '',
                                '%url%' => $url,
                            ],
                            'Admin.Advparameters.Notification'
                        )
                    );
                }
            }
        }
    }

    /**
     * Update additional product data.
     *
     * @param Product $product
     * @param bool $validateOnly
     */
    private function updateAdditionalData(Product $product, $validateOnly)
    {
        if (!$validateOnly && isset($product->id_category) && is_array($product->id_category)) {
            $product->updateCategories(array_map('intval', $product->id_category));
        }

        $product->checkDefaultAttributes();
        if (!$validateOnly && !$product->cache_default_attribute) {
            Product::updateDefaultAttribute($product->id);
        }
    }

    /**
     * Save product features.
     *
     * @param Product $product
     * @param ImportConfigInterface $importConfig
     */
    private function saveFeatures(Product $product, ImportConfigInterface $importConfig)
    {
        // Features import
        $features = get_object_vars($product);
        $multipleValueSeparator = $importConfig->getMultipleValueSeparator();

        if (empty($features['features']) || empty($multipleValueSeparator)) {
            return;
        }

        foreach (explode($multipleValueSeparator, $features['features']) as $singleFeature) {
            if (empty($singleFeature)) {
                continue;
            }
            $feature = explode(':', $singleFeature);
            $featureName = isset($feature[0]) ? trim($feature[0]) : '';
            $featureValue = isset($feature[1]) ? trim($feature[1]) : '';
            $position = isset($feature[2]) ? (int) $feature[2] - 1 : false;
            $custom = isset($feature[3]) ? (int) $feature[3] : false;

            if (!empty($featureName) && !empty($featureValue)) {
                $featureId = (int) Feature::addFeatureImport($featureName, $position);
                $productId = null;
                if ($importConfig->forceIds() || $importConfig->matchReferences()) {
                    $productId = (int) $product->id;
                }
                $featureValueId = (int) FeatureValue::addFeatureValueImport(
                    $featureId,
                    $featureValue,
                    $productId,
                    $this->languageId,
                    $custom
                );
                Product::addFeatureProductImport($product->id, $featureId, $featureValueId);
            }
        }

        // clean feature positions to avoid conflict
        Feature::cleanPositions();
    }

    /**
     * Save stock data for the product.
     *
     * @param Product $product
     * @param bool $validateOnly
     * @param bool $productExists
     */
    private function saveStock(Product $product, $validateOnly, $productExists)
    {
        $asmEnabled = $this->configuration->getBoolean('PS_ADVANCED_STOCK_MANAGEMENT');

        // set advanced stock managment
        if (!$validateOnly) {
            /* @phpstan-ignore-next-line Data of the property `advanced_stock_management` comes from database */
            if ($product->advanced_stock_management != 1 && $product->advanced_stock_management != 0) {
                $this->warning(
                    $this->translator->trans(
                        'Advanced stock management has incorrect value. Not set for product %name%',
                        ['%name%' => $product->name[$this->languageId]],
                        'Admin.Advparameters.Notification'
                    )
                );
            } elseif (!$asmEnabled && $product->advanced_stock_management == 1) {
                $this->warning(
                    $this->translator->trans(
                        'Advanced stock management is not enabled, cannot enable on product %name%',
                        ['%name%' => $product->name[$this->languageId]],
                        'Admin.Advparameters.Notification'
                    )
                );
            } elseif ($productExists) {
                $product->setAdvancedStockManagement($product->advanced_stock_management);
            }
            // automaticly disable depends on stock, if a_s_m set to disabled
            if (StockAvailable::dependsOnStock($product->id) == 1 && $product->advanced_stock_management == 0) {
                StockAvailable::setProductDependsOnStock($product->id, false);
            }
        }

        // Check if warehouse exists
        if (isset($product->warehouse) && $product->warehouse) {
            if (!$asmEnabled) {
                $this->warning(
                    $this->translator->trans(
                        'Advanced stock management is not enabled, warehouse not set on product %name%',
                        ['%name%' => $product->name[$this->languageId]],
                        'Admin.Advparameters.Notification'
                    )
                );
            } elseif (!$validateOnly) {
                if (Warehouse::exists($product->warehouse)) {
                    // Get already associated warehouses
                    $associatedWarehousesCollection = WarehouseProductLocation::getCollection($product->id);
                    // Delete any entry in warehouse for this product
                    foreach ($associatedWarehousesCollection as $awc) {
                        $awc->delete();
                    }
                    $warehouseLocationEntity = new WarehouseProductLocation();
                    $warehouseLocationEntity->id_product = $product->id;
                    $warehouseLocationEntity->id_product_attribute = 0;
                    $warehouseLocationEntity->id_warehouse = $product->warehouse;
                    $warehouseLocationEntity->save();
                    StockAvailable::synchronize($product->id);
                } else {
                    $this->warning(
                        $this->translator->trans(
                            'Warehouse did not exist, cannot set on product %name%',
                            ['%name%' => $product->name[$this->languageId]],
                            'Admin.Advparameters.Notification'
                        )
                    );
                }
            }
        }

        if ($this->isMultistoreEnabled) {
            $shopIds = $product->id_shop_list;
        } else {
            $shopIds = [
                $this->currentContextShopId,
            ];
        }

        // stock available
        if (isset($product->depends_on_stock)) {
            /* @phpstan-ignore-next-line Data of the property `depends_on_stock` comes from database */
            if ($product->depends_on_stock != 0 && $product->depends_on_stock != 1) {
                $this->warning(
                    $this->translator->trans(
                        'Incorrect value for "Depends on stock" for product %name%',
                        ['%name%' => $product->name[$this->languageId]],
                        'Admin.Advparameters.Notification'
                    )
                );
            /* @phpstan-ignore-next-line Data of properties `advanced_stock_management` & `depends_on_stock` comes from database */
            } elseif ((!$product->advanced_stock_management || $product->advanced_stock_management == 0) && $product->depends_on_stock == 1) {
                $this->warning(
                    $this->translator->trans(
                        'Advanced stock management is not enabled, cannot set "Depends on stock" for product %name%',
                        ['%name%' => $product->name[$this->languageId]],
                        'Admin.Advparameters.Notification'
                    )
                );
            } elseif (!$validateOnly) {
                StockAvailable::setProductDependsOnStock($product->id, $product->depends_on_stock);
            }

            // This code allows us to set qty and disable depends on stock
            if (!$validateOnly) {
                // if depends on stock and quantity, add quantity to stock
                if ($product->depends_on_stock == 1) {
                    $stockManager = StockManagerFactory::getManager();
                    $price = str_replace(',', '.', (string) $product->wholesale_price);
                    if ($price == 0) {
                        $price = 0.000001;
                    }
                    $price = round(floatval($price), 6);
                    $warehouse = new Warehouse($product->warehouse);
                    $productAdded = $stockManager->addProduct(
                        (int) $product->id,
                        0,
                        $warehouse,
                        (int) $product->quantity,
                        1,
                        $price,
                        true
                    );

                    if ($productAdded) {
                        StockAvailable::synchronize((int) $product->id);
                    }
                } else {
                    foreach ($shopIds as $shop) {
                        StockAvailable::setQuantity((int) $product->id, 0, (int) $product->quantity, (int) $shop);
                    }
                }
            }
        } elseif (!$validateOnly) {
            // if not depends_on_stock set, use normal qty
            foreach ($shopIds as $shop) {
                StockAvailable::setQuantity((int) $product->id, 0, (int) $product->quantity, (int) $shop);
            }
        }
    }

    /**
     * Link product accessories.
     *
     * @param Product $product
     * @param ImportRuntimeConfigInterface $runtimeConfig
     */
    private function linkAccessories(Product $product, ImportRuntimeConfigInterface $runtimeConfig)
    {
        // Accessories linkage
        if ($runtimeConfig->shouldValidateData()) {
            return;
        }

        $hasAccessories =
            isset($product->accessories) &&
            is_array($product->accessories) &&
            count($product->accessories)
        ;

        if ($hasAccessories) {
            $sharedData = $runtimeConfig->getSharedData();
            $accessories = isset($sharedData['accessories']) ? $sharedData['accessories'] : [];
            $accessories[$product->id] = $product->accessories;
            $runtimeConfig->addSharedDataItem('accessories', $accessories);
        }
    }

    /**
     * Import accessories.
     *
     * @param ImportRuntimeConfigInterface $runtimeConfig
     */
    private function importAccessories(ImportRuntimeConfigInterface $runtimeConfig)
    {
        $sharedData = $runtimeConfig->getSharedData();

        if (!isset($sharedData['accessories'])) {
            return;
        }

        foreach ($sharedData['accessories'] as $productId => $links) {
            if (count($links) > 0) { // We delete and relink only if there is accessories to link...
                // Bulk jobs: for performances, we need to do a minimum amount of SQL queries. No product inflation.
                $uniqueIds = Product::getExistingIdsFromIdsOrRefs($links);
                $this->connection->delete(
                    $this->accessoryTable,
                    [
                        'id_product_1' => (int) $productId,
                    ]
                );
                Product::changeAccessoriesForProduct($uniqueIds, $productId);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function supports($importEntityType)
    {
        return $importEntityType === Entity::TYPE_PRODUCTS;
    }
}
