<?php
namespace PrestaShop\Module\PolarisPrestaConnector;

use DateTime;
use Exception;
use PolarisPrestaConnector;
use PrestaShop\Module\PolarisPrestaConnector\Models\DiscountPlan;
use SpecificPrice;

/**
 * Synchronisation des promotions
 */
class SyncerDiscounts
{
    /**
     * Synchroniseur
     * 
     */
    private Syncer $syncer;
    /**
     * Indicateur de progression
     */
    private SyncProgress $progress;

    /**
     * Constructeur
     * 
     * @param Syncer $syncer Synchroniseur
     * @param SyncProgress|null $progress Indicateur de progression
     */
    public function __construct(Syncer $syncer, SyncProgress|null $progress = null)
    {
        $this->syncer = $syncer;
        $this->progress = $progress ?? new SyncProgress($syncer);
    }

    /**
     * Synchronisation des plans de promo
     */
    public function sync()
    {
        $bridge = $this->syncer->bridge;

        $this->progress->startStep("import_discounts", "Synchronisation des prix promotionnels...", "%d promo synchronisées");

        $startTime = microtime(true);
        $nbRefsDiscounts = 0;
        
        // On récupère une première fois les promotions depuis le pont
        $discounts = $bridge->pullDiscounts();

        if (count($discounts) > 0)
        {
            // Nous avons donc tiré tous les plans de promotions à ce moment précis selon la conception
            // du pont, nous pouvons donc faire le ménage au niveau de la base de données sur toutes les promotions
            // et établir notre plan d'action :
            // Nous allons donc établir une requête SQL qui met la date de fin de la promotion dans le passé pour toutes
            // les lignes de la table promotion qui ne sont pas dans la liste des clés du tableau discount 
            $db = \Db::getInstance();

            // Mais avant, on va changer toutes les dates d'un coup ici !
            // On commence par charger tous les plans de promotions
            $sql = 'SELECT `code`, `label`, `date_start`, `date_end`, `id_category` FROM `' . $this->syncer->module->TblPrefix . 'promotion`';
            $res = $db->executeS($sql);
            if ($res === false)
                throw new Exception('Erreur lors de la récupération des promotions : '.$db->getMsgError());

            $discountPlans = [];
            foreach (array_keys($discounts) as $code)
                $discountPlans[$code] = ['code' => $code, 'label' => null, 'date_start' => null, 'date_end' => null];
            foreach ($res as $row)
                $discountPlans[$row['code']] = ['code' => $row['code'], 'label' => $row['label'], 'date_start' => $row['date_start'], 'date_end' => $row['date_end']];  
            
            // Pour chaque discount plan, on update la base de travail s'il y a des différences
            foreach ($discountPlans as $row)
            {
                // Le plan existe-t-il encore ?
                if (!isset($discounts[$row['code']]))
                {
                    // Non: on met la date de fin à 2000-01-01, il sera supprimé                    
                    $sql = 'UPDATE `' . $this->syncer->module->TblPrefix . 'promotion` SET `date_end` = \'2000-01-01\' WHERE `code` = \''.$row['code'].'\'';
                    $res = $db->execute($sql);
                    if ($res === false)
                        throw new Exception('Erreur lors de la mise à jour de la promotion : '.$db->getMsgError());
                }
                else
                {
                    $dateStart = $discounts[$row['code']]->dateStart->format('Y-m-d H:i:s');
                    $dateEnd = $discounts[$row['code']]->dateEnd->format('Y-m-d H:i:s');

                    if (
                        $discounts[$row['code']]->name != $row['label'] || 
                        $dateStart != $row['date_start'] || 
                        $dateEnd != $row['date_end'])
                    {
                        // Oui et il a changé !
                        $sql = 'INSERT INTO `' . $this->syncer->module->TblPrefix . 'promotion` (`code`, `label`, `date_start`, `date_end`) VALUES ('.
                                    '\''.$db->escape($row['code']).'\', \''.
                                    $db->escape($discounts[$row['code']]->name).'\', \''.$dateStart.'\', \''.$dateEnd.'\') '.
                                    'ON DUPLICATE KEY UPDATE `label` = VALUES(`label`), `date_start` = VALUES(`date_start`), `date_end` = VALUES(`date_end`)';
                        $res = $db->execute($sql);
                        if ($res === false)
                            throw new Exception('Erreur lors de la mise à jour de la promotion : '.$db->getMsgError());                        
                    }
                }
            }                    

            // Puis on les synchronise normalement les détails
            $nbRefsDiscounts += $this->syncDiscounts($discounts);

            // Et on recommence en boucle, juste les détails
            while ($discounts = $bridge->pullDiscounts())
            {
                $this->syncer->checkMaxExecTime();
                $nbRefsDiscounts += $this->syncDiscounts($discounts);
            }

            // Et enfin, reconstruction discounts !            
            $this->rebuildDiscounts();
        }        

        $stopTime = microtime(true);
        if ($nbRefsDiscounts > 0)
            $this->syncer->audit(sprintf("[INF] Synchronisation de %d promotions effectuée en %.2f secondes", $nbRefsDiscounts, $stopTime - $startTime));    

        $this->progress->endStep("import_discounts");
    }

