<?php

declare(strict_types=1);

use PrestaShop\Module\PolarisPrestaConnector\AutoUpdater;
use PrestaShop\Module\PolarisPrestaConnector\BridgeAdapter;
use PrestaShop\Module\PolarisPrestaConnector\Controller\VatReportAdminController;
use PrestaShop\Module\PolarisPrestaConnector\MultipleProductsManager;
use PrestaShop\Module\PolarisPrestaConnector\SupplierRefManager;
use PrestaShop\Module\PolarisPrestaConnector\SyncConfiguration;
use PrestaShop\Module\PolarisPrestaConnector\Syncer;
use PrestaShop\Module\PolarisPrestaConnector\SyncerOrders;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShop\Module\PolarisPrestaConnector\UrlRedirectManager;

if (!defined('_PS_VERSION_'))
  exit;

class PolarisPrestaConnector extends Module
{
    /**
     * Numéro de version du module
     * 
     * @var string
     */
    public const MODULE_VERSION = '9.0.30';

    /**
     * Version installée
     */
    public string $installed_version = "";

    /**
     * Identifiant du module
     */
    public string $BridgePrefix;

      /**
     * chemin vers tpl
     */
    public string $TplPath;

    /**
     * Préfixe complet des tables
     */
    public string $TblPrefix;

    /**
     * Préfixe court des tables
     */
    public string $TblShortPrefix;

     /**
     * Configuration du module
     *      
     */
    private ?SyncConfiguration $cfg = null;

    /**
     * Lien vers le pont
     */
    private ?BridgeAdapter $bridge = null;

    /**
     * Gestionnaire de produits connexes
     */
    private ?MultipleProductsManager $mpm = null;
    

