<?php
namespace PrestaShop\Module\PolarisPrestaConnector;

use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use PolarisPrestaConnector;
use PrestaShop\Module\PolarisPrestaConnector\Models\Voucher;

/**
 * Configuration du module
 */
class SyncConfiguration
{
    const PRODUCT_ATTR_PRODUCT = 1;
    const PRODUCT_ATTR_VARIANT = 2;
    const PRODUCT_ATTR_FEATURE = 3;

    /**
     * Clé de configuration pour le fournisseur de référence
     */
    const CFG_SUPPLIER = 'id_supplier';

    /**
     * PolarisLink migration effectuée ?
     */
    const CFG_MIGRATION_POLARISLINK = 'migration_polarislink';

    /**
     * Version détectée du backoffice
     */
    const CFG_BACKOFFICE_VERSION = 'backoffice_version';

    /**
     * Clé de configuration pour le login
     */
    const CFG_LOGIN = 'login';

    /**
     * Clé de configuration pour la taille mémoire maximale à utiliser pour la synchronisation
     */
    const CFG_MEMORY_MAX_SIZE = 'memory_max_size';

    /**
     * Token de sécurité pour les appels de tâches CRON
     */
    const CFG_GUARD_TOKEN = 'guard_token';

    /**
     * Progression de la synchronisation
     */
    const CFG_SYNC_STATE = 'sync_state';

    /**
     * Photo auto clean (Nombre de jours avant nettoyage des photos, 0 pour désactiver)
     */
    const CFG_PHOTO_CLEAN = 'photo_clean';

    /**
     * Clé de configuration pour le pattern de création des URLs de redirection des produits
     */
    const CFG_PRODUCT_REDIRECT_URL_CREATE_PATTERN = 'product_redirect_url_create_pattern';

    /**
     * Clé de configuration pour le mode de fonctionnement du module.
     */
    const CFG_ACTIVE = 'actived';

    /**
     * Clé de configuration pour l'interval de temps entre deux synchronisations de produits
     */
    const CFG_SYNC_PRODUCT_INTERVAL = 'sync_product_interval';

    /**
     * Clé de configuration pour l'interval de temps entre deux synchronisations des stocks
     */
    const CFG_SYNC_STOCK_INTERVAL = 'sync_stock_interval';

    /**
     * Clé de configuration pour l'interval de temps entre deux synchronisations complètes de produits
     */
    const CFG_SYNC_FULL_PRODUCT_INTERVAL = 'sync_full_products_interval';

    /**
     * Clé de configuration pour l'interval de temps entre deux synchronisations des stocks
     */
    const CFG_SYNC_FULL_STOCK_INTERVAL = 'sync_full_stock_interval';

    /**
     * Clé de configuration pour les dernières synchronisations
     */
    const CFG_SYNCS_LAST = 'syncs_last_date';

    /**
     * Clé de configuration pour le stock de sécurité à appliquer
     */
    const CFG_SECURED_STOCK = 'stock_security';

    /**
     * Identifiant de l'état de commande pour l'export
     */
    const CFG_ORDER_EXPORT_STATE = 'order_export_state';

    /**
     * Identifiant de l'état de commande pour l'export
     */
    const CFG_ORDER_EXPORT_REFUND = 'order_export_refund';

    /**
     * Identifiant de l'état de commande pour l'export
     */
    const CFG_ORDER_PAY_MAPPAGE = 'order_pay_mappage';

    /**
     * Identifiant de l'état de commande pour l'export
     */
    const CFG_ORDER_ERROR_EXPORT_STATE = 'order_err_export_state';

    /**
     * Référence pour les frais de port
     */
    const CFG_SHIPPING_COST_REF = 'shipping_cost_ref';

    /**
     * Importation des bons/chèques cadeaux
     */
    const CFG_VOUCHER_IMPORT_TYPES = 'voucher_import_types';

    /**
     * Importation des clients depuis le backoffice
     */
    const CFG_IMPORT_CUSTOMERS = 'import_customers';

    /**
     * Utilisation des cartes prépayées
     */
    const CFG_SUPPORT_PREPAID_CARDS = 'use_prepaidcards';

    /**
     * Utilisation des cartes prépayées
     */
    const CFG_PRODUCT_NAME_TEMPLATE = 'product_name_template';

