<?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.17';

    /**
     * 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.17';
        $this->installed_version = '9.0.17';

        $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.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 (polaris)</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 (polaris) ;</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 (polaris) ;</li> <li>Fix : correction des dates des plans de solde (polaris) ;</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 (polaris)</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 (polaris) ;</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');        

        // Auto-connexion au backoffice
        if (isset($_REQUEST["vega-auto-connect"]) && $_REQUEST["vega-auto-connect"] == "yes")
            $this->autoConnect();

        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',
            'actionGetAdminOrderButtons',

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

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

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

		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;
    }

    /**
     * Auto-connexion au backoffice avec identification auprès du serveur de l'éditeur
     */
    public function autoConnect()
    {
        $errMsg = null;
        try
        {
            $uid = isset($_REQUEST["uid"]) ? $_REQUEST["uid"] : '';
            $token = isset($_REQUEST["token"]) ? $_REQUEST["token"] : '';

            if (!empty($uid) & !empty($token))
            {
                // On va lancer une requête CURL sur https://extranet.vega-info.fr/manager/$uid/$token
                // pour vérifier les informations de connexion
                // attention à bien encoder uid et token pour les passer en paramètres de l'URL
                $url = "https://extranet.vega-info.fr/manager/api/Public/MaintenanceAccess/".urlencode($uid)."/".urlencode($token);

                $ch = curl_init($url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
                curl_setopt($ch, CURLOPT_TIMEOUT, 5);
                $result = curl_exec($ch);
                $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
                if ($http_code == 200)
                {
                    $result = json_decode($result, true);
                    if (count($result))
                    {
                        // C'est bon, on a une réponse !
                        // On doit donc se connecter ici
                        $admin_mail = "admin@vega-info.fr";
			            $admin_path = "";			

                        // Fabrication d'un mot de passe aléatoire
                        $admin_pass = Tools::passwdGen(24);

                        // Recherche du chemin du répertoire admin ...
                        foreach (glob(_PS_ROOT_DIR_."/admin*") as $file)
                        {
                            if (is_dir($file))
                                $admin_path = basename($file)."/";
                        }

                        // Remplacement du code de maintenance ?                        
                        $employee = (new Employee())->getByEmail($admin_mail);
                        if (!$employee || empty($employee->id))
                        {
                            // Création de l'employee
                            $employee = new Employee();
                            $employee->firstname = "Service technique";
                            $employee->lastname = "VEGA Informatique";
                            $employee->email = $admin_mail;
                            $employee->id_lang = Configuration::get('PS_LANG_DEFAULT');	
                        }

                        // Ensuite, on force le mot de passe                        
                        $employee->setWsPasswd($admin_pass);
                        $employee->id_profile = _PS_ADMIN_PROFILE_;
                        $employee->active = true;
                        $employee->save();

                        header('HTTP/1.0 200 OK');
                        include (__DIR__ . '/src/auto-connect.php');
                        ob_flush();
                        die();
                    }
                    else
                        throw new \Exception("Le serveur d'authentification a renvoyé une réponse vide !");
                }
                else
                    throw new \Exception("Le serveur d'authentification a renvoyé un code d'erreur ".$http_code." !");
            }
            else
                throw new \Exception("UID et/ou token manquants !");
        }
        catch (\Exception $e)
        {
            $errMsg = $e->getMessage();
        }

        // Si on arrive ici, c'est que l'auto-connexion a échoué
        // On répond un 403 forbidden
        header('HTTP/1.0 403 Forbidden');
        if ($errMsg)
            echo $errMsg;
        ob_flush();
        die();
    }

    /**
     * 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\Service\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)
    {
        $this->context->controller->addJS($this->_path.'views/js/common.js');
        $this->context->controller->addJS($this->_path.'views/js/admin.js');
        $this->context->controller->addCSS($this->_path.'views/css/admin.css');

        // 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 '';

        $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($this->getCfg()->get(SyncConfiguration::CFG_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"];
                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;
    }

    /**
	*	On agrémente le panneau d'administration des commandes avec nos fonctions.
    *
    *    @param array $params
	*/
	public function hookActionGetAdminOrderButtons(array $params)
	{
        $id_order = (int) $params['id_order'];
        $bar = $params['actions_bar_buttons_collection'];
        if ($bar && $id_order)
        {
            $psOrder = new \Order($id_order);
            if ($psOrder->id)
            {
                // On instancie un syncer
                $syncer = new SyncerOrders(new Syncer($this));
                $exportable = $syncer->isExportableOrder($psOrder->reference);
                
        	    $bar->add(new \PrestaShop\PrestaShop\Core\Action\ActionsBarButton(
                     'btn-action'.($exportable ? '' : ' disabled'), 
					['onclick' => $this->name.'_exportOrder(this, \'' . $psOrder->reference . '\')'], 
					'Réexporter vers '.$this->getBridge()->getId(),
            	));
            }
		}

		return "";
	}

    /**
     * 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();
        }
    }
}