<?php 

namespace PrestaShop\Module\PolarisPrestaConnector\Controller;

use Context;
use PolarisPrestaConnector;
use PrestaShop\Module\PolarisPrestaConnector\SyncerProducts;
use PrestaShop\Module\PolarisPrestaConnector\Forms\CategoriesMappingType;
use PrestaShop\Module\PolarisPrestaConnector\Forms\ColorMapType;
use PrestaShop\Module\PolarisPrestaConnector\Forms\MappingType;
use Symfony\Component\HttpFoundation\Request;
use PrestaShop\Module\PolarisPrestaConnector\SyncConfiguration;
use PrestaShop\Module\PolarisPrestaConnector\Syncer;
use PrestaShop\Module\PolarisPrestaConnector\SyncerDiscounts;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Response;

class MappageAdminController extends AdminController
{
    /**
     * Constructeur.
     * On injecte le router par injection de dépendance dans le constructeur
     */
    public function __construct(
        private \Symfony\Component\Routing\RouterInterface $router)
    {        
        parent::__construct();  

        $this->router = $router;
    }

    /**
     * On enrechit la configuration de la barre d'outils
     * 
     * @return array
     */
    protected function getAdminLayoutToolBar() : array 
    {
        $toolbar = parent::getAdminLayoutToolBar();

        $toolbar['settings'] = [
                    'href' => $this->generateUrl($this->module->name . '_configuration'),
                    'class' => 'btn-outline-secondary pc-toolbar-button',
                    'desc' => 'Paramètres généraux',
                    'help' => 'Configurer les paramètres généraux',
                    'icon' => 'settings',                    
                ];    

        return $toolbar;
    }

    /**
     * Retourne un tableau initialisé à label pour toutes les langues gérées par le système
     * 
     */
    protected function setLanguages(string $label) : array
    {
        $langs = [];

        foreach (Context::getContext()->controller->getLanguages() as $lang)
            $langs[$lang['id_lang']] = $label;

        return $langs;
    }

    /**
     * Retourne les variables passées en paramètre enrichies des variables par défaut
     * 
     * @param array $vars Les variables à enrichir
     */
    protected function addDefaultVars(array $vars) : array
    {
        $vars = parent::addDefaultVars($vars);
        
        // chargement des noms des catégories dynamique depuis la configuration
        $vars['categoryTypes'] = $this->module->getCfg()->get(SyncConfiguration::CFG_DYN_CAT);
        
        return $vars;
    }

    /**
     * Charge et retourne les données de mapping
     * 
     * @param string $cfgKey La clé de configuration à utiliser
     * @param array $labels Les labels des éléments à mapper
     * 
     * @return array Le tableau de données [ 'key' => 'value', ... ]
     */
    private function &loadMappingData(string $cfgKey, array &$labels) : array
    {
        // Rechargement
        $data = $this->module->getCfg()->get($cfgKey) ?? [];

        // On tri le tableau data en fonction d'une fonction anonyme
        uksort($data, function($a, $b) use ($labels) {
            if ($a == '*') return -1;
            if ($b == '*') return 1;
            return strcmp($labels[$a] ?? $a, $labels[$b] ?? $b);
        });

        return $data;
    }

    /**
     * Redirection vers la page suivante dans le mapping
     * 
     * @param Request $request
     * @param array $data Le tableau de données complet
     * @param string $key La clé courante
     */
    private function nextMappingPage(Request $request, &$labels, $cfgKey, $key, ?array $routeArgs = null) : Response
    {
        // Redirection vers le même contrôleur avec l'id suivant
        // Locate current $key in $data key, and take the next one
        $data = $this->loadMappingData($cfgKey, $labels);
        $allKeys = array_keys($data);
        $currentIndex = array_search($key, $allKeys);
        $nextKey = $currentIndex !== false && isset($allKeys[$currentIndex + 1]) ? $allKeys[$currentIndex + 1] : null;

        if (!$routeArgs)
            $routeArgs = [];

        if ($nextKey)
        {
            $routeArgs['id'] = $nextKey;
            return $this->redirectToRoute($request->attributes->get('_route'), $routeArgs);
        }
        else
            return $this->redirectToRoute($request->attributes->get('_route'), $routeArgs);
    }
    