    /**
     * Met à jour un plan dans la base de données
     * 
     * @param PolarisPrestaConnector $module Module PolarisPrestaConnector
     * @param DiscountPlan $discountPlan Plan à créer ou mettre à jour
     */
    public static function updateDiscountPlan(PolarisPrestaConnector $module, DiscountPlan $discountPlan)
    {
        // On cherche dans la base de données le plan de réduction courant                
        $db = \Db::getInstance();

        $sql = 'SELECT `code`, `label`, `date_start`, `date_end`, `id_category` FROM `' . $module->TblPrefix . 'promotion`';
        $res = $db->executeS($sql);
        if ($res === false)
            throw new \Exception('Erreur lors de la récupération des promotions : '.$db->getMsgError());

        $data = [
                ':code' => $discountPlan->id,
                ':label' => $discountPlan->name,
                ':date_start' => $discountPlan->dateStart->format('Y-m-d H:i:s'),
                ':date_end' => $discountPlan->dateEnd->format('Y-m-d H:i:s'),
        ];

        // S'il n'y a aucun résultat, créer le plan de réduction courant avec instruction insert
        // Sinon, le mettre à jour si le label ou les dates ne correspondent pas avec instruction update
        if (count($res) === 0)
        {
            $sql = 'INSERT INTO `' . $module->TblPrefix . 'promotion` (`code`, `label`, `date_start`, `date_end`) VALUES (:code, :label, :date_start, :date_end)';
            foreach ($data as $d => $v) $sql = str_replace($d, "'".$db->escape($v)."'", $sql);

            $res = $db->execute($sql, $data);
            if ($res === false)
                throw new \Exception('Erreur lors de l\'insertion du plan de réduction '.$discountPlan->id.' : '.$db->getMsgError());
        }
        else if ($res[0]['date_start'] != $data[':date_start'] || $res[0]['date_end'] != $data[':date_end'] || $res[0]['label'] != $data[':label'])
        {
            $sql = 'UPDATE `' . $module->TblPrefix . 'promotion` SET `label` = :label, `date_start` = :date_start, `date_end` = :date_end WHERE `code` = :code';
            foreach ($data as $d => $v) $sql = str_replace($d, "'".$db->escape($v)."'", $sql);

            $res = $db->execute($sql, $data);
            if ($res === false)
                throw new \Exception('Erreur lors de la mise à jour du plan de réduction '.$discountPlan->id.' : '.$db->getMsgError());
        }
    }