     /**
     * New instance
     * 
     */
    public function __construct()
    {
        // Nom du module
        $this->name = basename(__FILE__, '.php');

        // On va se noter une constante pour identifier la version de notre module
        // Cette constante, nommée Id est composée des 3 premieres lettres du nom du module
        $this->BridgePrefix = substr($this->name, 0, 3);
        if ($this->BridgePrefix == 'pre')
        {
            // Nous sommes en développement, on va prendre le contenu du fichier local 'DEV'
            // Si ce dernier n'existe pas, on le crée avec 'pol' à l'intérieur
            // Puis on charge le prefixe avec le contenu du fichier (nettoyer des espaces et des retours chariots)
            $file = __DIR__ . '/DEV';
            if (!file_exists($file))
                file_put_contents($file, 'pol');
            $this->BridgePrefix = trim(file_get_contents($file));            
        }

        $this->TblPrefix = _DB_PREFIX_ . 'pc_' . $this->BridgePrefix . '_';
        $this->TblShortPrefix = 'pc_' . $this->BridgePrefix . '_';
        $this->TplPath = '@Modules/' . $this->name . '/views/templates/';

        $this->author = 'VEGA Informatique';
                
        $this->version = '9.0.30';
        $this->installed_version = '9.0.30';

        $this->tab = 'synchronisation';        
        $this->need_instance = 0;
        $this->ps_versions_compliancy = [
            'min' => '8.1.0',
            'max' => _PS_VERSION_,
        ];
        $this->bootstrap = true;

        parent::__construct();

        $this->displayName = $this->trans('PolarisPrestaConnector', [], 'Modules.prestaconnector.Admin');        

        // On enrichi la description avec le changelog ici
        $this->description = '<h3 id="version-9.0.30">Version 9.0.30</h3> <ul> <li>Change : le suivi d’exportation des commandes vers le backoffice ne repose plus sur des états de commande PrestaShop dédiés (« Exportée » / « Erreur d’export ») qui écrasaient l’état métier réel de la commande (Expédiée, Livrée, Remboursée…) ; il s’appuie désormais sur un statut d’exportation isolé propre au module ;</li> <li>Add : nouvel onglet « Export » sur la fiche commande, affichant le statut d’exportation courant (non exportée / exportée / erreur avec sa cause) et un journal daté de toutes les tentatives ;</li> <li>Change : le bouton « Réexporter » est déplacé de la barre d’actions de la commande vers l’onglet « Export » ; après un ré-export (réussi comme en erreur), le journal et le statut se rafraîchissent sur place, sans recharger la page, avec un message de retour ;</li> <li>Fix : les scripts et styles du back-office du module sont versionnés (fin du cache navigateur servant d’anciennes versions après une mise à jour) ;</li> <li>Change : à la mise à jour, le journal est reconstitué depuis l’historique et les commandes existantes sont repositionnées sur leur véritable état métier (les deux anciens états de commande du module sont retirés de l’historique et désactivés) ;</li> </ul> <h3 id="version-9.0.29">Version 9.0.29</h3> <ul> <li>Add : mappage des propriétés personnalisables Polaris (15+) vers des caractéristiques produit — un sélecteur par propriété dans les Paramètres du catalogue, valeur reportée en texte libre et caractéristique vidée quand la propriété est absente ; les propriétés booléennes (case à cocher) sont reportées en « Oui » / « Non » (pol) ;</li> <li>Change : refonte de l’écran de mappage des couleurs — un couple de sélecteurs « attribut/catégorie source ⇒ groupe d’attributs de destination » remplace le sélecteur par attribut, pour désigner clairement l’unique source de couleur passée par la moulinette ;</li> <li>Fix : les descriptions de produits Polaris ne sont plus doublement encodées — les accents et symboles n’apparaissent plus sous la forme « Ã© », « â¬ »… : un texte déjà en UTF-8 renvoyé par l’API n’est plus reconverti à tort depuis l’ISO-8859-1 (pol) ;</li> </ul> <h3 id="version-9.0.28">Version 9.0.28</h3> <ul> <li>Change : les libellés des méta-catégories sont désormais reconstruits à l’affichage à partir des catégories qui les composent, au lieu d’être stockés (allègement de la configuration) ;</li> <li>Fix : la synchronisation des promotions ne s’interrompt plus sur une erreur de contrainte (déclinaison référencée mais absente) ; les références obsolètes de product_supplier ne sont plus laissées orphelines lors d’un changement d’attributs ;</li> <li>Fix : les corrections de prix inférieures à 0,0001 (fréquentes à précision d’arrondi élevée) ne sont plus ignorées ; un produit dont le prix calculé est légèrement inférieur au prix stocké n’engendre plus une boucle « Changement de prix » à chaque synchronisation ;</li> </ul> <h3 id="version-9.0.27">Version 9.0.27</h3> <ul> <li>Add : possibilité de saisir directement l’adresse https:// de votre Polaris au jumelage, à la place du numéro de licence (l’adresse est alors utilisée telle quelle, sans passer par le gestionnaire de licence) (pol) ;</li> </ul> <h3 id="version-9.0.26">Version 9.0.26</h3> <ul> <li>Fix : le jumelage Polaris ne reste plus bloqué jusqu’au timeout du proxy (erreur 524) quand le backoffice local n’est pas joignable ; l’erreur « ports/TLR » est désormais renvoyée rapidement (pol) ;</li> </ul> <h3 id="version-9.0.25">Version 9.0.25</h3> <ul> <li>Change : documentation refondue en un document unique, consultable en PDF directement dans la page de configuration du module (remplace les anciens manuels utilisateur et technique) ;</li> </ul> <h3 id="version-9.0.24">Version 9.0.24</h3> <ul> <li>Add : option pour exporter aussi les comptes clients inscrits à la newsletter, même sans commande ;</li> <li>Add : option pour régler le délai d’attente des appels au backoffice (600s par défaut) ;</li> <li>Fix : l’association au backoffice ne bloque plus le temps du tirage de l’environnement (récupéré ensuite en arrière-plan, et garanti au démarrage de la première synchronisation) ;</li> </ul> <h3 id="version-9.0.23">Version 9.0.23</h3> <ul> <li>Fix : correction prix solde et promo (précision de l’arrondi) ;</li> <li>Fix : correction de l’erreur « Attempt to read property “precision” on null » à l’exportation des commandes via le cron / la CLI (devise du contexte non initialisée) ;</li> </ul> <h3 id="version-9.0.22">Version 9.0.22</h3> <ul> <li>Add : calcul de la catégorie par défaut des produits ;</li> <li>Add : ajout d’un indicateur d’obsolescence ou de non compatibilité du backoffice sur la page de configuration ;</li> <li>Add : génération de l’URL simplifiée du produit lorsqu’elle n’existe pas ;</li> <li>Add : réglage consommation mémoire directement dans les options ;</li> <li>Add : indicateur sur le dashboard signalant qu’une synchronisation s’est interrompue anormalement ;</li> <li>Fix : arrêt de prise en compte des règles de taxes supprimées dans l’auto-détection du groupe de taxes à utiliser ;</li> <li>Fix : diverses corrections de documentation ;</li> <li>Fix : correction de l’erreur ‘La propriété Combination-&gt;ean13 n’est pas valide’.</li> <li>Fix : problème aléatoire d’assignation de photos ;</li> <li>Fix : correction “La propriété Product-&gt;reference n’est pas valide.” lorsque certaines références contiennent des caractères interdits ;</li> <li>Fix : correction de l’erreur “La propriété Combination-&gt;price n’est pas valide.” ;</li> <li>Fix : renommage des utilitaires CLI ;</li> <li>Fix : correction “utf8_encode deprecated” ;</li> </ul> <h3 id="version-9.0.21">Version 9.0.21</h3> <ul> <li>Add : options de connexion sécurisée des équipes Polaris à votre backoffice (pol) ;</li> <li>Add : gestion de méta-catégories composées d’une concaténation de plusieurs autres catégories de produits ;</li> <li>Add : possibilité d’exécuter la tâche CRON en mode PHP CLI ;</li> <li>Chg : création d’un menu de configuration avancé et déplacement des sections redirection et mapping (reprise de site) dans ce menu ;</li> </ul> <h3 id="version-9.0.20">Version 9.0.20</h3> <ul> <li>Add : gestion de la raison sociale pour les installations supportant le B2B ;</li> <li>Fix : tous les articles nouvellement publiés ne sont pas immédiatement présents sur le site ;</li> </ul> <h3 id="version-9.0.19">Version 9.0.19</h3> <ul> <li>Fix : importation des emails en minuscule ;</li> <li>Fix : meilleur message de non importation</li> </ul> <h3 id="version-9.0.18">Version 9.0.18</h3> <ul> <li>Fix : sous Prestashop 8, mauvaise URL pour la tâche CRON</li> <li>Fix : tâche cron de synchronisation inaccessible sous Prestashop &lt; v9 quand non connecté au panneau d’administration</li> </ul> <h3 id="version-9.0.17">Version 9.0.17</h3> <ul> <li>Add : bouton de réexportation grisée dans les commandes si la commande a déjà été exportée correctement ;</li> <li>Add : amélioration de la visibilité des erreurs sur le dashboard ;</li> <li>Fix : correction du problème trim(): Argument #1 ($string) must be of type string, array given ;</li> <li>Fix : remplacement des informations non désirées lorsque réutilisation de produit ;</li> <li>Fix : PrestaShop::__construct(): Argument #1 ($uid) must be of type string (pol)</li> </ul> <h3 id="version-9.0.16">Version 9.0.16</h3> <ul> <li>Fix : meilleure détection des comptes administrateurs</li> </ul> <h3 id="version-9.0.15">Version 9.0.15</h3> <ul> <li>Add : amélioration de la segmentation des produits ;</li> </ul> <h3 id="version-9.0.14">Version 9.0.14</h3> <ul> <li>Fix : erreur près de “LIMIT 1” lors de l’exécution de la tâche CRON sur certains environnements ;</li> <li>Fix : problème de timeout durant la synchronisation ;</li> <li>Fix : erreur combination price not valid</li> <li>Fix : php.INFO Deprecated Creation of dynamic property PrestaShop::xxx is deprecated</li> <li>Fix : sous certaines conditions le prix minimum déduit d’un produit n’est pas bon !</li> <li>Fix : ignore désormais les codes barres EAN13 trop longs plutôt que d’ignorer la déclinaison ;</li> <li>Fix : problème dans l’ordre des photos ;</li> </ul> <h3 id="version-9.0.13">Version 9.0.13</h3> <ul> <li>Fix : résolution des problèmes de concaténation des noms produits ;</li> <li>Fix : problème de reconnaissance des couleurs (sensible à casse) ;</li> <li>Add : moteur de reprise d’anciennes URL (migration) ;</li> </ul> <h3 id="version-9.0.12">Version 9.0.12</h3> <ul> <li>Add : sécurisation des synchronisations clients après reprise de données sur désastre (pol) ;</li> <li>Add : regénération automatique d’une clé lorsqu’elle est positionné sur “new” ;</li> <li>Add : possibilité de désactiver l’exportation des commandes (option par défaut à l’installation) ;</li> <li>Add : QoL sur le lancement de la tâche CRON d’essai dans le module de configuration ;</li> <li>Add : collecte et affichage de la version du Backoffice connecté ;</li> <li>Add : ajout du support des cartes de fidélité ;</li> <li>Add : possibilité de formater le nom automatique d’un produit ;</li> <li>Fix : typo avec les valeurs égales à 0 sur le tableau de synchronisation ;</li> <li>Chg : les journaux de la tâche automatisée sont désormais affichées sous forme de liste JSON ;</li> <li>Fix : une erreur sur une section de la tâche n’arrête plus la synchronisation entière ;</li> <li>Fix : erreur ‘PrestaShopBundle::setIdEmployee(): Argument #1 (idEmployee) must be of type int, null given’ à la mise à jour du stock</li> <li>Fix : correction d’une erreur Symfony quand appel de mappage sur un pont jamais synchronisé ;</li> </ul> <h3 id="version-9.0.11">Version 9.0.11</h3> <ul> <li>Add : support des codes douanes (pol) ;</li> <li>Fix : correction des dates des plans de solde (pol) ;</li> <li>Fix : correction de l’erreur à la synchronisation des données : Argument #1 must be of type string, null given @ BridgeAdapter::cleanHtmlText()</li> </ul> <h3 id="versions-antérieures">Versions antérieures</h3> <ul> <li>Fix : Erreur loadMappingData() must be of type array, null given ;</li> <li>Fix : Base table or view not found : link_categories_map does not exists ;</li> <li>Add : purge automatique des photos des produits désactivés gérés et désactivés depuis 1 mois ;</li> <li>Add : la tâche de synchronisation est maintenant gérée par un contrôleur admin (changement de la route) ;</li> <li>Fix : correction du problème <code>Table ps_product_attribute_combination’ does not exists</code> dans le mapping produit sur certaines installations (pol)</li> <li>Ajout d’un bouton pour passer au mappage suivant dans le mapping des catégories ;</li> <li>Fix : mauvais calcul C.A. union européenne ;</li> <li>Fix : problème de dépublication de produit trop aggressive ;</li> <li>Fix : problème de timeout trop court ;</li> <li>Add : reprise automatique de configuration du module PolarisLink (pol) ;</li> <li>Fix : désormais, à l’installation, le module est inactif et ne tente aucune synchronisation.</li> <li>Add : informations de classification du backoffice dans la fiche produit.</li> <li>Add : la configuration d’un mappage l’applique désormais immédiatement.</li> <li>Fix : correction de l’erreur “Impossible de libérer le verrou syncer”.</li> <li>Fix : correction de la commande n’a pas été exporté car 0.00 quand le produit n’a pas de taxe.</li> <li>Fix : application de la visibilité par défaut d’un produit.</li> <li>Fix : propagation des erreurs sur l’utilitaire de synchronisation de la fiche produit</li> <li>Fix : erreur lorsque le taux de taxe du produit n’est pas trouvé et pas mappable automatique sur Prestashop.</li> <li>Fix : bouclage infini sur mise à jour de certains produits</li> <li>Ignorer les produits sans stock à l’importation des produits.</li> <li>Fix : ne pas ignorer le catch-all quand une catégorie de produit n’est pas définie.</li> <li>Fix : la resynchronisation force dorénavant le traitement même quand l’heure n’est pas venue.</li> <li>Correction d’une racing condition bloquant tous les processus PHP (Gateway Timeout).</li> <li>Correction de la non prise en compte du filtre du catch-all des catégories.</li> <li>Ajout d’un nettoyage automatique des descriptions produits et conversion automatique de codepage au besoin.</li> <li>Meilleure détection du code produit.</li> <li>Meilleure prise en charge des photos de déclinaisons.</li> <li>Désactivation du mode maintenance pour la tâche CRON.</li> <li>Nettoyage automatique des mappages non utilisés.</li> <li>Amélioration du choix automatique de la catégorie par défaut.</li> <li>Ajout du relevé de la TVA collectée par pays.</li> <li>Support multi-magasins pour LCV gestion boutique.</li> <li>Pouvoir choisir les magasins/entrepôts qui contribuent au stock de la vente en ligne.</li> <li>Mise en évidence des mappings obligatoires.</li> <li>Pouvoir choisir la branche de mise à jour (stable/testing).</li> <li>Documentation erreur d’association.</li> <li>Support de la compatibilité avec Prestashop 9.</li> <li>Correction d’un problème d’affichage du talon de Backoffice.</li> <li>Call to undefined method GuzzleHttp::redactUserInfo().</li> <li>Ajout d’une option de nettoyage des attributs non utilisés.</li> <li>Ajout d’une option d’activation automatique d’un produit complet (photo + description).</li> <li>Revue du système de mise à jour.</li> <li>Ajout de la possibilité d’ajouter le sélecteur de produits connexes sans modifier le thème (si thème compatible)</li> <li>Nouveau hook de calcul de la disponibilité des pièces dans chaque magasin.</li> <li>Nouveau hook de rendu de la disponibilité des pièces dans chaque magasin.</li> <li>Possibilité d’afficher la disponibilité des pièces dans chaque magasin sur la fiche produit sans modifier le thème (si thème compatible)</li> <li>Pouvoir mapper les magasins du backoffice aux boutiques renseignées sur Prestashop</li> <li>Exporter l’adresse de facturation du client vers le backoffice quand celui-ci le supporte</li> <li>Amélioration de l’association au backoffice</li> <li>Ajout du lien vers la catégorie ou le groupe d’attributs dans les listes de mappings</li> <li>Ajout d’un catch-all sur le mappage des couleurs et la couleur “autre”</li> <li>Correction : l’appel de la tâche CRON depuis le backoffice de Prestashop ne fonctionne pas</li> <li>Correction : le mappage produit ne présente désormais plus que la référence concernée</li> <li>Correction : “la propriété catégorie-&gt;name est vide” à l’auto-création des catégories du mappage</li> <li>Correction : manque des hooks et des tabs lors d’une réinstallation</li> <li>Correction : l’audit et le mapping sont mal placés dans le menu</li> <li>Correction : le fournisseur de référence est maintenant activé et dispose d’une adresse éditable</li> <li>Mise à disposition d’un raccourci vers le paramétrage du module en mode testing (localhost)</li> <li>Correction de l’erreur ‘Fournisseur de références introuvable’</li> <li>Les erreurs de synchronisation remontent maintenant dans le journal de Prestashop</li> <li>Possibilité de consulter le journal d’audit directement dans l’interface, sans le télécharger.</li> <li>réutilisation d’un éventuel précédent fournisseur compatible pour ne pas perdre les références préalablement saisies lors de l’installation du module.</li> <li>Correction de l’erreur ‘form_title.html.twig is not defined’</li> <li>Les boutons de raccourci ne sont plus disponibles tant que l’association avec le backoffice n’est pas fait.</li> <li>Rappel du numéro de version dans la page de garde de la configuration du module et bouton pour rechercher la disponibilité d’une mise à jour.</li> <li>Correction d’un problème empêchant l’installation du module sans erreur.</li> <li>Correction d’un problème d’exportation d’annulation de commande quand celle-ci a été réglée avec un bon de réduction.</li> <li>Support des dé-publications de produits</li> <li>Auto-détection des modes de paiements des commandes existantes pour le mappage</li> <li>Possibilité de gérer des poids par défaut par catégories de produits.</li> <li>Importation des modifications de commandes, des remboursements et des retours sur le backoffice.</li> <li>Possibilité de tirer les modifications produits depuis la fiche d’un produit.</li> <li>Optimisation du tirage des soldes et promos en même temps que les prix produits.</li> <li>Système et fonctionnalités de base de synchronisation des commandes, fiches produits et des stocks.</li> </ul>';

        $this->confirmUninstall = $this->trans('Are you sure you want to uninstall this module ?', [], 'Modules.Polarisprestaconnector.Admin');                

        if (isset($_REQUEST["remount_hooks"]))
        {
            $this->registerTabs();
            $this->registerHooks();
        }
    }