    /**
     * Gestion du mappage des grilles de taille
     */
    public function mapSizeGridAction(Request $request)
    {
        // En mode affichage => pas de form
        $form = null;
        $mappings = null;
        
        $cfgKey = SyncConfiguration::CFG_MAP_SIZEGRIDS;
        $labels = $this->module->getCfg()->get(SyncConfiguration::CFG_MAP_SIZEGRIDS_NAME) ?? [ '*' => 'Toutes les autres' ];
        $noImportText = 'Ne pas importer les produits avec cette grille de tailles';
        
        $choices = [ $noImportText => -1 ];
        $choices_available = ['-1' => $noImportText];

        foreach(\AttributeGroup::getAttributesGroups(Context::getContext()->language->id) as $item) {
            // Ajouter chaque nom de groupe dans le tableau de choix
            $choices[$item['name'] . ' ('.$item['id_attribute_group'].')'] = $item['id_attribute_group'];   
            $choices_available[$item['id_attribute_group']] = $item['name'];
        }

        $label = null;        

        if ($request->get('id') !== null)
        {
            $key = $request->get('id');            
            $data = $this->module->getCfg()->get($cfgKey) ?? [ '*' => -1 ];            
            if (!array_key_exists($key, $data))
                throw new \Exception('Grille de tailles inconnue !');

            $label = $labels[$key] ?? $key;

            // Edition !
            $form = $this->createForm(MappingType::class, [
                            'cancel' => $this->generateUrl($request->attributes->get('_route')),
                            'id' => $key,
                            'label' => $label,
                            'choice' => $data[$key] ?? null,                            
                            'choices' => $choices,
                            'no-import' => $noImportText,
                            'placeholder' => 'Sélectionner un groupe d\'attributs',
                            ]);
            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) 
            {
                $data = $form->getData();                               
    
                // On enregistre
                $this->module->getCfg()->setMap($cfgKey, $key, $data['choice'] ?? 0);                

                $this->addFlash('success', 'Configuration de mapping sauvegardée !');

                // Si c'est une demande d'enregistrement + suivant
                if ($form->get('actions')->get('submitAndNext')->isClicked())
                    return $this->nextMappingPage($request, $labels, $cfgKey, $key);

                // Puis on passe en mode affichage
                $form = null;
            }
        }

        if ($request->get('autocreate') !== null)
        {
            $key = $request->get('autocreate');            
            $data = $this->module->getCfg()->get($cfgKey) ?? [ '*' => -1 ];

            if (!array_key_exists($key, $data))
                throw new \Exception('Grille de tailles inconnue !');

            $label = $labels[$key] ?? $key;
            // On retire l'id quand le label commence par "<num> - vrai-label"
            $label = preg_replace('/^'.$key.' - /', '', $label);

            // On cherche parmi les groupes d'attributs existants un groupe avec le même nom
            $existent = null;
            foreach($choices_available as $id => $name){
                if ($name == $label)
                {
                    $existent = $id;
                    break;
                }
            }

            // Si trouvé, on map dessus
            if ($existent)
            {
                $this->module->getCfg()->setMap($cfgKey, $key, $existent);
                $this->addFlash('success', 'Groupe d\'attributs existant trouvé et mappé !');
            }
            else
            {
                // Sinon, on crée un nouveau groupe
                $newItem = new \AttributeGroup();
                $newItem->name = $this->setLanguages($label);
                $newItem->public_name = $this->setLanguages($label);
                $newItem->is_color_group = 0;
                $newItem->group_type = 'select';
                $newItem->add();

                // On ajoute le groupe au cache pour le display
                $choices_available[$newItem->id] = $label;

                $this->module->getCfg()->setMap($cfgKey, $key, $newItem->id);
                $this->addFlash('success', 'Groupe d\'attributs créé et mappé !');
            }
        }
        
        if (!$form) // affichage !
        {
            // Rechargement des données
            $data = $this->loadMappingData($cfgKey, $labels);
            if (!$data)
                $data = ['*' => -1];

            // Affichage :
            // [ [ 'key' => '', 'value' => ''], ...]
            $mappings = [];                        
                    
            foreach($data as $key => $value)
            {
                // On encode la clé avec base64 sans = pour éviter les problèmes de clé
                $mappings[] = [
                    'key' => $key,
                    'title' => isset($labels[$key]) ? $labels[$key] : $key,
                    'value' => $value && $value != 0 ? $value : null,
                    'value_title' => $value !== null && isset($choices_available[$value]) ? 
                        ($value > 0 ?
                            '<a href="'.$this->router->generate('admin_attribute_groups_edit', ['attributeGroupId' => $value]).'" target="_blank">'.$choices_available[$value].' ('.$value.')</a>' : $choices_available[$value])
                        : null,
                ];                
            }
        }

        // get current route        
        $backoffice = $this->module->getBridge()->getId();