    /**
     * Synchronisation des promotions précisées
     * 
     * @param array<Discount> $discounts Promotions à synchroniser
     * @return int Nombre de stocks synchronisés 
     */
    public function syncDiscounts(array & $discounts) : int
    {
        // Identifions les références concernées
        $refMngr = $this->syncer->getReferencesManager();

        // Un tableau de stock par id_product_attribute
        $nbDone = 0;

        // Chargement en cache des promotions déjà existantes
        $cachePromo = [];
        $id_products = [];
        foreach ($discounts as $plan)
        {
            foreach ($plan->discounts as $discount)
            {
                $psRef = $refMngr->translateCombinationOneRef($discount->refs);
                if ($psRef === null || empty($psRef))  // Référence non trouvée
                    continue;   // On ignore !

                $psRef = $psRef[0]; // On ne garde que le premier

                // Bien, si le produit n'est pas dans le cache, on le met pour 
                // forcer à updater dessus
                $discount->key = $plan->id.'-'.$psRef[0].'-'.$psRef[1];
                $discount->id_product = $psRef[0];
                $discount->id_product_attribute = $psRef[1];
                
                $cachePromo[$discount->key] = null;

                if (!isset($id_products[$psRef[0]]))
                    $id_products[$psRef[0]]= $psRef[0];
            }
        }
        
        if (count($id_products) == 0)
            return 0;   // Rien à faire !

        $db = \Db::getInstance();
        $sql = 'SELECT code, id_product, id_product_attribute, id_specific_price, reduction FROM `' . $this->syncer->module->TblPrefix . 'promotion_detail` WHERE ';
        $sql .= ' `id_product` IN ('.implode(',', $id_products).')';
        
        $res = $db->executeS($sql);
        if ($res === false)
            throw new Exception('Erreur lors de la récupération des promotions : '.$db->getMsgError());

        foreach ($res as $row)
        {
            $key = $row['code'].'-'.$row['id_product'].'-'.$row['id_product_attribute'];
            $cachePromo[$key] = $row;
        }

        foreach ($discounts as $plan)
        {
            foreach ($plan->discounts as $discount)
            {
                // On vérifie si la promotion existe déjà ou si on doit faire des modifications
                if (!isset($discount->key))
                    continue;   // référence non trouvée !

                $promo = $cachePromo[$discount->key] ?? null;

                // Référence trouvée !
                if ($discount->active || $promo)
                {
                    if (!$promo)    // La promotion n'existe pas, on doit la créer
                    {
                        $promo = [
                            "code" => $plan->id,
                            "id_specific_price" => null,
                            "reduction" => 0,
                        ];
                    }

                    $reduction = $discount->active ? $discount->discount : 0; 

                    // Et maintenant, s'il y a la moindre modification, on met à jour
                    if ($promo['reduction'] != $reduction)
                    {
                        $promo['reduction'] = $reduction;

                        // On le stock dans la bdd
                        $sql = 'INSERT INTO `' . $this->syncer->module->TblPrefix . 'promotion_detail` (`code`, `id_product`, `id_product_attribute`, `id_specific_price`, `reduction`) VALUES ('.
                                '\''.$promo['code'].'\', '.$discount->id_product.', '.$discount->id_product_attribute.', '.($promo['id_specific_price'] ?? 'NULL').', '.$promo['reduction'].') '.
                                'ON DUPLICATE KEY UPDATE `id_product` = VALUES(`id_product`), `id_product_attribute` = VALUES(`id_product_attribute`), `id_specific_price` = VALUES(`id_specific_price`), `reduction` = VALUES(`reduction`)';

                        $res = $db->execute($sql);
                        if ($res === false)
                            throw new Exception('Erreur lors de la mise à jour de la promotion : '.$db->getMsgError());

                        $nbDone++;
                        $this->progress->progress('import_discounts');
                    }
                }
            }
        }

        return $nbDone;
    }