    /**
     * L'installation est toujours un succès !
     * 
     */
    public function install()
    {
        // Force l'installation des hooks et des tabs
        if (parent::install())
        {
            $this->registerHooks();
            $this->registerTabs();
        }

        return true;
    }

    /**
     * A la désinstallation
     * CA NE FONCTIONNE PAS !
     */
    /*public function uninstall()
    {
        // On désinstalle
        parent::uninstall();

        // Et on purge les cache
        $this->resetCache();
    }*/

    /**
     * Réinitialisation des caches
     */
    public function resetCache()
    {
        // On va vider le cache de la configuration
        $context = \Context::getContext();
        $context->smarty->clearCompiledTemplate();
        $context->smarty->clearAllCache();

        // 2. Clear filesystem caches
        $cacheDirs = [
            _PS_ROOT_DIR_.'/var/cache/dev',
            _PS_ROOT_DIR_.'/var/cache/prod',
        ];

        $autoUpdater = new AutoUpdater($this);
        foreach ($cacheDirs as $dir)
        {
            if (is_dir($dir)) {
                $autoUpdater->deleteDirectory($dir);
            }
        }
    }

    /**
     * Obtenir les hooks utilisés par ce module
     */
    public function getHooks()
	{
		$hooks = array(
            'displayHeader',
			'dashboardZoneOne',
			'displayBackOfficeHeader',
            'displayAdminOrderTabLink',
            'displayAdminOrderTabContent',

            // Montage hook maisons
            'displayProductPriceBlock',
            'displayProductAdditionalInfo',

            // Hooks maisons
            'displayMultiProductSelector',
            'buildStoreStocks',
            'displayStoreStocks',

            // Controle et action commandes
            'actionValidateOrder',
            'displayAdminLogin',
			);

		return $hooks;
	}