        $mandatory = "<p>Le mapping des grilles de tailles est <strong>obligatoire</strong> pour qu'un produit puisse être importé.</p>";

        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => 'Grille de tailles '.$backoffice,
            'map_value' => 'Groupe d\'attributs Prestashop associé',
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => '<p>Pour chaque grille de tailles de '.$backoffice.' (colonne de gauche), vous pouvez affecter un groupe d\'attributs Prestashop dans lesquelles seront créées les tailles. <br>Quand une grille n\'est pas mappée, c\'est le mappage spécial <code>Toutes les autres</code> qui est utilisé.</p>'.$mandatory,
            'help_edit' => '<p>Vous éditez actuellement le mapping de la grille de tailles <strong>'.$label.'</strong> de '.$backoffice.' sur Prestashop. <br>Sélectionnez un groupe d\'attributs dans lequel créer les tailles de cette grille (chaque taille créant automatiquement un attribut dans le groupe sélectionné).</p>',
            'help_automapping' => 'Créer un groupe d\'attributs automatiquement et le mapper pour cette grille de tailles',
            'mappings' => $mappings,
            'form' => $form ? $form->createView() : null,
        ]));
    }

    /**
     * Gestion du mappage des marques
     */
    public function mappingManufacturerAction(Request $request)
    {
        // En mode affichage => pas de form
        $form = null;
        $mappings = null;
        
        $cfgKey = SyncConfiguration::CFG_MAP_MANUFACTURER;
        $labels = $this->module->getCfg()->get(SyncConfiguration::CFG_MAP_MANUFACTURER_NAME);
        $noImportText = 'Ne pas importer les produits de cette marque';
        
        $choices = [ $noImportText => -1 ];
        $choices_available = ['-1' => $noImportText];

        foreach(\Manufacturer::getManufacturers(false, 0, false) as $item) 
        {
            // Ajouter chaque nom de groupe dans le tableau de choix
            $choices[$item['name'] . ' ('.$item['id_manufacturer'].')'] = $item['id_manufacturer'];   
            $choices_available[$item['id_manufacturer']] = $item['name'];
        }

        $label = null;

        if ($request->get('id') !== null)
        {
            $key = $request->get('id');            
            $data = $this->module->getCfg()->get($cfgKey);            
            if (!array_key_exists($key, $data))
                throw new \Exception('Marque inconnue !');

            $label = $labels[$key] ?? $key;            

            // Edition !
            $form = $this->createForm(MappingType::class, [
                            'cancel' => $this->generateUrl($request->attributes->get('_route')),
                            'id' => $key,
                            'label' => $label,
                            'choice' => $data[$key] ?? null,
                            'choices' => $choices,
                            'no-import' => $noImportText,
                            'placeholder' => 'Sélectionner une marque',
                            ]);
            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) 
            {
                $data = $form->getData();                               
    
                // On enregistre
                $this->module->getCfg()->setMap($cfgKey, $key, $data['choice'] ?? 0);
    
                $this->addFlash('success', 'Configuration de mapping sauvegardée !');

                // Si c'est une demande d'enregistrement + suivant
                if ($form->get('actions')->get('submitAndNext')->isClicked())
                    return $this->nextMappingPage($request, $labels, $cfgKey, $key);

                // Sinon, on passe en mode affichage
                $form = null;
            }
        }

        if ($request->get('autocreate') !== null)
        {
            $key = $request->get('autocreate');            
            $data = $this->module->getCfg()->get($cfgKey);

            if (!array_key_exists($key, $data))
                throw new \Exception('Marque inconnue !');

            $label = $labels[$key] ?? $key;
            // On retire l'id quand le label commence par "<num> - vrai-label"
            $label = preg_replace('/^'.$key.' - /', '', $label);

            // On cherche parmi les marques existantes une marque avec le même nom
            $existent = null;
            foreach($choices_available as $id => $name){
                if ($name == $label)
                {
                    $existent = $id;
                    break;
                }
            }

            // Si trouvé, on map dessus
            if ($existent)
            {
                $this->module->getCfg()->setMap($cfgKey, $key, $existent);
                $this->addFlash('success', 'Marque existante trouvée et mappée !');
            }
            else
            {
                // Sinon, on crée un nouveau groupe
                $newItem = new \Manufacturer(null, Context::getContext()->language->id);
                $newItem->name = $label;
                $newItem->active = 1;
                $newItem->add();

                // On ajoute la marque au cache pour le display
                $choices_available[$newItem->id] = $newItem->name;

                $this->module->getCfg()->setMap($cfgKey, $key, $newItem->id);
                $this->addFlash('success', 'Marque créée et mappée !');
            }
        }
        
        if (!$form) // affichage !
        {
            // Rechargement
            $data = $this->loadMappingData($cfgKey, $labels);
            if (!$data)
                $data = ['*' => -1];

            // Affichage :
            // [ [ 'key' => '', 'value' => ''], ...]
            $mappings = [];            

            foreach($data as $key => $value)
            {
                // On encode la clé avec base64 sans = pour éviter les problèmes de clé
                $mappings[] = [
                    'key' => $key,
                    'title' => isset($labels[$key]) ? $labels[$key] : $key,
                    'value' => $value && $value != 0 ? $value : null,
                    'value_title' => $value !== null && isset($choices_available[$value]) ? 
                        ($value > 0 ? '<a href="'.$this->router->generate('admin_manufacturers_edit', ['manufacturerId' => $value]).'" target="_blank">'.$choices_available[$value].' ('.$value.')</a>' : $choices_available[$value])
                         : null,
                ];                
            }
        }

        $mandatory = "<p>Le mapping des marques est <strong>obligatoire</strong> pour qu'un produit puisse être importé.</p>";

        // get current route        
        $backoffice = $this->module->getBridge()->getId();
        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => 'Marque '.$backoffice,
            'map_value' => 'Marque Prestashop associée',
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => '<p>Pour chaque marque '.$backoffice.' (colonne de gauche), vous pouvez affecter une marque Prestashop. <br>Quand une marque n\'est pas mappée, c\'est le mappage spécial <code>Toutes les autres</code> qui est utilisé.</p>'.$mandatory,
            'help_edit' => '<p>Vous éditez actuellement le mapping de la marque <strong>'.$label.'</strong> de '.$backoffice.' sur Prestashop. <br>Sélectionnez une marque Prestashop à utiliser.</p>',
            'help_automapping' => 'Créer une marque automatiquement et la mapper pour cette marque.',
            'mappings' => $mappings,
            'form' => $form ? $form->createView() : null,
        ]));
    }

    /**
     * Génère un link_rewrite unique pour une catégorie.
     *
     * @param string $link_rewrite Le link_rewrite souhaité.
     * @param int $id_lang L'ID de la langue.
     * @param int|null $id_category L'ID de la catégorie actuelle (pour exclure lors de la vérification).
     * @return string Le link_rewrite unique.
     */
    function generateUniqueLinkRewrite($link_rewrite, $id_lang, $id_category = null)
    {
        $original_link_rewrite = $link_rewrite;
        $suffix = 1;

        while ($this->linkRewriteExists($link_rewrite, $id_lang, $id_category)) {
            $link_rewrite = $original_link_rewrite . '-' . $suffix;
            $suffix++;
        }

        return $link_rewrite;
    }

    /**
     * Vérifie si un link_rewrite existe déjà pour une catégorie dans une certaine langue.
     *
     * @param string $link_rewrite Le link_rewrite à vérifier.
     * @param int $id_lang L'ID de la langue.
     * @param int|null $id_category L'ID de la catégorie actuelle (pour exclure lors de la vérification).
     * @return bool True si le link_rewrite existe déjà, sinon False.
     */
    function linkRewriteExists($link_rewrite, $id_lang, $id_category = null)
    {
        $sql = new \DbQuery();
        $sql->select('cl.id_category');
        $sql->from('category_lang', 'cl');
        $sql->where('cl.link_rewrite = \'' . pSQL($link_rewrite) . '\'');
        $sql->where('cl.id_lang = ' . (int) $id_lang);

        if ($id_category !== null) {
            $sql->where('cl.id_category != ' . (int) $id_category);
        }

        $result = \Db::getInstance()->getValue($sql);

        return (bool) $result;
    }
    
    /**
     * Gestion du mappage des categories
     */
    public function mappingCategoryAction(Request $request)
    {
        $cfg = $this->module->getCfg();
        $cat_types = $cfg->get(SyncConfiguration::CFG_DYN_CAT);

        $cat_type = $request->get('type');
        if ($cat_type === null)
        {
            // Redirection sur la même URL avec le premier type de catégorie renseigné en paramètre
            $cat_type = array_key_first($cat_types);
            return $this->redirectToRoute($this->module->name.'_map_typed_category', ['type' => $cat_type]);
        }

        if (!array_key_exists($cat_type, $cat_types))
            throw new \Exception('Type de catégorie inconnu !');
        
        $cat_type_name = $cat_types[$cat_type] ?? $cat_type;

        // En mode affichage => pas de form
        $form = null;
        $mappings = null;
        
        $cfgKey = SyncConfiguration::CFG_MAP_DYN_CAT . $cat_type;

        $labels = $cfg->get(SyncConfiguration::CFG_MAP_DYN_CAT . $cat_type . '_name');
        $extDatas = $cfg->get(SyncConfiguration::CFG_MAP_DYN_CAT . $cat_type . '_exdata');        

        $noImportText = 'Ne pas importer les produits de cette catégorie';
        
        $choices = [ $noImportText => -1 ];
        $choices_available = ['-1' => $noImportText];

        foreach(\Category::getAllCategoriesName(null, false, false) as $item) 
        {
            // Ajouter chaque nom de groupe dans le tableau de choix
            $choices[$item['name'] . ' ('.$item['id_category'].')'] = $item['id_category'];   
            $choices_available[$item['id_category']] = $item['name'];
        }

        $label = null;  

        if ($request->get('id') !== null)
        {
            $key = $request->get('id');            
            $data = $cfg->get($cfgKey);            
            if (!array_key_exists($key, $data))
                throw new \Exception('Catégorie inconnue !');

            $label = $labels[$key] ?? $key; 

            // Edition !
            $currentvalue = $data[$key] ?? null;            
            if ($currentvalue)
            { 
                if (!is_array($currentvalue))
                    $currentvalue = [$currentvalue];
            }

            $extended_data = [
                'default_weight' => null,
            ];

            // Lecture des données étendues
            foreach (array_keys($extended_data) as $ek)
            {
                $exData = $extDatas[$key] ?? null;
                if ($exData && isset($exData[$ek]))
                    $extended_data[$ek] = $exData[$ek];
            }

            $data = [
                'cancel' => $this->generateUrl($request->attributes->get('_route'), ['type' => $cat_type]),
                'id' => $key,
                'label' => $label,
                'choice' => $currentvalue,
                'choices' => $choices,                            
                'no-import' => $noImportText,
                'placeholder' => 'Sélectionner une ou plusieurs catégories',
            ];

            $form = $this->createForm(CategoriesMappingType::class, array_merge($data, $extended_data));
            $form->handleRequest($request);

            $data = $form->getData();
    
            // On enregistre
            if ($form->isSubmitted() && $form->isValid()) 
            {                
                $data = $form->getData();                               
    
                // On enregistre
                $choices = $data['choice'] ?? [];
                if (!is_array($choices))
                    $choices = [$choices];

                // Selon la configuration de sync on modifie ou non le tableau de choix
                if (isset($data['sync']))
                {                  
                    // Ok, on a une valeur
                    if ($data['sync'])  // On doit synchroniser donc si on a -1 dans la liste, on le retire
                    {
                        if (in_array(-1, $choices))
                            $choices = array_diff($choices, [-1]);                        
                    }
                    else    // On ne doit pas synchroniser donc si on a pas -1 dans la liste, on l'ajoute
                    {
                        if (!in_array(-1, $choices))
                            $choices[] = -1;
                    }
                }

                // Données étendues
                $exDataHasBeenModified = false;
                foreach (array_keys($extended_data) as $ek)
                {
                    if (isset($data[$ek]) && $data[$ek] != $extended_data[$ek])
                    {
                        $extended_data[$ek] = $data[$ek];
                        $exDataHasBeenModified = true;
                    }
                }

                // Sauvegarde des données étendues
                if ($exDataHasBeenModified)
                {
                    // Est-ce que la table existe ?
                    if ($extDatas == null)
                    {
                        // Non => on crée la table
                        $cfg->set(SyncConfiguration::CFG_MAP_DYN_CAT . $cat_type . '_exdata', [ $key => $extended_data ]);
                    }

                    $cfg->setMap(SyncConfiguration::CFG_MAP_DYN_CAT . $cat_type . '_exdata', $key, $extended_data);
                }

                $cfg->setMap($cfgKey, $key, count($choices) > 0 ? $choices : 0);
                SyncerProducts::markForUpdate($this->module, $cat_type, $key, true);
    
                $this->addFlash('success', 'Configuration de mapping sauvegardée !');

                if ($form->get('actions')->get('submitAndNext')->isClicked())
                    return $this->nextMappingPage($request, $labels, $cfgKey, $key, ['type' => $cat_type]);

                // Puis on passe en mode affichage
                $form = null;
            }
        }

        if ($request->get('autocreate') !== null)
        {
            $key = $request->get('autocreate');            
            $data = $this->module->getCfg()->get($cfgKey);

            if (!array_key_exists($key, $data))
                throw new \Exception('Catégorie inconnue !');

            $label = $labels[$key] ?? $key;
            // On retire l'id quand le label commence par "<num> - vrai-label" et que num == $key
            $label = preg_replace('/^'.$key.' - /', '', $label);            

            // On cherche parmi les catégories existantes une catégorie avec le même nom
            $existent = null;
            foreach($choices_available as $id => $name){
                if ($name == $label)
                {
                    $existent = $id;
                    break;
                }
            }

            // Si trouvé, on map dessus
            if ($existent)
            {
                $cfg->setMap($cfgKey, $key, $existent);
                $this->addFlash('success', 'Catégorie existante trouvée et mappée !');
            }
            else
            {
                // Sinon, on crée une nouvelle catégorie
                // Localisation de la catégorie accueil
                $home = \Category::getRootCategory(Context::getContext()->language->id);

                $newItem = new \Category();
                $newItem->name = $this->setLanguages($label);

                // calcul du rewrite automatique
                $newItem->link_rewrite = [];
                // Un par langue
                foreach (Context::getContext()->controller->getLanguages() as $lang)
                    $newItem->link_rewrite[$lang['id_lang']] = $this->generateUniqueLinkRewrite(\Tools::str2url($label), $lang['id_lang']);
                
                $newItem->active = 1;
                // C'est une fille de la catégorie accueil
                $newItem->id_parent = $home->id;
                $newItem->add();

                // On ajoute la catégorie au cache pour le display
                $choices_available[$newItem->id] = $label;

                $cfg->setMap($cfgKey, $key, $newItem->id);
                $this->addFlash('success', 'Catégorie créée et mappée !');
            }

            SyncerProducts::markForUpdate($this->module, $cat_type, $key);
        }

        if (!$form) // affichage !
        {
            // Rechargement
            $data = $this->loadMappingData($cfgKey, $labels);

            // Affichage :
            // [ [ 'key' => '', 'value' => ''], ...]
            $mappings = [];

            // On tri le tableau data en fonction d'une fonction anonyme
            uksort($data, function($a, $b) use ($labels) {
                if ($a == '*') return -1;
                if ($b == '*') return 1;
                return strcmp($labels[$a] ?? $a, $labels[$b] ?? $b);
            });

            foreach($data as $key => $value)
            {
                // On encode la clé avec base64 sans = pour éviter les problèmes de clé                
                if ($value)
                {
                    $bits = [];

                    if (!is_array($value))
                        $value = [$value];

                    if (in_array(-1, $value))
                    {
                        $bits[] = '<em>'.$noImportText.'</em>';
                    }
                    else
                    {
                        foreach ($value as $v)
                        {
                            $cat_title = '';
                            if (isset($choices_available[$v]))
                                $cat_title = $choices_available[$v].' ('.$v.')';
                            else
                                $cat_title = 'Catégorie #'.$v;

                            $bits[] = '<a href="'.$this->router->generate('admin_categories_edit', ['categoryId' => $v]).'" target="_blank">'.$cat_title.'</a>';
                        }
                    }

                    if (count($bits) > 1)
                        $value_title = '<ul><li>' . implode('</li><li>', $bits) . '</li></ul>';
                    else if (count($bits) > 0)
                        $value_title = $bits[0];
                    else
                        $value_title = null;
                }
                else
                    $value_title = null;

                $mappings[] = [
                    'key' => $key,
                    'title' => isset($labels[$key]) ? $labels[$key] : $key,
                    'value' => $value && $value != 0 ? $value : null,
                    'value_title' => $value_title,
                ];                
            }
        }


        $backoffice = $this->module->getBridge()->getId();
        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => $cat_type_name.' '.$backoffice,
            'map_value' => 'Catégories Prestashop associées',            
            'cat_type' => $cat_type,
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => '<p>Pour chaque '.$cat_type_name.' '.$backoffice.' (colonne de gauche), vous pouvez affecter une ou plusieurs catégories Prestashop dans lesquelles le module rangera vos produits. <br>Quand une catégorie n\'est pas mappée, c\'est le mappage spécial <code>Toutes les autres</code> qui est utilisé.  Si le mappage spécial n\'est pas mappé, alors la donnée est simplement ignorée.</p>'
                        .'<p><em><strong>Notes&nbsp;: </strong>vous n\'avez pas besoin de faire une resynchronisation pour mettre à jour les produits concernés quand vous changez un mappage dans cette section, la réaffectation aux bonnes catégories interviendra lors de la prochaine synchronisation (en règle générale, sous 5 minutes ou lançable immédiatement en appuyant sur le bouton <i class="material-icons">play_circle</i>).</em></p>',
            'help_edit' => '<p>Vous éditez actuellement le mapping de la catégorie <strong>'.$label.'</strong> de '.$backoffice.' sur Prestashop. <br>Sélectionnez une ou plusieurs catégories Prestashop à utiliser.</p>',
            'help_automapping' => 'Créer une catégorie automatiquement et la mapper pour cette catégorie '.$backoffice.'.',
            'mappings' => $mappings,
            'form' => $form ? $form->createView() : null,
        ]));
    }

    /**
     * Gestion du mappage des nuance 
     */
    public function mappingShadeAction(Request $request){

        // En mode affichage => pas de form
        $form = null;

        // Edition !
        $form = $this->createForm(ColorMapType::class, [
                        'cancel' => $this->generateUrl($request->attributes->get('_route')),
                        ]);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) 
        {
            $data = $form->getData();                               
            if (isset($data["choice"]))
            {
                $colorMap = $this->module->getCfg()->get(SyncConfiguration::CFG_COLOR_MAP);
                if (!isset($colorMap))
                    $colorMap = [];
                if (!isset($colorMap["strict"]))
                    $colorMap["strict"] = [];

                $changed = false;
                foreach (explode("\n", $data["choice"]) as $line)
                {
                    $bits = explode(';', $line);
                    if (count($bits) > 1)
                    {
                        $color = trim($bits[0]);
                        $map = trim($bits[1]);
                        if ($color && $map && (!isset($colorMap["strict"][$color]) || $colorMap["strict"][$color] != $map))
                        {                            
                            $changed = true;
                            $colorMap["strict"][$color] = $map;
                        }
                    }
                }

                // Sauvegarde
                if ($changed)
                    $this->module->getCfg()->set(SyncConfiguration::CFG_COLOR_MAP, $colorMap);
            }

            $this->addFlash('success', 'Configuration de mapping couleur sauvegardée !');
        }

        // get current route        
        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => "Simplification des couleurs",
            'help' => '<p>Liste des correspondances de couleurs en CSV (couleur;correspondance), une correspondance par ligne</p>',
            'help_edit' => '<p>Vous éditez actuellement la correspondance des nuances vers les couleurs générique. </p>
                            <p>Complétez la liste des correspondances de couleurs en mode CSV (couleur;correspondance), une correspondance par ligne, pas d\'encapsulage des textes.</p>
                            ',
            'mappings' => null,
            'form' => $form->createView(),
        ]));
    }

    /**
     * Gestion du mappage des promos
     */
    public function mappingDiscountsAction(Request $request)
    {
        // En mode affichage => pas de form
        $form = null;
        $mappings = null;
         
        $db = \Db::getInstance();
        $label = 'promotion';
        $choices = ["Ne pas catégoriser les produits de cette promotion" => 0];
        $choices_available = [0 => "Ne pas catégoriser les produits de cette promotion"];

        foreach(\Category::getAllCategoriesName(null, false, false) as $item) 
        {
            // Ajouter chaque nom de groupe dans le tableau de choix
            $choices[$item['name'] . ' ('.$item['id_category'].')'] = $item['id_category'];   
            $choices_available[$item['id_category']] = $item['name'];
        }

        if ($request->get('id') !== null)
        {
            // Chargement des mappings depuis la table promotion
            $key = $request->get('id');

            $results = $db->executeS('SELECT * FROM `'.$this->module->TblPrefix.'promotion` WHERE code = \''.pSQL($key).'\'');
            if ($results === FALSE)
                throw new \Exception('Erreur au chargement des promotions : '.$db->getMsgError());
            if (count($results) == 0)
                throw new \Exception('Promotion inconnue !');
            
            // Edition !            
            $currentvalue = $results[0]['id_category'] ?? null;
            $label = $results[0]['label'] ?? $key;

            $form = $this->createForm(MappingType::class, [
                            'cancel' => $this->generateUrl($request->attributes->get('_route')),
                            'id' => $key,
                            'label' => $label,
                            'choice' => $currentvalue ?? 0,
                            'choices' => $choices,
                            'no-import' => null,
                            'placeholder' => 'Sélectionner une catégorie',
                            ]);
            $form->handleRequest($request);

            $data = $form->getData();

            // On enregistre
            if ($form->isSubmitted() && $form->isValid()) 
            {                
                $data = $form->getData();                               
    
                // On enregistre
                $choice = $data['choice'] ?? null;

                // Selon la configuration de sync on modifie ou non le tableau de choix
                if (isset($choice) && $choice != $currentvalue)
                {
                    // On commence par retirer tous les produits des anciennes catégories !
                    if (isset($currentvalue) && $currentvalue != 0)
                    {
                        // Ménage
                        $sql = 'DELETE FROM `' . _DB_PREFIX_ . 'category_product` WHERE id_category = '.$currentvalue.' AND id_product IN (SELECT `id_product` FROM `' . $this->module->TblPrefix . 'promotion_detail` WHERE `code` IN (\''.pSQL($key).'\') AND `id_category` IN ('.$currentvalue.'))';
                        if (!$db->execute($sql))
                            throw new \Exception('Erreur au nettoyage des produits de la promotion : '.$db->getMsgError());
                    }

                    if (!$db->update($this->module->TblShortPrefix.'promotion', ['id_category' => $choice !== 0 ? $choice : null], 'code = \''.pSQL($key).'\'', 0, true))
                        throw new \Exception('Erreur au sauvegarde de la promotion : '.$db->getMsgError());

                    // Recalcul des promos
                    $syncer = new SyncerDiscounts(new Syncer($this->module, false));
                    $syncer->rebuildDiscounts();
                }
    
                $this->addFlash('success', 'Configuration de mapping sauvegardée !');

                // Puis on passe en mode affichage
                $form = null;
            }
        }

        if (!$form) // affichage !
        {
            // Chargement des mappings depuis la table promotion
            $results = $db->executeS('SELECT * FROM `'.$this->module->TblPrefix.'promotion` ORDER BY label ASC');
            if ($results === FALSE)
                throw new \Exception('Erreur au chargement des promotions : '.$db->getMsgError());

            // Affichage :
            // [ [ 'key' => '', 'value' => ''], ...]
            $mappings = [];

            foreach ($results as $row)
            {                
                $value = $row['id_category'] ?? 0;
                $mappings[] = [
                    'key' => $row['code'],
                    'title' => sprintf('%s<br><small>(du %s au %s)</small>', $row['label'], $row['date_start'], $row['date_end']),
                    'value' => $value,
                    'value_title' => $value !== null && isset($choices_available[$value]) ? $choices_available[$value].' ('.$value.')' : null,
                ];                
            }
        }

        $backoffice = $this->module->getBridge()->getId();
        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => 'Affectation des promotions à une catégorie',
            'map_value' => 'Catégorie Prestashop associée',
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => '<p>Pour chaque promotion (colonne de gauche), vous pouvez affecter une catégorie Prestashop dans laquelle le module rangera les produits qui sont concernés par cette promotion.</p>'.
                        '<p>Cette fonction est particulièrement utile pour mettre en avant un type de promotion particulier, telle la semaine du <em>Black Friday</em> ou la <em>Saint-Valentin</em> et rassemblés en une seule page tous les articles concernés par l\'opération.</p>'
                        .'<p><em><strong>Notes&nbsp;: </strong>l\'affectation aux catégories est recalculé immédiatement.</em></p>',
            'help_edit' => '<p>Sélectionnez une catégorie Prestashop dans laquelle classer les produits concernés par la promotion ' . $label . '.</p>',
            'help_automapping' => null,
            'mappings' => $mappings,
            'form' => $form ? $form->createView() : null,
        ]));
    }


    /**
     * Gestion du mappage des boutiques
     */
    public function mappingStoresAction(Request $request)
    {
        // En mode affichage => pas de form
        $form = null;
        $mappings = null;
        
        $cfgKey = SyncConfiguration::CFG_MAP_STORES;        
        $noImportText = 'Non mappé';

        $choices = [];
        $choices_available = [];

        foreach (\Store::getStores(\Context::getContext()->language->id) as $item) 
        {
            // Ajouter chaque nom de groupe dans le tableau de choix
            $choices[$item['name'] . ' ('.$item['id_store'].')'] = $item['id_store'];   
            $choices_available[$item['id_store']] = $item['name'];
        }

        $data = $this->module->getCfg()->get($cfgKey);
        if (!$this->module->getCfg()->get($cfgKey))
            $this->module->getCfg()->set($cfgKey, ['*' => 0]);

        $labels = $this->module->getCfg()->get(SyncConfiguration::CFG_MAP_STORES_NAME);
        if (!$labels)
            $labels = ['*' => 'Tous les autres'];

        $label = null;        

        if ($request->get('id') !== null)
        {
            $key = $request->get('id');            
            $data = $this->module->getCfg()->get($cfgKey);            
            if (!array_key_exists($key, $data))
                throw new \Exception('Magasin inconnu !');

            $label = $labels[$key] ?? $key;            

            // Edition !
            $form = $this->createForm(MappingType::class, [
                            'cancel' => $this->generateUrl($request->attributes->get('_route')),
                            'id' => $key,
                            'label' => $label,
                            'choice' => $data[$key] ?? null,
                            'choices' => $choices,
                            'no-import' => $noImportText,
                            'placeholder' => 'Sélectionner une boutique',
                            ]);
            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) 
            {
                $data = $form->getData();
    
                // On enregistre
                $this->module->getCfg()->setMap($cfgKey, $key, $data['choice'] ?? 0);
    
                $this->addFlash('success', 'Configuration de mapping sauvegardée !');

                if ($form->get('actions')->get('submitAndNext')->isClicked())
                    return $this->nextMappingPage($request, $labels, $cfgKey, $key);

                // Puis on passe en mode affichage
                $form = null;
            }
        }
        
        if (!$form) // affichage !
        {
            // Rechargement
            $data = $this->loadMappingData($cfgKey, $labels);
            if (!$data)
                $data = ['*' => -1];

            // Affichage :
            // [ [ 'key' => '', 'value' => ''], ...]
            $mappings = [];            

            foreach($data as $key => $value)
            {
                // On encode la clé avec base64 sans = pour éviter les problèmes de clé
                $mappings[] = [
                    'key' => $key,
                    'title' => isset($labels[$key]) ? $labels[$key] : $key,
                    'value' => $value && $value != 0 ? $value : null,
                    'value_title' => $value !== null && isset($choices_available[$value]) ? $choices_available[$value].' ('.$value.')' : null,
                ];                
            }
        }

        // get current route        
        $backoffice = $this->module->getBridge()->getId();
        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => 'Magasin '.$backoffice,
            'map_value' => 'Magasin Prestashop associé',
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => '<p>Pour chaque magasin '.$backoffice.' (colonne de gauche), vous pouvez affecter un magasin Prestashop. <br>Quand un magasin n\'est pas mappé, c\'est le mappage spécial <code>Tous les autres</code> qui est utilisé. Si le mappage spécial n\'est pas mappé, alors la donnée est simplement ignorée.</p>',
            'help_edit' => '<p>Vous éditez actuellement le mapping du magasin <strong>'.$label.'</strong> de '.$backoffice.' sur Prestashop. <br>Sélectionnez une boutique/magasin Prestashop à utiliser.</p>',            
            'mappings' => $mappings,
            'form' => $form ? $form->createView() : null,
        ]));
    }
}