    /**
     * Reconstruction des promotions
     * 
     */
    public function rebuildDiscounts()
    {
        $db = \Db::getInstance();
        $ctx = \Context::getContext();

        // On commence par supprimer les plans caduques en prenant garde à désassocier préalablement les produits
        // des catégories de promotions dans lesquelles ils se trouvent
        $now = (new DateTime())->format('Y-m-d H:i:s');

        $sql = 'SELECT DISTINCT d.id_product, d.id_category
                FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d 
                LEFT JOIN `' . $this->syncer->module->TblPrefix.'promotion` AS p ON (p.`code` = d.`code`)                
                WHERE 
                    p.`date_end` < \''.$now.'\' AND d.id_category IS NOT NULL';
        $res = $db->executeS($sql);
        if ($res === false)
            throw new Exception('Erreur lors de la récupération des promotions détails : '.$db->getMsgError());

        foreach ($res as $row)
        {
            // Les produits sélectionnés sont à retirer de la catégorie
            $id_product = $row["id_product"];
            $id_category = $row["id_category"];

            $product = new \Product($row["id_product"]);
            if ($product->id)
                $product->deleteCategory($id_category);
        }

        // Maintenant on peut supprimer toutes les promotions caduques d'un coup
        if (!$db->execute('DELETE FROM `'._DB_PREFIX_.'specific_price` WHERE `id_specific_price` IN (SELECT `id_specific_price` FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` LEFT JOIN `'.$this->syncer->module->TblPrefix.'promotion` USING (`code`) WHERE `date_end` < \''.$now.'\')'))
            throw new \Exception('Erreur lors de la suppression des promotions caduques : '.$db->getMsgError());

        // Ensuite, on peut se débarasser des promotions caduques dans nos tables
        if (!$db->execute('DELETE FROM `'.$this->syncer->module->TblPrefix.'promotion` WHERE `date_end` < \''.$now.'\''))
            throw new \Exception('Erreur lors de la suppression des promotions caduques : '.$db->getMsgError());

        // Maintenant, on supprime tous les ps_specific_price qui ont une réduction minimum à 0
        // Ou qui ont une réduction identique à la réduction minimum pour un product_attribute (pas une promo de base)
        $sql = 'DELETE FROM `' . _DB_PREFIX_.'specific_price`
                WHERE `id_specific_price` IN (
                    SELECT sp.`id_specific_price` FROM `' . _DB_PREFIX_.'specific_price` AS sp
                    LEFT JOIN `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d ON (d.`id_specific_price` = sp.`id_specific_price`)
                    LEFT JOIN (SELECT id_product, code, COUNT(DISTINCT reduction) AS nb, MIN(reduction) AS min FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` GROUP BY code, id_product) AS nbtbl ON (nbtbl.code = d.code AND nbtbl.id_product = d.id_product)
                    WHERE d.reduction <= 0 OR (nbtbl.min = d.reduction AND sp.id_product_attribute != 0))';
        $res = $db->execute($sql);
        if ($res === false)
            throw new Exception('Erreur lors de la récupération des promotions détails : '.$db->getMsgError());
        