    /**
     * Obtenir les tabs utilisés par ce module
     */
    public function getTabs()
    {
        $backofficeId = $this->getBridge()->getId();
        $tabs = [];

        // Page de mapping
        $name = [];
        foreach (Language::getLanguages() as $lang)
            $name[$lang['id_lang']] = $this->trans('Mapping', [], 'Modules.'.$this->name.'.Admin', $lang['locale'])
                .' '.$this->getBridge()->getId();

        $tabs[] = [ 'class_name' => 'Mappage' . $backofficeId,
                    'route_name' => $this->name . '_map_manufacturer',
                    'parent_class' => 'AdminCatalog',
                    'name' => $name,
                    ];

        // Page d'audit
        $name = [];
        foreach (Language::getLanguages() as $lang)
            $name[$lang['id_lang']] = $this->trans('Audit', [], 'Modules.'.$this->name.'.Admin', $lang['locale'])
                .' '.$this->getBridge()->getId();

        $tabs[] = [ 'class_name' => 'Audit'.$backofficeId,
                    'route_name' => $this->name . '_audit',
                    'parent_class' => 'AdminAdvancedParameters',
                    'name' => $name,
                    ];

        // Relevé de TVA
        $name = [];
        foreach (Language::getLanguages() as $lang)
            $name[$lang['id_lang']] = $this->trans('Relevé de TVA', [], 'Modules.'.$this->name.'.Admin', $lang['locale'])
                .' '.$this->getBridge()->getId();

        $tabs[] = [ 'class_name' => 'VatReport' . $backofficeId,
                    'route_name' => $this->name . '_vat_report',
                    'parent_class' => 'AdminParentOrders',
                    'name' => $name,
                    ];

        return $tabs;
    }    

    /**
     * Réenregistrement des tabs...
     */
    public function registerTabs()
    {
        // Attention, on a pas de container symfony sur un contrôleur v7
        //$container = SymfonyContainer::getInstance();
        //$tabRepository = $container ? $container->get('prestashop.core.admin.tab.repository') : null;
        $tabRepository = null; // Ne fonctionne pas !
        
        foreach ($this->getTabs() as $tabData) {
            // Vérifier si le tab existe déjà
            $exists = $tabRepository ?
                        $tabRepository->findOneIdByClassName($tabData['class_name']) :
                        Tab::getIdFromClassName($tabData['class_name']);
                        
            $tab = $exists ? new Tab((int) $exists) : null;
            if (!$tab || $tab->route_name != $tabData['route_name']) {                
                if (!$tab)
                    $tab = new Tab();
                $tab->active = 1;
                $tab->class_name = $tabData['class_name'];
                $tab->route_name = $tabData['route_name'];
                if (isset($tabData['parent_class']))
                    $tab->id_parent = $tabRepository ?
                                            $tabRepository->findOneIdByClassName($tabData['parent_class']) :
                                            Tab::getIdFromClassName($tabData['parent_class']);                
                $tab->module = $this->name;                
                $tab->name = $tabData['name'];

                // Sauvegarder le tab
                $tab->save();
            }
        }
    }

    /**
     * Déclenche une mise à jour de la base si nécessaire !
     */
    public function dbUpgrade()
    {       
        // Faut-il déclencher la mise à jour de la base de données du module ?
        // pour le savoir on regarde, si la version enregistrée dans la configuration est inférieure à la version actuelle                
        $currentDbVersion = \Configuration::get($this->BridgePrefix . 'prestaconnector_version');
        if (!$currentDbVersion)
            $currentDbVersion = '0.0';

        if (version_compare($currentDbVersion, $this->installed_version) < 0) {            
            // On met à jour la version du module
            require_once _PS_MODULE_DIR_ . $this->name . '/db/upgrade.php';
            $upgrade = new \PrestaShop\Module\PolarisPrestaConnector\Upgrade();
            $upgrade->upgradeModule($this, $currentDbVersion);
        }
    }
    
    /*
		Réenregistrement des hooks...
	*/
	public function registerHooks($updatePosition = true)
	{
        // Pour chacun de nos hooks, s'ils ne sont pas déjà enregistrés, on les enregistre
		foreach ($this->getHooks() as $hook)
        {
            if (!$this->isRegisteredInHook($hook))
                $this->registerHook($hook);
        }

		if ($updatePosition)
			$this->updatePosition(Hook::getIdByName('dashboardZoneOne'), 0, 1);
	}

    /**
     * Obtenir l'objet de configuration
     */
    public function getCfg() : SyncConfiguration
    {
        if (!$this->cfg)
            $this->cfg = new SyncConfiguration($this);

        return $this->cfg;
    }

    /**
     * Obtenir le gestionnaire de produits connexes
     */
    public function getMpm() : MultipleProductsManager
    {
        if (!$this->mpm)
            $this->mpm = new MultipleProductsManager($this);

        return $this->mpm;
    }

    /**
     * Obtient le pont configuré
     */
    public function getBridge() : BridgeAdapter
    {
        if (!$this->bridge)
        {
            $classname = "PrestaShop\\Module\\PolarisPrestaConnector\\" . $this->BridgePrefix . "\\Bridge";

            // On vérifie que la classe existe, ce qui n'est pas forcément le cas à l'installation du module
            // Si ce n'est pas le cas, on va charger l'autoload nous même
            if (!class_exists($classname))
            {
                if (file_exists(__DIR__ . '/vendor/autoload.php')) {
                    require_once __DIR__ . '/vendor/autoload.php';
                }
                else {
                    throw new \Exception("Impossible de charger le pont " . $classname . " !");
                }
            }

            $this->bridge = new $classname($this);
        }
        return $this->bridge;
    }

    /*
    * Page de configuration
    */
    public function getContent()
    {
        Tools::redirectAdmin(
            SymfonyContainer::getInstance()->get('router')->generate($this->name . '_changelog')
        );
    }