    /**
     * Exportation des clients vers le backoffice
     */
    const CFG_EXPORT_CUSTOMERS = 'export_customers';

    /**
     * Exportation des clients inscrits à la newsletter même sans commande
     */
    const CFG_EXPORT_CUSTOMERS_NEWSLETTER = 'export_customers_newsletter';

    /**
     * Référence pour les frais d'emballage
     */
    const CFG_WRAPPING_COST_REF = 'wrapping_cost_ref';

    /**
     * Age maximum d'une commande pour l'exporter automatiquement
     */
    const CFG_ORDER_MAX_TIME = 'order_max_age';

    /**
     * ID minimum d'une commande pour l'exporter automatiquement
     */
    const CFG_ORDER_BARRIER = 'order_id_barrier';

    /**
     * Clé de configuration pour la visibilité des produits sans stock
     */
    const CFG_NO_STOCK_PRODUCT_VISIBILITY = 'no_stock_product_visibility';

    /**
     * Clé de configuration pour ignorer les produits hors stock lors de l'importation
     */
    const CFG_PRODUCT_IMPORT_ONLY_WITH_STOCK = 'import_only_with_stock';

    /**
     * Clé de configuration pour le mode de fonctionnement du module.
     */
    const CFG_WORKMODE = 'mode';

    /**
     * Produits verrouillés
     */
    const CFG_LOCKED_PRODUCT = 'locked_product';

    /**
     * Tri automatique des attributs lorsqu'un nouvel attribut est inséré
     */
    const CFG_ATTR_AUTOSORT = 'attr_auto_sort';

    /**
     * Clé de configuration pour les positions de synchronisation
     */
    const CFG_SYNC_POSITIONS = 'sync_positions';

    /**
     * Clé de configuration pour le mapping des catégories (string)
     */
    const CFG_MAP_MANUFACTURER = 'map_manufacturers';

    /**
     * Clé de configuration pour le mapping des noms de marques
     */
    const CFG_MAP_MANUFACTURER_NAME = 'manufacturer_names';

    /**
     * Clé de configuration pour le mapping des catégories dynamiques
     */
    const CFG_MAP_DYN_CAT = 'map_dyn_cat_';

    /**
     * Noms des catégories dynamiques
     */
    const CFG_DYN_CAT = 'dyn_cat';

    /**
     * Noms des attributs dynamiques
     */
    const CFG_DYN_ATTR = 'dyn_attr';

    /**
     * Propriétés dynamiques (attributs non discriminants)
     */
    const CFG_DYN_PROP = 'dyn_prop';

    /**
     * Nettoyage automatique des attributs non utilisés
     */
    const CFG_ATTR_AUTO_CLEAN = 'attr_auto_clean';

    /**
     * Partie de clé de configuration pour le mapping des grilles de tailles
     */
    const CFG_MAP_SIZEGRIDS = 'map_sizegrids';

    /**
     * Clé de configuration pour le mapping des magasins
     */
    const CFG_MAP_STORES = 'map_stores';

    /**
     * Clé de configuration pour les noms de magasins
     */
    const CFG_MAP_STORES_NAME = 'map_stores_names';

    /**
     * Clé de configuration pour définir la liste des magasins qui participent aux stocks des ventes en ligne
     */
    const CFG_ONLINE_STOCK_STORES = 'oss';

    /**
     * Nom des grilles de tailles
     */
    const CFG_MAP_SIZEGRIDS_NAME = 'sizegrid_names';

    /**
     * Clé de configuration pour le mapping des couleurs
     */
    const CFG_COLOR_MAP = 'color_map';

    /**
     * Clé de configuration pour le code magasin de la boutique
     */
    const CFG_WEB_STORE_CODE = 'webstore_code';    

    /**
     * Clé de configuration pour le groupe d'attribut qui doit être mappé au niveau couleur
     */
    const CFG_MAP_COLOR_ATTRS = 'color_map_attributes';

    /**
     * constante indiquant le tableau associatif reflétant la distribution des attributs des produits.
     * Cela doit ressembler à ça dans la configuration :
     * 
     * [
     *      PRODUCT_ATTR_PRODUCT => [ 'niveau2' => 0, 
     *                                'niveau3' => 0, 
     *                                'niveau4' => 0 ],
     *      PRODUCT_ATTR_VARIANT => [ 'niveau1' => <id_groupe_attribute> ],
     * ]
     * 
     */
    const CFG_PR_ATTRIBUTES_MAPPING = 'p_attr_mapping';
    
