<?php
namespace PrestaShop\Module\LCVPrestaConnector;

use Exception;
use finfo;
use PrestaShop\Module\LCVPrestaConnector\Models\Customer;
use PrestaShopCollection;

/**
 * Synchronisation des clients
 */
class SyncerCustomers
{
    /**
     * Synchroniseur
     * 
     */
    private Syncer $syncer;
    /**
     * Indicateur de progression
     */
    private SyncProgress $progress;

    /**
     * Groupe de clients du backoffice
     */
    private int $backofficeCustomerGroupId = 0;

    /**
     * Constructeur
     * 
     * @param Syncer $syncer Synchroniseur
     * @param SyncProgress|null $progress Indicateur de progression
     */
    public function __construct(Syncer $syncer, SyncProgress|null $progress = null)
    {
        $this->syncer = $syncer;
        $this->progress = $progress ?? new SyncProgress($syncer);

        // On vérifie que la configuration contient un groupe pour les clients du backoffice
        // Si ce n'est pas le cas, on le crée
        $this->backofficeCustomerGroupId = $this->syncer->module->getCfg()->get(SyncConfiguration::CFG_CUSTOMERS_GROUP_ID) ?? 0;
        if (!$this->backofficeCustomerGroupId)
        {
            // Création du groupe de clients du backoffice
            $group = new \Group();
            // On affecte le même nom à toutes les langues
            foreach (\Language::getLanguages(false) as $lang)
                $group->name[$lang['id_lang']] = 'Clients '.$this->syncer->module->getBridge()->getId();
            $group->reduction = 0;
            $group->price_display_method = 0;
            $group->show_prices = 1;
            $group->add();
            $this->syncer->module->getCfg()->set(SyncConfiguration::CFG_CUSTOMERS_GROUP_ID, $group->id);
            $this->backofficeCustomerGroupId = $group->id;
        }
    }

    /**
     * Synchronisation des stocks
     */
    public function sync()
    {
        $bridge = $this->syncer->bridge;

        // On commence par envoyer les clients, si on doit les exporter
        if ($this->syncer->module->getCfg()->get(SyncConfiguration::CFG_EXPORT_CUSTOMERS))
        {            
            $startTime = microtime(true);            
            try
            {
                $nbRefs = $this->exportCustomers();            
                $stopTime = microtime(true);
                if ($nbRefs > 0)
                    $this->syncer->audit(sprintf("[INF] Exportation de %d clients effectuée en %.2f secondes", $nbRefs, $stopTime - $startTime));    
            }
            catch (Exception $e)
            {
                $this->syncer->audit(sprintf("[ERR] Erreur lors de l'exportation des clients : %s", $e->getMessage()));                
            }
        }

        // Puis on récupère les clients
        if ($this->syncer->module->getCfg()->get(SyncConfiguration::CFG_IMPORT_CUSTOMERS))
        {
            $nbRefs = 0;
            $startTime = microtime(true);        

            // On récupère les bons depuis le pont, en boucle
            $this->progress->startStep('import_customers', 'Importation des clients...', '%d clients importés');
            try
            {
                while ($customers = $bridge->pullCustomers())
                    $nbRefs += $this->importCustomers($customers);
            }
            catch (Exception $e)
            {
                $this->syncer->audit(sprintf("[ERR] Erreur lors de l'importation des clients : %s", $e->getMessage()));                
                $this->progress->error('import_customers', $e->getMessage());
            }
            finally
            {
                $this->progress->endStep('import_customers');
            }

            $stopTime = microtime(true);
            if ($nbRefs > 0)
                $this->syncer->audit(sprintf("[INF] Importation de %d clients effectuée en %.2f secondes", $nbRefs, $stopTime - $startTime));    
        }
    }