    /**
     * Dashboard !
     */
    public function hookDashboardZoneOne($params)
	{
        try
        {
            // On contrôle la TVA
            // dernière vérification ?
            $last_check = \Configuration::get('EU_VAT_LAST_CHECKS') ?? '2000-01-01';
            if ($last_check != date('Y-m-d'))
            {
                // On contrôle la TVA
                VatReportAdminController::ControlVat();
                \Configuration::updateValue('EU_VAT_LAST_CHECKS', date('Y-m-d'));
            }

            $this->dbUpgrade();

            $isTesting = preg_match('/^localhost(:\d+)?$/', $_SERVER['HTTP_HOST']);
            $router = SymfonyContainer::getInstance()->get('router');
            $tpl_variables = array(
                'title_name' => 'Backoffice ' . $this->getBridge()->getId(),
                'module_name' => $this->name,
                'module_version' => $this->installed_version,
                'configUrl'=> $router->generate($this->name . "_configuration"),
                'auditUrl' => $router->generate($this->name . "_audit"),
                'logsUrl' => $this->context->link->getAdminLink("AdminLogs"),
                'cronGuardToken' => $this->getCfg()->get(SyncConfiguration::CFG_GUARD_TOKEN),
                'progress' => base64_encode(json_encode(Syncer::getSyncProgress($this)->toArray())),
                'vat_limit' => VatReportAdminController::EU_VAT_LIMIT,
                'vat_alert' => \Configuration::get('EU_VAT_ALERT'),
                // On montre la configuration que si l'utilisateur en cours a une adresse mail qui se finit par @vega-info.fr ou @lcv.fr
                'show_cfg' => $isTesting || preg_match('/@(vega-info|lcvmultimedia)\.fr$/', $this->context->employee->email),                
                'clearCacheUrl'=> $router->generate("admin_clear_cache"),                
            );            

            // Récupère le container Symfony pour ensuite obtenir le service Twig pour le rendu
            $container = \PrestaShop\PrestaShop\Adapter\SymfonyContainer::getInstance();
            /** @var \PrestaShop\Module\PolarisPrestaConnector\Controller\TwigRenderer $renderer */
            $renderer = $container->get($this->name.'.twig_renderer');

            return $renderer->render($this->TplPath . 'hook/dashboard.html.twig', $tpl_variables);
        }
        catch (\Throwable $ex)
        {
            // Erreur ?
            return '<div class="alert alert-danger">'
                . "Erreur lors du chargement du module PolarisPrestaConnector : " . $ex->getMessage()
                . '</div>';
        }
	}

    /**
     * Injectons du CSS
     */
    public function hookDisplayHeader()
    {
        // Ajouter le fichier CSS à la page du front-office
        $this->context->controller->addCSS($this->_path . 'views/css/front.css');

        // Redirection des anciennes URLs si on est sur une page 404
        if ($this->context->controller->php_self === 'pagenotfound') {
            (new UrlRedirectManager($this))->handle();
        }
    }

    /**
     * Injection du JS et CSS dans l'admin
     * 
     */
    public function hookDisplayBackOfficeHeader($params)
    {
        // Le suffixe ?v=MODULE_VERSION force les navigateurs à recharger les assets à chaque
        // nouvelle version du module (évite de servir un ancien JS/CSS mis en cache).
        $this->context->controller->addJS($this->_path.'views/js/common.js?v='.'9.0.30');
        $this->context->controller->addJS($this->_path.'views/js/admin.js?v='.'9.0.30');
        $this->context->controller->addCSS($this->_path.'views/css/admin.css?v='.'9.0.30');

        // Si nous sommes sur l'admin controller, alors on tweak l'interface
        switch (Tools::getValue('controller'))
        {
            case "AdminProducts":
                return $this->tweakAdminProductUI($params);
        }

        return '';
    }