    /**
     * Clé de configuration pour la description des données à prendre en charge lors de la synchronisation (tableau)
     */
    const CFG_DATA_SYNC = 'data_sync';    
    /**
     * Synchronisation des photos des produits
     */
    const CFG_PRODUCT_SYNC_PHOTOS = 'p_photos';
    /**
     * Publication automatique des produits
     */
    const CFG_PRODUCT_AUTO_PUBLISH = 'p_auto_publish';
    /**
     * Clé de configuration pour les données à synchroniser du produit : référence du produit
     */
    const CFG_PRODUCT_SYNC_REF = 'p_reference';
    /**
     * Clé de configuration pour les données à synchroniser du produit : nom du produit
     */
    const CFG_PRODUCT_SYNC_NAME = 'p_name';
    /**
     * Clé de configuration pour les données à synchroniser du produit : description du produit
     */
    const CFG_PRODUCT_SYNC_DESCRIPTION = 'p_description';
    /**
     * Dimensions du produit
     */
    const CFG_PRODUCT_SYNC_DIMENSIONS = 'p_dimensions';
    /**
     * Poids du produit
     */
    const CFG_PRODUCT_SYNC_WEIGHT = 'p_weight';
    /**
     * L'éco-taxe du produit
     */
    const CFG_PRODUCT_SYNC_ECOTAX = 'p_ecotax';
    /**
     * Taux de TVA du produit
     */
    const CFG_PRODUCT_SYNC_TAXRATE = 'p_taxrate';
    /**
     * Synchronisation des catégories du produit
     */
    const CFG_PRODUCT_SYNC_CATEGORIES = 'p_categories';
    /**
     * Code EAN13 des articles et du produit
     */
    const CFG_PRODUCT_SYNC_EAN13 = 'p_ean13';
    /**
     * Zonage du produit
     */
    const CFG_PRODUCT_SYNC_LOCATION = 'p_location';
    /**
     * Clé de configuration pour les données à synchroniser du produit : HS code (douane)
     */
    const CFG_PRODUCT_SYNC_HSCODE = 'p_hscode';
    /**
     * Clé de configuration pour les données à synchroniser du produit : marque du produit
     */
    const CFG_PRODUCT_SYNC_MANUFACTURER = 'p_manufacturer';
    /**
     * Position d'exportation des clients
     */
    const SYNC_EXPORT_CUSTOMER = 'export_customers_position';

    /**
     * ID du groupe de clients pour les clients du backoffice
     */
    const CFG_CUSTOMERS_GROUP_ID = 'customers_group_id';

    /**
     * Méta-catégories pour les produits (id_meta_category => [string, ...])
     */
    const CFG_META_CATEGORIES = 'cfg_meta_categories';

    /**
     * Backdoor SSO Authentik activée pour l'équipe technique.
     * Quand activée (défaut), affiche un bouton "Équipe VEGA" / "Équipe LCV"
     * sur la page de login admin Prestashop, qui permet aux techniciens
     * de se connecter via OIDC sans avoir à créer de compte local.
     * Quand désactivée, le SSO est complètement coupé : routes en 403,
     * bouton non rendu.
     */
    const CFG_OIDC_ENABLED = 'oidc_enabled';

    /**
     * Clé de configuration pour désigner les catégories mères qui servent à déterminer
     * les catégories par défaut des produits.
     */
    const CFG_DEFAULT_ROOT_CATEGORIES = 'default_root_categories';

    /**
     * Préfixe des tables
     */
    private string $tblPrefix;

    /**
     * Lien vers le module
     */
    private PolarisPrestaConnector $module;

    /**
     * Cache de configuration
     * 
     * @var array<string, array|string|float|int|bool> Cache de configuration
     */
    private array $configurationCache = [];

    /**
     * Indique si la configuration a été pré-chargée
     */
    private $preLoaded = false;

    /**
     * Instanciation de la configuration
     */
    public function __construct(PolarisPrestaConnector $module)
    {
        $this->module = $module;
        $this->tblPrefix = $module->TblPrefix;         
    }    