    /**
     * Importation des clients précisés
     * 
     * @param array<Customer> $vouchers clients à synchroniser
     * @return int Nombre de clients synchronisés 
     */
    public function importCustomers(array & $customers) : int
    {
        $nbDone = 0;  

        // Gestion de la référence client externe : si le pont supporte les références clients externes, on les récupère pour savoir si on doit exporter la nouvelle référence en cas de changement de référence
        $refExtsSupport = $this->syncer->bridge->supportCustomersRefExt() && $this->syncer->module->getCfg()->get(SyncConfiguration::CFG_EXPORT_CUSTOMERS);

        // Faisons des paquets de 100 vouchers max.
        $nbAtATime = 100;
        while (count($customers) > 0)
        {
            $customers_slice = array_slice($customers, 0, $nbAtATime);
            $customers = array_slice($customers, $nbAtATime);
            
            // Le plus simple est de charger tous les Customers de la bdd en mémoire et de les comparer            
            $existingCustomers = [];

            /** @var \Customer $customer */
            foreach (
                (new PrestaShopCollection('Customer'))->sqlWhere("id_customer IN (".implode(",", 
                    array_unique(
                        array_map(function($v) { return Customer::extractIdFromUid($v->uid); }, $customers_slice))) . ")")->getAll() as $psCustomer)
                $existingCustomers[Customer::getCustomerUid($psCustomer)] = $psCustomer;
            
            /** @var array<\Customer> $existingCustomers */

            // On a maintenant un tableau de clients existants par mail
            // On le parcours
            foreach ($customers_slice as $customer)
            {
                $nbDone++;                
                $this->progress->progress('import_customers');

                try
                {
                    // Si le client doit être effacé
                    if ($customer->delete)
                    {
                        if ($customer->uid)
                        {
                            $psCustomer = $existingCustomers[$customer->uid] ?? null;
                            if ($psCustomer && $psCustomer->id)
                            {
                                $psCustomer->delete();
                                $this->syncer->audit(sprintf("[INF] Le client %s a été effacé", $customer->mail));
                            }
                        }
                        // Et c'est fini !
                        continue;
                    }

                    // On vérifie si le mail est correct
                    if (empty($customer->mail))
                    {
                        // Pas besoin de signaler ce cas !
                        continue;
                    }

                    if (!\Validate::isEmail($customer->mail))
                    {
                        $this->syncer->audit(sprintf("[INF] Le client %s a été ignoré car son adresse mail est incorrecte", 
                            $customer->mail));
                        continue;
                    }

                    // On vérifie le nom du client
                    if (!\Validate::isCustomerName($customer->lastname))
                    {
                        $this->syncer->audit(sprintf("[INF] Le client %s a été ignoré car son nom '%s' est incorrect (alpha uniquement)", 
                            $customer->mail, $customer->lastname));
                        continue;
                    }

                    // On vérifie le prénom du client
                    if (!\Validate::isCustomerName($customer->firstname))
                    {
                        $this->syncer->audit(sprintf("[INF] Le client %s a été ignoré car son prénom '%s' est incorrect (alpha uniquement)", 
                            $customer->mail, $customer->firstname));
                        continue;
                    }

                    // id_customer non vide ? On essaye de le charger !
                    /** @var \Customer $psCustomer */
                    $psCustomer = null;

                    if ($customer->uid)
                        $psCustomer = $existingCustomers[$customer->uid] ?? null;

                    if (!isset($psCustomer) || !$psCustomer)
                    {
                        // par email
                        $psCustomer = (new \Customer())->getByEmail($customer->mail, null, false);
                        if ($psCustomer && $psCustomer->id)
                            $existingCustomers[Customer::getCustomerUid($psCustomer)] = $psCustomer;  // cache !
                        else
                            $psCustomer = null;                    
                    }

                    $isCreation = false;
                    if ($psCustomer === null)
                    {
                        $psCustomer = new \Customer();
                        // Mot de passe au hasard sans utiliser Tools::encrypt qui est deprecated
                        $psCustomer->passwd = \Tools::passwdGen(32);
                        $isCreation = true;
                    }

                    $customerAudit = new AuditHelper($psCustomer);

                    // On met à jour
                    $psCustomer->email = $customer->mail;
                    $psCustomer->is_guest = false;  // Il est sous le backoffice, ce n'est donc pas un compte invité !
                    $psCustomer->lastname = $customer->lastname;
                    $psCustomer->firstname = $customer->firstname;
                    if ($customer->company !== null)
                        $psCustomer->company = $customer->company;

                    // Gender ?
                    $gender = $this->syncer->module->getBridge()->getPsGenderFromGender($customer->civilite);
                    if ($gender)
                        $psCustomer->id_gender = $gender->id;

                    if ($customer->newsletter !== null)
                    {
                        $psCustomer->newsletter = $customer->newsletter;
                        if ($psCustomer->newsletter && !isset($psCustomer->newsletter_date_add))
                            $psCustomer->newsletter_date_add = date('Y-m-d H:i:s');
                    }
                    if ($customer->demarchage !== null)
                        $psCustomer->optin = $customer->demarchage;
                    
                    $customerAudit->save($this->syncer);

                    // La référence client a changé !                    
                    if ($refExtsSupport && $customer->uid != Customer::getCustomerUid($psCustomer))
                    {
                        // On doit exporter la nouvelle référence !                        
                        $this->exportCustomer($psCustomer);
                    }
                    
                    if ($isCreation)
                    {
                        // Ajout du client au groupe du backoffice
                        \Hook::exec('actionCustomerAccountAdd', [ 'newCustomer' => $psCustomer ]);    
                        $psCustomer->addGroups([$this->backofficeCustomerGroupId]);
                    }
                }
                catch (Exception $e)
                {
                    $this->syncer->audit(sprintf(
                    "[ERR] Erreur lors de l'importation du client %s : %s\n  dans %s ligne %d\nStack trace:\n%s",
                        $customer->mail,
                        $e->getMessage(),
                        $e->getFile(),
                        $e->getLine(),
                        $e->getTraceAsString()
                    ));

                    //$this->syncer->audit(sprintf("[ERR] Erreur lors de l'importation du client %s : %s", $customer->mail, $e->getMessage()));
                }
            }
        }

        return $nbDone;
    }

