<?php 

namespace PrestaShop\Module\PolarisPrestaConnector\Controller;

use Context;
use PolarisPrestaConnector;
use PrestaShop\Module\PolarisPrestaConnector\CategoriesManager;
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'] = CategoriesManager::getDynCatsTypes($this->module->getCfg());
        
        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);
        if (!$labels)
            $labels = [ '*' => '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);
        if (!$labels)
            $labels = [ '*' => 'Toutes les autres' ];
        $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 = CategoriesManager::getDynCatsTypes($cfg);

        $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');
        if (!$labels)
            $labels = [ '*' => 'Toutes les autres' ];         
        $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();

        // Help
        if (str_starts_with($cat_type, 'metacat_'))
        {
            $help = "<p>Les méta-catégories sont des catégories virtuelles côté backoffice, définies entièrement dans la configuration du module. Elles n'existent pas en tant que telles dans le backoffice ".$backoffice." : elles sont construites par le module comme la combinaison logique de plusieurs catégories dynamiques existantes. L'objectif est de permettre de créer des catégories Prestashop dont le peuplement dépend de l'appartenance simultanée d'un produit à plusieurs catégories backoffice.</p>";            

            // Obtention de la définition de la catégorie composite
            $metaCatDef = $cfg->get(SyncConfiguration::CFG_META_CATEGORIES);
            if ($metaCatDef && $metaCatDef[$cat_type])
            {
                $help .= '<p>La méta-catégorie <strong>'.$cat_type_name.'</strong> est définie par la combinaison des catégories '.$backoffice.' suivantes :</p><ul>';
                $idx = 0;
                foreach ($metaCatDef[$cat_type] as $compCat)
                {
                    if ($idx++ == 0) continue;  // la première catégorie est la méta-catégorie elle même, on l'affiche pas dans la liste des catégories composantes                    
                    if ($compCat == '__manufacturer')
                        $name = 'Marque';
                    else
                        $name = $cat_types[$compCat] ?? '?? '.$compCat;

                    $help .= '<li><strong>'.$name.'</strong></li>';
                }   
                $help .= '</ul>';       
            }

            $help .= '<p>Pour chaque méta-catégorie '.$cat_type_name.' (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>';            
            $help .= '<p>Contrairement aux autres catégories, les entrées de mappage pour les méta-catégories sont créées à la volée, au fur et à mesure que le moteur de synchronisation rencontre de nouvelles clés composites.</p>';
            $help .= '<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>';
        }
        else
        {            
            $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>';
        }

        return $this->render('@Modules/' . $this->module->name . '/views/templates/admin/mapping.html.twig', $this->addDefaultVars([
            'map_key' => $cat_type_name.' ⚙',
            'map_value' => 'Catégories Prestashop associées',            
            'cat_type' => $cat_type,
            'edit_route' => $request->attributes->get('_route'),
            'edit_title' => $label,
            'help' => $help,
            '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,
        ]));
    }
}