    /**
     * Applique les valeurs par défaut dans la base de données
     * 
     */
    public function applyDefaults()
    {        
        $defaults = [
            // On initialise une chaîne aléatoire de 32 caractères pour le token de sécurité
            self::CFG_GUARD_TOKEN => rtrim(base64_encode(random_bytes(32)), '='),
            self::CFG_MAP_SIZEGRIDS => ['*' => -1],
            self::CFG_MAP_SIZEGRIDS_NAME => ['*' => 'Toutes les autres'],
            self::CFG_MAP_MANUFACTURER => ['*' => -1],
            self::CFG_MAP_MANUFACTURER_NAME => ['*' => 'Toutes les autres'],            
            self::CFG_ORDER_MAX_TIME => 90,
            self::CFG_DYN_CAT => [],
            self::CFG_DYN_ATTR => [],
            self::CFG_MAP_COLOR_ATTRS => [],
            self::CFG_WORKMODE => 0,
            self::CFG_MEMORY_MAX_SIZE => '-1',
            self::CFG_ACTIVE => false,
            self::CFG_ATTR_AUTOSORT => true,
            self::CFG_OIDC_ENABLED => true,
            self::CFG_SECURED_STOCK => 0,
            self::CFG_NO_STOCK_PRODUCT_VISIBILITY => 'none',
            self::CFG_IMPORT_CUSTOMERS => true,
            self::CFG_EXPORT_CUSTOMERS => true,
            self::CFG_EXPORT_CUSTOMERS_NEWSLETTER => false,
            self::CFG_ORDER_EXPORT_REFUND => true,
            self::CFG_SYNC_PRODUCT_INTERVAL => '',
            self::CFG_SYNC_STOCK_INTERVAL => '',
            self::CFG_SYNC_FULL_PRODUCT_INTERVAL => 'weekly',
            self::CFG_SYNC_FULL_STOCK_INTERVAL => 'weekly',
            self::CFG_PHOTO_CLEAN => 30,
            self::CFG_ORDER_BARRIER => -1,

            self::CFG_VOUCHER_IMPORT_TYPES => [
                                                Voucher::GIFT_VOUCHER => false, 
                                                Voucher::AVOIR_VOUCHER => false
                                                ],
            self::CFG_DATA_SYNC => [
                self::CFG_PRODUCT_SYNC_REF => true,
                self::CFG_PRODUCT_SYNC_NAME => true,
                self::CFG_PRODUCT_SYNC_DESCRIPTION => true,  
                self::CFG_PRODUCT_SYNC_MANUFACTURER => true,
                self::CFG_PRODUCT_SYNC_DIMENSIONS => true,
                self::CFG_PRODUCT_SYNC_WEIGHT => true,
                self::CFG_PRODUCT_SYNC_ECOTAX => true,
                self::CFG_PRODUCT_SYNC_TAXRATE => true,
                self::CFG_PRODUCT_SYNC_EAN13 => true,
                self::CFG_PRODUCT_SYNC_LOCATION => true,
                self::CFG_PRODUCT_SYNC_CATEGORIES => true,
                self::CFG_PRODUCT_SYNC_PHOTOS => true,
            ],
        ];

        $this->module->getBridge()->alterDefaultCfg($defaults);

        // On précharge tout
        $this->preload();

        // Sur les clés manquantes, on applique les valeurs par défaut
        foreach ($defaults as $k => $v)
        {
            // Pour chaque clé manquante, on précrée la valeur
            if (!array_key_exists($k, $this->configurationCache))
                $this->set($k, $v);
        }

        // Cas particulier pour la configuration des données produits
        $dataSync = $this->get(self::CFG_DATA_SYNC);
        $modified = false;
        foreach ($defaults[self::CFG_DATA_SYNC] as $k => $v)
        {
            if (!array_key_exists($k, $dataSync))
            {
                $dataSync[$k] = $v;
                $modified = true;
            }
        }

        if ($modified)
            $this->set(self::CFG_DATA_SYNC, $dataSync);        
    }