    /**
     * Contrôle des champs protégés
     * 
     * @param array $params
     *
     * @return string
     */
    public function tweakAdminProductUI($params) : string
    {
        // Si nous sommes en mode sans synchro de fiche, inutile de protéger les champs, donc on sort
        $cfg = $this->getCfg();
        if ($cfg->get(SyncConfiguration::CFG_WORKMODE) != 1)
            return '';

        $id_supplier = $this->getOrCreateSupplierId();

        $syncCfg = $cfg->get(SyncConfiguration::CFG_DATA_SYNC);
        if (empty($syncCfg))
            return '';

        // Sinon, on va chercher à savoir si le produit est un produit qu'on entretien. 
        // Si nous retrouvons des références fournisseurs pour ce produit, alors c'est le cas
        $id_product = Tools::getValue('id_product');
		if (empty($id_product))
			return '';

        // On va chercher les références fournisseurs
        $refMngr = new SupplierRefManager($id_supplier, [$id_product]);
        if ($refMngr->getNbProducts() == 0)
            return '';    // Non pas géré

        $locked = $this->getCfg()->isProductLocked((int) $id_product);

        $class = $this->name . "-disabled";
        $polarisColor = "#e4a12d";
		$style = "";
		$js = "";

        // Rétribution des informations backoffices depuis la base de données
        $db = \Db::getInstance();
        $sql = "SELECT backoffice_categorization FROM " . $this->TblPrefix . "product WHERE id_product = " . (int) $id_product;
        $value = $db->getValue($sql);
        if ($value)
        {
            $infos = json_decode($value, true);
            if ($infos)
            {                
                // On liste les catégories
                $cats = ['#product_ref' => "Référence"];
                // pour inclure même les méta => foreach (CategoriesManager::getDynCatsTypes($cfg) as $catid => $catname)                    
                foreach ($cfg->get(SyncConfiguration::CFG_DYN_CAT) as $catid => $catname)
                    $cats[$catid] = $catname;

                // On affiche sur 2 colonnes
                $backOfficeInfos = '';
                $backOfficeInfos .= '<div class="ps_bo_infos">';
                $backOfficeInfos .= '<table>';                                        
                foreach ($cats as $cat => $name)
                {                    
                    $backOfficeInfos .= '<tr>';
                    $backOfficeInfos .= '<th>' . htmlspecialchars($name) . '</th>';
                    if ($cat === "#product_ref")
                    {
                        $backOfficeInfos .= '<td><div id="pc_product_id"></div></td>';                            
                    }
                    else
                    {
                        $names = $cfg->get(SyncConfiguration::CFG_MAP_DYN_CAT . $cat . '_name');
                        
                        $backOfficeInfos .= '<td>';

                        if (isset($infos[$cat]))
                        {
                            $n = htmlspecialchars($infos[$cat]);
                            if (isset($names[$infos[$cat]]))
                            {
                                $name = $names[$infos[$cat]];
                                // si le nom commence déjà par la référence, on ne la remet pas
                                if (str_starts_with($name, $n))
                                    $name = trim(substr($name, strlen($n)), ' -');
                                $n .= ' – ' . htmlspecialchars($name);
                            }
                            
                            $backOfficeInfos .= $n;
                        }
                        $backOfficeInfos .= '</td>';
                    }
                    $backOfficeInfos .= '</tr>';
                }

                $backOfficeInfos .= '</table>';
                $backOfficeInfos .= '</div>';
            }
            else
                $backOfficeInfos = 'Informations backoffice non disponibles ?!';            

            // Injection du panneau d'information
            $js .= "$('.product-reference').hide();\n";
            $js .= "$('.product-header-summary').append('" . addslashes($backOfficeInfos) . "');\n";
            $js .= "$('#pc_product_id').html($('.product-reference').find('span').html());\n";            
        }        

        if (!$locked)
        {
            $js .= "$('.product-type-preview').before('<p class=\"".$class."\" >(Les champs de cette couleur sont contrôlés par votre backoffice ".$this->getBridge()->getId()." et <strong>vous ne devez pas les modifier</strong> directement sous Prestashop)</p>');\n";

            $item_to_disable = [
                '.product-type-preview',        // Pas de changement de type de produit
                '#combination-list-actions',    // ni de création de déclinaisons                        
            ];

            $fields_to_disable = [
                '#product_pricing_retail_price_price_tax_included', // Pas d'actions sur les prix
                '#product_pricing_retail_price_price_tax_excluded',            
            ];

            $fields_to_style = [
                '.delete-combination-item',     // pas de suppression de déclinaisons            
                '.edit-combination-item',       // pas de modification de déclinaisons
            ];

            // Cas particulier des caractéristiques
            $features = $this->getCfg()->get(SyncConfiguration::CFG_PR_ATTRIBUTES_MAPPING);
            $caracts = [];
            $id_lang = $this->context->language->id;
            if (!empty($features) && isset($features[SyncConfiguration::PRODUCT_ATTR_FEATURE]))
            {            
                foreach ($features[SyncConfiguration::PRODUCT_ATTR_FEATURE] as $feature => $id_feature)
                {
                    if ($id_feature > 0)
                    {
                        $feature = new Feature($id_feature);
                        $caracts[] = mb_strtolower($feature->name[$id_lang]);
                    }
                }
            }
            
            if (!empty($caracts))
                $js .= "$('#product_details_features_feature_values').before('<p class=\"".$class."\">Attention, certaines caractéristiques (".join(", ", $caracts).") sont maintenues par le module ".$this->name.".<br>Ne modifiez pas ces caractéristiques, le module annulera vos modifications pour ces catégories.</p>');\n";


            // Application des données de synchronisation
            foreach( [
                SyncConfiguration::CFG_PRODUCT_SYNC_REF => '#product_details_references_reference',
                SyncConfiguration::CFG_PRODUCT_SYNC_NAME => '#product_header_name',
                SyncConfiguration::CFG_PRODUCT_SYNC_DESCRIPTION => '#product_description_description',
                SyncConfiguration::CFG_PRODUCT_SYNC_MANUFACTURER => '#product_description_manufacturer',
                SyncConfiguration::CFG_PRODUCT_SYNC_DIMENSIONS => ['#product_shipping_dimensions_width', '#product_shipping_dimensions_height', '#product_shipping_dimensions_depth'],
                SyncConfiguration::CFG_PRODUCT_SYNC_WEIGHT => '#product_shipping_dimensions_weight',
                //SyncConfiguration::CFG_PRODUCT_SYNC_ECOTAX => true,
                //SyncConfiguration::CFG_PRODUCT_SYNC_LOCATION => '#combination_form_stock_options_stock_location',
                SyncConfiguration::CFG_PRODUCT_SYNC_TAXRATE => '#product_pricing_retail_price_tax_rules_group_id',
                SyncConfiguration::CFG_PRODUCT_SYNC_EAN13 => '#product_details_references_ean_13',            
                // SyncConfiguration::CFG_PRODUCT_SYNC_CATEGORIES => '#product_description_categories', => LAISSONS la possibilité au marchand de compléter la fiche comme il le souhaite
                SyncConfiguration::CFG_PRODUCT_SYNC_PHOTOS => '#product_description_images',            
            ] as $k => $v)
            {
                if (isset($syncCfg[$k]) && $syncCfg[$k])
                {
                    if (is_array($v))
                        $fields_to_disable = array_merge($fields_to_disable, $v);
                    else
                        $fields_to_disable[] = $v;
                }
            }

            // Application
            foreach ($item_to_disable as $field)
                $js .= "$('".$field."').attr('tabindex', '-1');\n$('".$field."').addClass('".$class."');\n";

            foreach ($fields_to_disable as $field)
                $js .= "$('".$field."').closest('.form-group').addClass('".$class."');\n"; // Remonter le champs pour mettre la classe sur le premier form-group qui englobe notre classe !            

            // Pour les fields_to_style, on laisse la possibilité tout de même de les modifier, mais on les met en couleur
            foreach ($fields_to_style as $field)
                $style .= $field." { color: $polarisColor !important; border-color: $polarisColor !important;}\n";

            $style .= ".".$class." { pointer-events: none !important; color: $polarisColor !important; border-color: $polarisColor !important;}\n";
            $style .= ".".$class." * { pointer-events: none !important; color: $polarisColor !important; border-color: $polarisColor !important;}\n";
            //$style .= join(", ", $fields_to_hide)." { display: none !important; }\n";
        }

        // Si le produit est verrouillé, on propose la possibilité de le déverrouiller
        if ($locked)
            $js .= '$("#product__toolbar_buttons").prepend(\'<a id="'.$this->name.'_lock" class="toolbar-button btn" style="color: crimson;" title="Ce produit est verrouillé. Il ne sera pas synchronisé depuis '. $this->getBridge()->getId() .'"><i class="material-icons">lock</i><span class="btn-label"></span></a>\');';
        else
        {            
            // Injection d'un bouton de synchronisation sur la fiche produit
            if ($this->getBridge()->canSyncOneProductOnly())
                $js .= '$("#product__toolbar_buttons").prepend(\'<a id="'.$this->name.'_sync" class="toolbar-button btn" style="color: #6c868e;" title="Synchroniser depuis ' . $this->getBridge()->getId() . '"><i class="material-icons">sync</i><span class="btn-label"></span></a>\');';
            $js .= '$("#product__toolbar_buttons").prepend(\'<a id="'.$this->name.'_lock" class="toolbar-button btn" style="color: #6c868e;" title="Empêcher de mettre à jour la fiche depuis '. $this->getBridge()->getId() .'"><i class="material-icons">lock_open</i><span class="btn-label"></span></a>\');';
            $js .= '$("#'.$this->name.'_sync").click((e) => ' . $this->name . '_syncProduct(e, '.$id_product.'));';        
        }

        // Synchroniser les données        
        $js .= '$("#'.$this->name.'_lock").click((e) => ' . $this->name . '_toggleLockProduct(e, '.$id_product.'));';

        $output = "";
        if (!empty($style))
			$output .= "<style>\n".$style."</style>\n";
		if (!empty($js))
			$output .= "<script language='javascript'>\n$(document).ready(function() {".$js."});\n</script>\n";

		return $output;
    }

    /**
     * Onglet « Export {backoffice} » sur la fiche commande : lien de l'onglet.
     * Hook disponible et identique en PS 8.1 et 9.0 (paramètre id_order).
     *
     * @param array $params
     * @return string
     */
    public function hookDisplayAdminOrderTabLink(array $params) : string
    {
        try
        {
            $id_order = (int) ($params['id_order'] ?? 0);
            if (!$id_order)
                return '';

            $psOrder = new \Order($id_order);
            if (!$psOrder->id)
                return '';

            $syncer = new SyncerOrders(new Syncer($this));
            $current = $syncer->getCurrentExportStatus($psOrder->reference);
            [$label, $color] = $this->getExportStatusMeta($current['status']);

            return '<li class="nav-item">'
                . '<a class="nav-link" id="pcExportTab" data-toggle="tab" href="#pcExportTabContent" role="tab" aria-controls="pcExportTabContent" aria-selected="false" title="' . htmlspecialchars($label, ENT_QUOTES) . '">'
                . '<span class="pc-export-dot" style="display:inline-block;width:9px;height:9px;border-radius:50%;background:' . $color . ';margin-right:6px;vertical-align:middle;"></span>'
                . 'Export ' . $this->getBridge()->getId()
                . '</a></li>';
        }
        catch (\Throwable $e)
        {
            return '';
        }
    }