        // On va procéder code promo par code promo
        // Localisons déjà les promotions à recalculer
        $sql = 'SELECT DISTINCT p.code
                FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d 
                LEFT JOIN `' . $this->syncer->module->TblPrefix.'promotion` AS p ON (p.`code` = d.`code`) 
                LEFT JOIN `' . _DB_PREFIX_.'specific_price` AS sp ON (sp.`id_specific_price` = d.`id_specific_price`) 
                LEFT JOIN (SELECT id_product, code, COUNT(DISTINCT reduction) AS nb, MIN(reduction) AS min FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` GROUP BY code, id_product) AS nbtbl ON (nbtbl.code = d.code AND nbtbl.id_product = d.id_product)
                WHERE 
                    sp.`id_specific_price` IS NULL OR                     
                    sp.`reduction` != d.`reduction` OR
                    p.`date_start` != sp.from OR
                    p.`date_end` != sp.to OR
                    p.`date_end` < NOW() OR
                    COALESCE(p.`id_category`, 0) != COALESCE(d.`id_category`, 0)
                ORDER BY d.code';
                
        $resPlan = $db->executeS($sql);
        if ($resPlan === false)
            throw new Exception('Erreur lors de la récupération des promotions à recalculer : '.$db->getMsgError());
        foreach ($resPlan as $rowPlan)
        {
            $code = $rowPlan['code'];

            $this->syncer->audit('[INF] Recalcul du plan de promotion '.$code.'...');

            // Enfin on charge tous les promotion_detail de tous les produits dont un promotion_detail ne correspond pas au ps_specific_price (null, prix différent, ...)
            $sql = 'SELECT d.id_product, d.id_product_attribute, d.reduction, p.date_start, p.date_end, sp.id_specific_price, 
                            d.id_category AS d_id_category, 
                            p.id_category AS p_id_category,
                            nbtbl.min,
                            nbtbl.nb                
                    FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d 
                    LEFT JOIN `' . $this->syncer->module->TblPrefix.'promotion` AS p ON (p.`code` = d.`code`) 
                    LEFT JOIN `' . _DB_PREFIX_.'specific_price` AS sp ON (sp.`id_specific_price` = d.`id_specific_price`) 
                    LEFT JOIN (SELECT id_product, code, COUNT(DISTINCT reduction) AS nb, MIN(reduction) AS min FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` GROUP BY code, id_product) AS nbtbl ON (nbtbl.code = d.code AND nbtbl.id_product = d.id_product)
                    WHERE 
                        p.code = \''.$db->escape($code).'\' AND 
                        d.id_product IN (
                            SELECT d2.id_product
                                FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d2 
                                LEFT JOIN `' . $this->syncer->module->TblPrefix.'promotion` AS p2 ON (p2.`code` = d2.`code`) 
                                LEFT JOIN `' . _DB_PREFIX_.'specific_price` AS sp2 ON (sp2.`id_specific_price` = d2.`id_specific_price`) 
                                LEFT JOIN (SELECT id_product, code, COUNT(DISTINCT reduction) AS nb, MIN(reduction) AS min FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` GROUP BY code, id_product) AS nbtbl2 ON (nbtbl2.code = d2.code AND nbtbl2.id_product = d2.id_product)
                                WHERE 
                                    p2.code = \''.$db->escape($code).'\' AND (
                                    sp2.`id_specific_price` IS NULL OR
                                    sp2.`reduction` != d2.`reduction` OR
                                    p2.`date_start` != sp2.from OR
                                    p2.`date_end` != sp2.to OR
                                    p2.`date_end` < NOW()
                                    )
                        )                                            
                    ORDER BY d.code, d.id_product, COALESCE(sp.id_product_attribute, d.id_product_attribute)';
            $res = $db->executeS($sql);
            if ($res === false)
                throw new Exception('Erreur lors de la récupération des promotions détails : '.$db->getMsgError());

            // Chargement des promotions déjà existantes et déjà associées
            $cacheSimpleProductPromos = [];

            foreach ($res as $row)
            {
                $ps_id = 0;                
                if ($row['min'] > 0)
                {
                    // S'il y a au moins une réduction valide, on va créer un specific_price pour tout le produit
                    // avec cette valeur, ça nous fera une réduction de référence.
                    // enfin, si on l'a pas déjà fait pour ce produit !
                    if (!isset($cacheSimpleProductPromos[$code][$row['id_product']]))
                    {
                        // Nope, on le crée maintenant !
                        $sp = new SpecificPrice($row["id_specific_price"]);
                        $sp->id_specific_price_rule = 0;
                        $sp->id_cart = 0;
                        $sp->id_shop = $ctx->shop->id;
                        $sp->id_shop_group = $ctx->shop->id_shop_group;            
                        $sp->id_group = 0;
                        $sp->id_currency = 0;
                        $sp->id_country = 0;            
                        $sp->id_customer = 0;
                        $sp->id_product = $row['id_product'];
                        $sp->id_product_attribute = 0;
                        $sp->from = $row["date_start"];
                        $sp->to = $row["date_end"];
                        $sp->price = -1;
                        $sp->from_quantity = 1;                    
                        $sp->reduction_tax = 1;
                        $sp->reduction_type = 'percentage';
                        $sp->reduction = $row["min"];

                        try
                        {
                            $sp->save();
                        }
                        catch (Exception $e)
                        {
                            // Erreur lors de la sauvegarde
                            // Si c'est une erreur duplicate-key entry, on ignore mais on signale
                            // qu'une promotion aux mêmes date pour ce produit existe déjà !
                            if (strpos($e->getMessage(), 'Duplicate entry') === false)
                                throw $e;
                            else
                            {
                                $this->syncer->audit('[WRN] Une promotion similaire (mêmes dates : du '.$sp->from.' au '.$sp->to.') sur le produit '.$row['id_product'].' existe déjà !');
                                continue;
                            }
                        }

                        $this->syncer->audit('[INF] Promotion '.$code.' sur produit '.$row['id_product'].' créée : -'.round($sp->reduction * 100, 2).'%');

                        $ps_id = $sp->id;
                        $cacheSimpleProductPromos[$code][$row['id_product']] = $ps_id;

                        if ($row['id_specific_price'] != $ps_id)
                        {
                            // Sauvegarde de id_specific_price dans la table promotion_detail                
                            if (!$db->execute('INSERT INTO `'.$this->syncer->module->TblPrefix.'promotion_detail` (`code`, `id_product`, `id_product_attribute`, `id_specific_price`, `reduction`) VALUES ('.
                            '\''.$db->escape($code).'\', '.$row['id_product'].', '.$row['id_product_attribute'].', '.$ps_id.', '.$row['reduction'].') '.
                                    'ON DUPLICATE KEY UPDATE `id_product` = VALUES(`id_product`), `id_product_attribute` = VALUES(`id_product_attribute`), `id_specific_price` = VALUES(`id_specific_price`), `reduction` = VALUES(`reduction`)'))
                                throw new Exception('Erreur lors de la mise à jour des détails de promotions : '.$db->getMsgError());
                        }
                    }
                }

                if ($row["reduction"] > 0)
                {
                    // Si nous avons une promotion valide avec une réduction différente de la réduction de référence, 
                    // alors on créer un spécific_price pour cette valeur
                    if ($row["reduction"] != $row["min"])
                    {
                        // Création d'un specific_price par produit
                        $sp = new SpecificPrice($row["id_specific_price"]);
                        $sp->id_specific_price_rule = 0;
                        $sp->id_cart = 0;
                        $sp->id_shop = $ctx->shop->id;
                        $sp->id_shop_group = $ctx->shop->id_shop_group;            
                        $sp->id_group = 0;
                        $sp->id_currency = 0;
                        $sp->id_country = 0;
                        $sp->id_customer = 0;
                        $sp->id_product = $row['id_product'];
                        $sp->id_product_attribute = $row['id_product_attribute'];
                        $sp->from = $row["date_start"];
                        $sp->to = $row["date_end"];
                        $sp->price = -1;
                        $sp->from_quantity = 1;                    
                        $sp->reduction_tax = 1;
                        $sp->reduction_type = 'percentage';
                        $sp->reduction = $row["reduction"];
                        try
                        {
                            $sp->save();
                        }
                        catch (Exception $e)
                        {
                            // Erreur lors de la sauvegarde
                            // Si c'est une erreur duplicate-key entry, on ignore mais on signale
                            // qu'une promotion aux mêmes date pour ce produit existe déjà !
                            if (strpos($e->getMessage(), 'Duplicate entry') === false)
                                throw $e;
                            else
                            {
                                $this->syncer->audit('[WRN] Une promotion similaire (mêmes dates : du '.$sp->from.' au '.$sp->to.') sur le produit '.$row['id_product'].' existe déjà !');
                                continue;
                            }
                        }

                        $this->syncer->audit('[INF] Promotion '.$code.' sur produit '.$row['id_product'].' ('.$row['id_product_attribute'].') créée : -'.$sp->reduction.'%');

                        $ps_id = $sp->id;
                    }
                    else
                    {
                        // Sinon, on s'assure qu'on a bien linké notre spécific_price de référence (qui existe forcément à ce stade)
                        $ps_id = $cacheSimpleProductPromos[$code][$row['id_product']];
                    }
                    
                    if ($row['id_specific_price'] != $ps_id)
                    {
                        // Sauvegarde de id_specific_price dans la table promotion_detail                
                        if (!$db->execute('INSERT INTO `'.$this->syncer->module->TblPrefix.'promotion_detail` (`code`, `id_product`, `id_product_attribute`, `id_specific_price`, `reduction`) VALUES ('.
                            '\''.$db->escape($code).'\', '.$row['id_product'].', '.$row['id_product_attribute'].', '.$ps_id.', '.$row['reduction'].') '.
                            'ON DUPLICATE KEY UPDATE `id_product` = VALUES(`id_product`), `id_product_attribute` = VALUES(`id_product_attribute`), `id_specific_price` = VALUES(`id_specific_price`), `reduction` = VALUES(`reduction`)'))
                            throw new Exception('Erreur lors de la mise à jour des détails de promotions : '.$db->getMsgError());
                    }
                }
            }

            // On s'occupe de placer les produits dans la bonne catégorie
            // On regarde ceux qu'ils faut déplacer promotion_detail.id_category != promotion.id_category
            $sql = 'SELECT d.id_product, p.id_category as p_id_category, d.id_category as d_id_category
                    FROM `'.$this->syncer->module->TblPrefix.'promotion_detail` AS d 
                    LEFT JOIN `' . $this->syncer->module->TblPrefix.'promotion` AS p ON (p.`code` = d.`code`)                
                    WHERE 
                        p.`code` = \''.$db->escape($code).'\' AND 
                        COALESCE(p.`id_category`, 0) != COALESCE(d.`id_category`, 0)';
            $res = $db->executeS($sql);
            if ($res === false)
                throw new Exception('Erreur lors de la récupération des promotions détails : '.$db->getMsgError());
            foreach ($res as $row)
            {
                $product = new \Product($row["id_product"]);
                if ($product->id)
                {                    
                    // Si on doit enlever l'ancienne catégorie
                    if ($row["d_id_category"])
                    {                        
                        if (in_array((int) $row['d_id_category'], $product->getCategories()))
                        {
                            $product->deleteCategory($row["d_id_category"]);
                            $this->syncer->audit('[INF] Produit '.$row['id_product'].' supprimé de la catégorie #'.$row['d_id_category'].' !');
                        }
                    }

                    if ($row["p_id_category"])
                    {
                        if (!in_array((int) $row['p_id_category'], $product->getCategories()))
                        {
                            $product->addToCategories([$row["p_id_category"]]);
                            $this->syncer->audit('[INF] Produit '.$row['id_product'].' ajouté à la catégorie #'.$row['p_id_category'].' !');
                        }
                    }

                    if ($row["d_id_category"] || $row["p_id_category"])
                    {
                        // On modifie la valeur dans la table promotion_detail
                        if (!$db->execute('UPDATE `'.$this->syncer->module->TblPrefix.'promotion_detail` SET `id_category` = '.($row["p_id_category"] ?? 'NULL').' WHERE `code` = \''.$db->escape($code).'\' AND `id_product` = '.$row['id_product']))
                            throw new Exception('Erreur lors de la mise à jour de l\'affectation de la catégorie au produit : '.$db->getMsgError());                        
                    }
                }
            }
        }
    }
}