    /**
     * Exportation du client précisé
     * 
     * @param \Customer $customer Client à exporter
     * @return bool Vrai si la client a été exportée, faux sinon
     */
    public function exportCustomer(\Customer $customer) : bool
    {
        try
        {
            if (!$this->syncer->bridge->isCustomerValidForExport($customer))
                return false;
                
            // on exporte le client ...

            // Puis on exporte le client
            $exportWrn = $this->syncer->bridge->pushCustomer($customer);
            if (!$exportWrn)
            {
                return true;
            }
            else
            {
                // On signale dans le journal d'audit que la commande a été ignorée et la raison
                $this->syncer->audit("[INF] Client ".$customer->email." ignoré : ".$exportWrn);
                return false;
            }            
        }
        catch (\Exception $e)
        {
            // On met les commandes en erreur, sans déclencher de hook
            $this->syncer->audit("[ERR] Erreur lors de l'export du client ".$customer->email." : ".$e->getMessage());
            throw $e;   
        }
    }

    /**
     * Importation des clients précisés
     * 
     * @return int Nombre de clients synchronisés 
     */
    public function exportCustomers() : int
    {
        $cfg = $this->syncer->module->getCfg();
        $nbDone = 0;

        $this->progress->startStep('export_customers', 'Exportation des clients', '%d clients exportés');

        // Chargement de la date des derniers clients exportés
        $lastSync = $cfg->get(SyncConfiguration::SYNC_EXPORT_CUSTOMER);

        // lastSync contient deux infos : date/no ; on sépare ces deux infos
        $lastSync = explode('/', $lastSync ?? '');
        if ($lastSync === FALSE || count($lastSync) != 2)
            $lastSync = [ date('Y-m-d H:i:s', 0), 0 ];

        // Clause optionnelle : inclure aussi les comptes inscrits à la newsletter, même sans commande
        $includeNewsletter = $cfg->get(SyncConfiguration::CFG_EXPORT_CUSTOMERS_NEWSLETTER)
            ? ' OR newsletter = 1'
            : '';

        // Chargement de tous les customer à synchroniser qui ont une date de mise à jour supérieure à la dernière synchro ou égale mais avec un numéro plus grand,
        // trié par date de mise à jour croissante puis par no, croissant
        // Chargement de tous les id_customer à synchroniser depuis la bdd
        do
        {
            // Tous les clients modifiés qui appartienent à un groupe de clients du backoffice ou qui ont passé au moins une commande
            // (et, si l'option est activée, ceux qui sont inscrits à la newsletter même sans commande)
            $customers = (new PrestaShopCollection('Customer'))
                        ->sqlWhere("(date_upd > '".pSQL($lastSync[0])."' OR (date_upd = '".pSQL($lastSync[0])."' AND id_customer > ".(int)$lastSync[1].')) AND
                                    (id_customer IN (SELECT DISTINCT id_customer FROM '._DB_PREFIX_.'orders)
                                    OR id_customer IN (SELECT id_customer FROM '._DB_PREFIX_.'customer_group WHERE id_group = '.(int)$this->backofficeCustomerGroupId.')'.$includeNewsletter.')')
                        ->sqlOrderBy('date_upd ASC, id_customer ASC')
                        ->setPageNumber(0)
                        ->setPageSize(100)
                        ->getAll();

            /** @var array<\Customer> $customers */          
            /** @var \Customer $psCustomer */
            foreach ($customers as $psCustomer)
            {
                $nbDone++;
                $this->progress->progress('export_customers');
                try
                {
                    // A cette fin, on se prépare
                    $lastSync = [ $psCustomer->date_upd, $psCustomer->id ];
                    $this->exportCustomer($psCustomer);                    
                }
                catch (Exception $e)
                {
                    // Et également dans le journal de Prestashop
                    \PrestaShopLogger::addLog(sprintf("Erreur lors de la l'exportation du client %s : %s", $psCustomer->email, $e->getMessage()), 3, null, $this->syncer->module->name); 
                }
            }

            // Fin de la boucle => on se note là où on s'est arrêté
            $cfg->set(SyncConfiguration::SYNC_EXPORT_CUSTOMER, implode('/', $lastSync));            
        } while (count($customers) > 0);

        $this->progress->endStep('export_customers');

        return $nbDone;
    }
}