    /**
     * Onglet « Export {backoffice} » sur la fiche commande : contenu de l'onglet
     * (statut d'exportation courant + journal daté des tentatives).
     *
     * @param array $params
     * @return string
     */
    public function hookDisplayAdminOrderTabContent(array $params) : string
    {
        try
        {
            $id_order = (int) ($params['id_order'] ?? 0);
            if (!$id_order)
                return '';

            $psOrder = new \Order($id_order);
            if (!$psOrder->id)
                return '';

            $renderer = SymfonyContainer::getInstance()->get($this->name . '.twig_renderer');
            return $renderer->render($this->TplPath . 'hook/order_export_tab.html.twig',
                $this->getOrderExportView($psOrder->reference));
        }
        catch (\Throwable $e)
        {
            return '';
        }
    }

    /**
     * Prépare les variables d'affichage du panneau d'export d'une commande (statut courant,
     * exportabilité, journal daté). Réutilisé par le hook de l'onglet et par le rafraîchissement
     * AJAX du panneau après un ré-export.
     *
     * new Syncer() déclenche dbUpgrade() : la table du journal est garantie même sur le chemin
     * « page commande » du core (qui n'appelle pas dbUpgrade lui-même).
     *
     * @param string $reference Référence de la commande
     * @return array Variables pour les templates order_export_tab / order_export_panel
     */
    public function getOrderExportView(string $reference) : array
    {
        $syncer = new SyncerOrders(new Syncer($this));
        $current = $syncer->getCurrentExportStatus($reference);
        [$statusLabel, $statusColor] = $this->getExportStatusMeta($current['status']);

        // Exportabilité (bouton « Réexporter »). isExportableOrder peut lever si le modèle de
        // commande ne peut être construit (ex. produit sans référence fournisseur ni EAN13) :
        // on protège localement pour afficher le panneau malgré tout (bouton désactivé).
        $exportable = false;
        try
        {
            $exportable = $syncer->isExportableOrder($reference);
        }
        catch (\Throwable $e)
        {
            $exportable = false;
        }

        $entries = [];
        foreach ($syncer->getExportLog($reference) as $row)
        {
            [$label, $color] = $this->getExportStatusMeta($row['status']);
            $entries[] = [
                'date_add' => $row['date_add'],
                'revision' => $row['revision'],
                'status'   => $row['status'],
                'label'    => $label,
                'color'    => $color,
                'message'  => $row['message'],
            ];
        }

        return [
            'module_name'    => $this->name,
            'backoffice'     => $this->getBridge()->getId(),
            'reference'      => $reference,
            'status'         => $current['status'],
            'status_label'   => $statusLabel,
            'status_color'   => $statusColor,
            'status_message' => $current['message'],
            'exportable'     => $exportable,
            'entries'        => $entries,
        ];
    }

    /**
     * Retourne le libellé et la couleur associés à un statut d'exportation.
     *
     * @param string $status Un des SyncerOrders::EXPORT_STATUS_*
     * @return array [string $label, string $color]
     */
    private function getExportStatusMeta(string $status) : array
    {
        switch ($status)
        {
            case SyncerOrders::EXPORT_STATUS_EXPORTED:
                return ['Exportée', '#4caf50'];
            case SyncerOrders::EXPORT_STATUS_ERROR:
                return ['Erreur d\'export', '#e53935'];
            case SyncerOrders::EXPORT_STATUS_SKIPPED:
                return ['Non exportée (ignorée)', '#ff9800'];
            case SyncerOrders::EXPORT_STATUS_RETRY:
                return ['Report (backoffice injoignable)', '#fb8c00'];
            default:
                return ['Non exportée', '#9e9e9e'];
        }
    }

    /**
     * Montage des hooks maisons !
     *
     * @param array $params
     * @return string
     */
    public function hookDisplayProductPriceBlock($params) : string
	{
        $build = '';

		if (isset($params["type"]) && $params["type"] == "after_price" && \Configuration::get($this->BridgePrefix."prestaconnector_use_mpm"))
			$build .= $this->hookDisplayMultiProductSelector($params);

        return $build;
	}

    /**
     * Affichage des disponibilités, le cas échéant
     * 
     * @param array $params
     * @return string
     */
    public function hookDisplayProductAdditionalInfo($params) : string
    {
        if (\Configuration::get($this->BridgePrefix."prestaconnector_use_availabilities"))
            return $this->hookDisplayStoreStocks($params);

        return '';
    }

    /**
     * Affichage du produit connexe manuel
     * 
     * @param array $params
     * @return string
     */
	public function hookDisplayMultiProductSelector($params) : string
	{
        if (!isset($params["product"]))
            return '⚠ Renseignez le paramètre `product` pour afficher le connecteur ici ! ⚠';   // Pas de chance

        if (!$params["product"] || !$params["product"]->id_product)
            return '';   // Pas de chance

        // On a besoin d'un gestionnaire de produits connexes
        $mpm = $this->getMpm();
        return $mpm->buildSelector($params["product"]->id_product);
	}
    
    /**
     * Chargement des disponibilités de stock par magasin
     */
    public function hookBuildStoreStocks(& $params)
    {
        if (!$params["product"] || !$params["product"]->id_product)
            return '';   // Pas de chance

        // On a besoin de calculer les stocks, on charge tous les stocks de ce produit
        $stocks = [];

        $db = \Db::getInstance(); 
        $results = $db->executeS('SELECT id_product_attribute, store_code, qty FROM ' . $this->TblPrefix . 'stocks WHERE id_product = '.(int) $params["product"]->id_product.' AND qty > 0');
        if ($results === false)
            return '';   // Pas de chance : BUG

        // Chargement du mappage des magasins
        $stores = $this->getCfg()->get(SyncConfiguration::CFG_MAP_STORES);

        // Chargement des boutiques
        foreach (\Store::getStores(\Context::getContext()->language->id) as $shop)
        {
            $stocks[$shop['id']] = [
                'name' => $shop['name'],
                'qties' => [],
            ];
        }

        foreach ($results as $row)
        {
            // On va chercher le code du magasin dans le mappage
            $store_id = 0;
            if (isset($stores[$row['store_code']]))
                $store_id = $stores[$row['store_code']];
            else if (isset($stores['*']))
                $store_id = $stores['*'];

            if ($store_id > 0)
            {
                $product_attribute_id = (int) $row['id_product_attribute'];
                $qty = (int) $row['qty'];

                if (!isset($stocks[$store_id]['qties'][$product_attribute_id]))
                    $stocks[$store_id]['qties'][$product_attribute_id] = 0;
                $stocks[$store_id]['qties'][$product_attribute_id] += $qty;
            }
        }

        $params['stores_stocks'] = $stocks;
    }