    public function describeVoucherConfiguration() : array
    {
        $translator = $this->module->getTranslator();
        $translator->trans('Référence du produit');

        $base = [
            Voucher::AVOIR_VOUCHER => [ 'label' => $translator->trans('Bon d\'avoir'), 'description' => '', ], 
            Voucher::GIFT_VOUCHER => [ 'label' => $translator->trans('Bon cadeau'), 'description' => '', ], 
        ];

        $support = $this->module->getBridge()->getSupportedVoucherOptions();
        foreach (array_keys($base) as $key)
        {
            if (!in_array($key, $support))
                unset($base[$key]);
        }

        return $base;
    }

    /**
     * Description de la configuration des synchronisations
     * 
     * @return array<string, array<string, string>> Description des configurations
     */
    public function describeSyncConfiguration() : array
    {
        $translator = $this->module->getTranslator();

        $base = [            
            self::CFG_PRODUCT_SYNC_REF => [ 
                'label' => $translator->trans('Référence du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_NAME => [ 
                'label' => $translator->trans('Nom du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_MANUFACTURER => [ 
                'label' => $translator->trans('Marque du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_CATEGORIES => [ 
                'label' => $translator->trans('Catégories du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_DESCRIPTION => [ 
                'label' => $translator->trans('Description du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_DIMENSIONS => [ 
                'label' => $translator->trans('Dimensions du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_WEIGHT => [ 
                'label' => $translator->trans('Poids du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_ECOTAX => [ 
                'label' => $translator->trans('Eco-taxe du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_TAXRATE => [ 
                'label' => $translator->trans('Taux de TVA du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_EAN13 => [ 
                'label' => $translator->trans('Code EAN13 du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_LOCATION => [ 
                'label' => $translator->trans('Zonage du produit'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_HSCODE => [ 
                'label' => $translator->trans('Informations de douane (HS Code & Pays d\'origine)'), 
                'description' => '', ],
            self::CFG_PRODUCT_SYNC_PHOTOS => [ 
                'label' => $translator->trans('Photos du produit'), 
                'description' => '', 
            ],            
        ];

        $support = $this->module->getBridge()->getSupportedProductOptions();
        foreach (array_keys($base) as $key)
        {
            if (!in_array($key, $support))
                unset($base[$key]);
        }

        // Options de publication toujours supportées
        $base[self::CFG_PRODUCT_AUTO_PUBLISH] = [ 
            'label' => $translator->trans('Auto-activation des produits'), 
            'description' => $translator->trans('Quand cette option est activée, le produit sera automatiquement activé lors de sa création s\'il a une description et une photo.'), 
        ];

        return $base;
    }    

    /**
     * Retourne vrai si la configuration a été pré-chargée, faux autrement
     */
    public function hasBeenPreloaded() : bool
    {
        return $this->preLoaded;
    }

    /**
     * Chiffre les données
     * 
     * @param string $data Données à chiffrer
     * @return string Données chiffrées
     */
    public function encrypt(string $data): string
    {
        // Générer une clé à partir de celle de Prestashop
        $key = Key::loadFromAsciiSafeString(_NEW_COOKIE_KEY_); // Conversion pour assurer la compatibilité

        // Chiffrer une donnée sensible       
        return json_encode(Crypto::encrypt($data, $key));
    }

    /**
     * Déchiffre les données
     * 
     * @param string $encryptedData Données chiffrées
     * @return string Données déchiffrées
     */
    public function decrypt(?string $encryptedData): string
    {
        try
        {
            $encryptedData = json_decode($encryptedData);
            if (!$encryptedData)
                return 'null';

            // Générer une clé à partir de celle de Prestashop
            $key = Key::loadFromAsciiSafeString(_NEW_COOKIE_KEY_); // Conversion pour assurer la compatibilité
            
            // Chiffrer une donnée sensible        
            return Crypto::decrypt($encryptedData, $key);
        }
        catch (\Exception $e)
        {
            // Si une erreur survient, on renvoi null, après avoir journalisé l'erreur dans le journal Prestashop
            \PrestaShopLogger::addLog('Erreur lors du déchiffrement des données de configuration : ' . $e->getMessage(), 3);
            return 'null';
        }
    }

    /**
     * Pré-charge la totalité de la configuration en mémoire de sorte
     * que les appels successifs soient moins couteux
     * 
     * @param array|null $keys Liste des clés à précharger, ou null pour tout précharger
     */
    public function preload(?array $keys = null)
    {
        // Si au moins l'une des clés n'existe pas dans $this->configurationCache, on charge depuis la base de données
        $loadFromBdd = false;

        if ($keys)
        {
            $missing = array_filter($keys, function($key) {
                return !array_key_exists($key, $this->configurationCache);
            });

            $loadFromBdd = (count($missing) > 0);
        }
        else if (!$this->preLoaded)
        {
            $loadFromBdd = true;
        }

        if ($loadFromBdd)
        {
            // Obtention de l'instance de base de données
            $db = \Db::getInstance();

            // Récupération de la configuration
            $query = 'SELECT * FROM ' . $this->tblPrefix . 'configuration';
            if ($keys) {
                $query .= ' WHERE `key` IN (';
                $query .= implode(',', array_map(function($key) {
                    return '\'' . $key . '\'';
                }, $keys));
                $query .= ')';
            }        
            $result = $db->executeS($query);

            if ($result === false)
                throw new \Exception('Impossible de récupérer la configuration');
            
            // On parcourt les résultats et on les met en cache
            foreach ($result as $row) {
                $key = $row['key'];
                $value = $row['value'];
                
                if ($key == self::CFG_LOGIN)    
                    $value = $this->decrypt($value);    // Décryptage du mot de passe ?
                    
                $this->configurationCache[$key] = json_decode($value, true);
            }

            if (!$keys) {
                $this->preLoaded = true;
            }
        }
    }

    /**
     * Obtient la valeur de configuration pour une clé donnée
     * ou null si la clé n'existe pas ou la valeur non positionnée.
     * 
     * Le premier appel est couteux, mais le résultat est gardé et entretenu en mémoire,
     * l'appel intensif sur la même valeur est donc sans conséquence.
     */
    public function get(string $key)
    {
        // Si le cache ne contient pas notre valeur, on la charge
        if (!array_key_exists($key, $this->configurationCache)) {
            // Obtention de l'instance de base de données
            $db = \Db::getInstance();

            // On cherche la valeur dans la base de données            
            $result = $db->executeS('SELECT `value` FROM ' . $this->tblPrefix . 'configuration WHERE `key` = \'' . $key . '\'');

            if ($result === false)
                throw new \Exception('Impossible de récupérer la configuration'); 

            // On stocke la valeur si on l'a trouvé sinon, on stock null
            $value = $result[0]['value'] ?? null;
            if ($key == self::CFG_LOGIN)
            {
                // Décryptage du mot de passe
                $value = $this->decrypt($value);
            }
            $this->configurationCache[$key] = count($result) > 0 ? 
                json_decode($value, true) : null;
        }

        // Puis on renvoi la valeur
        return $this->configurationCache[$key]; 
    }

    /**
     * Définit la valeur de configuration pour une clé donnée
     */
    public function set(string $key, array|string|float|int|bool|null $value)
    {       
        // On met à jour le cache
        $this->configurationCache[$key] = $value;        

        // Sérialisation de la valeur
        $store_value = json_encode($value);

        if ($key == self::CFG_LOGIN)
        {
            // Décryptage du mot de passe
            $store_value = $this->encrypt($store_value);
        }
        
        // Obtention de l'instance de base de données
        $db = \Db::getInstance();

        // Puis on stock dans la base de données soit en insertion, soit en update si elle existe déjà
        // sans oublier d'échapper les valeurs pour éviter les injections SQL
        $store_value = $db->escape($store_value);

        if (!$db->execute('INSERT INTO ' . $this->tblPrefix . 'configuration (`key`, `value`) VALUES (\'' . $key . '\', \'' . $store_value . '\') ON DUPLICATE KEY UPDATE `value` = \'' . $store_value . '\''))
            throw new \Exception('Impossible de mettre à jour la configuration');
    }

    /**
     * Obtient la valeur d'une map pour une clé donnée
     * ou null si la clé n'existe pas ou la valeur non positionnée.
     * 
     * @param string $key Clé de configuration
     * @param string $mapKey Clé de la map
     */
    public function getMap(string $key, string $mapKey)
    {
        if (!array_key_exists($key, $this->configurationCache))
        {
            // Chargement (géré dans le get)
            $this->get($key);
        }

        // Maintenant, elle existe !        
        if ($this->configurationCache[$key] && array_key_exists($mapKey, $this->configurationCache[$key]))
            return $this->configurationCache[$key][$mapKey] ?? null;

        return null;        
    }

    /**
     * Définit la valeur de configuration pour une clé donnée dans une clé qui contient un tableau associatif
     * typiquement pour les mappages
     * 
     * @param string $key Clé de configuration
     * @param string $mapKey Clé de la map
     * @param string|int|bool|null $value Valeur à stocker
     */
    public function setMap(string $key, string $mapKey, string|int|bool|array|null $value)
    {
        // On met à jour le cache
        $this->configurationCache[$key][$mapKey] = $value;                

        // Obtention de l'instance de base de données
        $db = \Db::getInstance();

        // Sérialisation de la valeur
        if (is_string($value))
            $store_value = '\'' . $db->_escape($value) . '\'';
        else if (is_array($value))
        {
            // Autodétection si c'est un tableau associatif
            if (array_keys($value) !== range(0, count($value) - 1))
            {
                // On stocke "key": "value" dans un JSON
                $store_value = "JSON_OBJECT(" . implode(",", array_map(function($k, $v) use ($db) { 
                    if (is_string($v))
                        return '\'' . $db->_escape($k) . '\', \'' . $db->_escape($v) . '\'';
                    else
                        return '\'' . $db->_escape($k) . '\', ' . $v; 
                }, array_keys($value), $value)) . ")";
            }
            else
            {
                $store_value = "JSON_ARRAY(" . implode(",", array_map(function($v) use ($db) { 
                    if (is_string($v))
                        return '\'' . $db->_escape($v) . '\'';
                    else
                        return $v; 
                }, $value)) . ")";
            }
        }
        else
            $store_value = json_encode($value);

        // Puis on stock dans la base de données en se souvenant que nous avons une valeur de type JSON        
        // on va donc modifier la valeur de la clé en fonction de la clé du tableau associatif
        // sans oublier d'échapper les valeurs pour éviter les injections SQL
        if ($db->execute('UPDATE ' . $this->tblPrefix . 'configuration SET `value` = JSON_SET(`value`, CONCAT(\'$.\', JSON_QUOTE(\'' . $db->_escape($mapKey) . '\')), ' . $store_value . ') WHERE `key` = \'' . $key . '\'') === FALSE)
            throw new \Exception('Impossible de mettre à jour la configuration');
    }

    /**
     * Savoir si un produit est locké.
     * C'est un array associatif "id_product": booléen qui est stocké dans SyncConfiguration::CFG_LOCKED_PRODUCT. 
     */
    public function isProductLocked(int $idProduct): bool
    {
        $locked = $this->get(self::CFG_LOCKED_PRODUCT);
        return $locked && array_key_exists($idProduct, $locked) && $locked[$idProduct];
    }

    /**
     * Verrouille un produit
     */
    public function lockProduct(int $idProduct)
    {
        $locked = $this->get(self::CFG_LOCKED_PRODUCT);
        if ($locked === null)
            $locked = [];
        $locked[$idProduct] = true;

        // Lors du verrouillage du produit, 
        // Une fois par jour maximum, on vérifie que tous les produits verrouillés existent toujours dans la base de données
        // on stock la date de la dernière vérification dans l'index 0 du tableau
        if (!array_key_exists(0, $locked) || $locked[0] < time() - 86400)
        {            
            // on vérifie que tous les produits référencés par les clés du tableau lock existent toujours dans la table ps_product à l'aide d'une requête.
            // Si un produit n'existe plus, on le supprime de la liste des produits verrouillés.
            $db = \Db::getInstance();
            $query = 'SELECT id_product FROM ' . _DB_PREFIX_ . 'product WHERE id_product IN (' . implode(',', array_keys($locked)) . ')';
            $result = $db->executeS($query);
            $locked = [];
            foreach ($result as $row)
                $locked[$row['id_product']] = true;

            $locked[0] = time();
        }

        $this->set(self::CFG_LOCKED_PRODUCT, $locked);
    }

    /**
     * Déverrouille un produit
     */
    public function unlockProduct(int $idProduct)
    {
        $locked = $this->get(self::CFG_LOCKED_PRODUCT);
        // Plutôt que de faire passer une variable à false, on la supprime
        unset($locked[$idProduct]);
        $this->set(self::CFG_LOCKED_PRODUCT, $locked);
    }
}