    /**
     * Affichage des disponibilités de stock par magasin
     * 
     * @param array $params    
     * @return string
     */
    public function hookDisplayStoreStocks($params) : string
    {
        // Calcul des stocks
        $this->hookBuildStoreStocks($params);
        $stocks = isset($params['stores_stocks']) ? $params['stores_stocks'] : null;

        $id_product_attribute = 0;
        if (isset($params["product"]))
            $id_product_attribute = (int) $params["product"]->id_product_attribute;            

        $html = '<div class="prestaconnector-stores-stocks">';
        $html .= '<p class="prestaconnector-stores-stocks-title">Disponibilités dans nos magasins&nbsp;:</p>';
        
        $html .= '<div class="details">';

        if ($stocks)
        {
            $trigger = \Configuration::get($this->BridgePrefix . 'prestaconnector_availability_trigger');
            foreach ($stocks as $store_id => $details)
            {
                $html .= '<div class="store">';
                $html .= '<div class="name">' . $details['name'] . '</div>';                

                $qty = isset($details['qties'][$id_product_attribute]) ? $details['qties'][$id_product_attribute] : 0;
                $dispo = 'outofstock';
                $help = "Non disponible";
                if ($qty > 0)
                {
                    $dispo = $qty > $trigger ? 'available' : 'lowstock';
                    $help = $qty > $trigger ? 'Disponible' : 'Attention, dernières pièces disponibles !';
                }

                $html .= '<div class="qty '.$dispo.'"title="'.$help.'">&nbsp;</div>';
                $html .= '</div>';
            }
        }
        
        $html .= '</div>';
        $html .= '</div>';
        
        return $html;
    }

    /**
     * Chargement du fournisseur de références
     * Lève une exception si le fournisseur de référence n'existe pas et ne peut être créé (et associé)
     * 
     * @return int L'identifiant du fournisseur de références
     */
    public function getOrCreateSupplierId() : int
    {
        $cfg = $this->getCfg();

        // Ensuite, on regarde si dans la configuration, nous avons l'identifiant du fournisseur pour ce module
        $supplierId = $cfg->get(SyncConfiguration::CFG_SUPPLIER);
        if ($supplierId)
        {
            // Chargement du fournisseur
            $supplier = new \Supplier($supplierId);
            if ($supplier->id != $supplierId)
            {
                // On log dans prestashop
                \PrestaShopLogger::addLog('Impossible de charger le fournisseur de référence #' . $supplierId . ' => recréation ! ' . $supplier->name, 3, null, "supplier", $supplierId);
                $supplierId = 0;
            }
        }

        if (!$supplierId)
        {
            \PrestaShopLogger::addLog('Fournisseur de références non trouvé ! ', 2);

            $supplier_name = ucfirst($this->getBridge()->getId());

            // On cherche un fournisseur du même nom ?
            $supplierId = \Supplier::getIdByName($supplier_name);
            if (!$supplierId)
            {
                // Toujours pas trouvé : création
                $supplier = new \Supplier();
                // Le nom du fournisseur est le nom qui précède "PolarisPrestaConnector" dans le nom du module, avec une majuscule                
                $supplier->name = $supplier_name;
                $supplier->active = 1;
                $supplier->add();

                $supplierId = $supplier->id;
                if (!$supplierId)
                    throw new Exception('Impossible de créer le fournisseur de référence !');                

                \PrestaShopLogger::addLog(sprintf('Création du fournisseur %s pour fournisseur de références', $supplier_name), 1, null, "supplier", $supplierId);

                // Création de l'adresse associée ...
                $address = new \Address();
                $address->id_country = \Configuration::get('PS_COUNTRY_DEFAULT');
                $address->id_state = 0;
                $address->alias = 'Fournisseur de références';
                $address->company = $supplier_name;                
                $address->lastname = '-';
                $address->firstname = '-';
                $address->address1 = '-';
                $address->postcode = '-';
                $address->city = '-';
                $address->id_supplier = $supplierId;
                $address->save();
            }
            else
                \PrestaShopLogger::addLog(sprintf('Utilisation du fournisseur %s pour fournisseur de références', $supplier_name), 1, null, "supplier", $supplierId);

            // Sauvegarde de l'identifiant du fournisseur dans la configuration
            $cfg->set(SyncConfiguration::CFG_SUPPLIER, $supplierId);
        }

        return (int) $supplierId;
    }

    /**
     * Validation d'une commande !
     * 
     */
    public function hookActionValidateOrder($params)
    {
        /** @var Order $order */
        $order = $params['order'];
        if (!Validate::isLoadedObject($order)) return;

        $orderCartRules = $order->getCartRules();
        if (!$orderCartRules) return;

        // Langue ID par défaut
        $id_lang = (int)Configuration::get('PS_LANG_DEFAULT');

        foreach ($orderCartRules as $ocr) {
            $idCartRule = (int)$ocr['id_cart_rule'];
            $usedValue  = (float)$ocr['value']; // montant réellement déduit sur la commande

            $cartRule = new CartRule($idCartRule);
            if (!Validate::isLoadedObject($cartRule)) continue;
            
            $name = is_array($cartRule->name) ? $cartRule->name[$id_lang] : $cartRule->name;
            if (!str_starts_with($name, 'Carte prépayée')) continue;   // Que les cartes réutilisables !

            $original  = (float)$cartRule->reduction_amount;
            $remaining = $original - $usedValue;

            // arrondi PS
            $remaining = (float) Tools::ps_round($remaining, 2);

            if ($remaining <= 0.0) {
                // Plus de solde : tu peux désactiver le bon
                $cartRule->reduction_amount = 0;
                $cartRule->active = 0;
                $cartRule->update();
                continue;
            }

            // 1) Mettre à jour le solde
            $cartRule->reduction_amount = $remaining;

            // 2) Garantir réutilisable (évite blocage "déjà utilisé")
            $cartRule->partial_use = 0;
            $cartRule->quantity = 1;

            $cartRule->update();
        }
    }

    /**
     * Hook de la page de login admin Prestashop.
     *
     * Rend un bouton « Équipe VEGA » ou « Équipe LCV » selon le bridge actif,
     * qui démarre le flow SSO OIDC vers Authentik. Le formulaire de login
     * local reste actif pour les utilisateurs normaux.
     *
     * Ne rend rien si la backdoor est désactivée en configuration.
     */
    public function hookDisplayAdminLogin($params) : string
    {
        try
        {
            $auth = new \PrestaShop\Module\PolarisPrestaConnector\OidcAuthenticator($this);
 
            if (!$auth->isEnabled())
                return '';

            $backoffice = $this->getBridge()->getId();
            $label    = htmlspecialchars($auth->getButtonLabel(), ENT_QUOTES, 'UTF-8');
            $loginUrl = htmlspecialchars($auth->getLoginUri(),   ENT_QUOTES, 'UTF-8');
 
            // HTML autonome — pas de dépendance Twig (qui n'est pas garanti
            // d'être initialisé sur la page de login admin).
            return <<<HTML
<div class="pc-sso-login" style="margin: 1.5em; padding-top: 1.5em; border-top: 1px solid #e0e0e0; text-align: center;">
    <p style="font-size: 0.85em; color: #888; margin-bottom: 0.8em;">
        Vous êtes membre de l'équipe support {$backoffice}&nbsp;?
    </p>
    <a href="{$loginUrl}" class="btn btn-primary btn-lg" style="display: inline-block;">
        <i class="material-icons" style="vertical-align: middle; margin-right: 0.3em;">vpn_key</i>
        {$label}
    </a>
</div>
HTML;
        }
        catch (\Throwable $e)
        {
            // Une erreur ici ne doit jamais bloquer la page de login normale
            \PrestaShopLogger::addLog('PolarisPrestaConnector SSO : erreur hookDisplayAdminLogin : ' . $e->getMessage(), 2);
            return '';
        }
    }

}