<?php
namespace PrestaShop\Module\PolarisPrestaConnector;

/**
 * Gestionnaire d'attributs
 * 
 */
class AttributesManager
{    
    /**
     * Les attributs du groupe suivant sont des couleurs qui doivent être mappées !
     * 
     * @var int[] identifiants des groupes d'attributs de couleur (plusieurs possibles)
     */    
    private array $color_mapped_attribute_groups_id = [];

    /**
     * Mapping couleur 
     */
    private array $colorMap = [];

    /**
     * Les groupes d'attributs sont chargés une seule fois en statique, par id
     * 
     * @var array<int, \AttributeGroup>|null tableau des id de features
     */
    private static ?array $attributeGroups = null;

    /**
     * Valeurs existantes des attributs
     * id_group_attribute => [ value => id_attribute ]
     * 
     * @var array<int, array<string, int>> tableau associatif de tableau associatif de valeurs d'attributs
     */
    private static array $attributeValues = [];

    /**
     * Attributs des déclinaisons
     * product_id => [ [ id_group_attribute => id_attribute ] ]
     * 
     * @var array<int, array<array<int, int>>> tableau associatif de tableau associatif de valeurs de features
     */
    public array $productsCombinations = [];

    /**
     * Déclinaisons existantes par produit
     * 
     * @var array<int, array<int, string>> tableau associatif de id de produit => id de déclinaison
     */
    private array $productAttributesCache = [];

    /**
     * Recalcule les positions des attributs spécifiés
     */
    private array $mustRecomputeOrders = [];

    /**
     * Récupère les features existantes
     * 
     * @return array<int, int> tableau des id de features
     */
    public static function & getExistingAttributeGroups() : array
    {
        // Chargement des id de features
        if (is_null(self::$attributeGroups)) {
            $db = \Db::getInstance();

            // Chargement des id de groupes d'attributs existant sur le système            
            $res = $db->executeS('SELECT `id_attribute_group` FROM `'._DB_PREFIX_.'attribute_group`');
            if ($res === FALSE)
                throw new \Exception('Erreur lors de la récupération des groupes d\'attributs de produits : '.$db->getMsgError());

            self::$attributeGroups = [];
            foreach ($res as $row)
                self::$attributeGroups[(int) $row['id_attribute_group']] = (int) $row['id_attribute_group'];
        }

        return self::$attributeGroups;
    }

    /**
     * Récupère les valeurs existantes pour une feature donnée
     * 
     */
    private static function loadAttributeValues(int $id_attribute_group)
    {        
        if (!array_key_exists($id_attribute_group, self::$attributeValues))
        {
            $db = \Db::getInstance();
            // Langue par défaut
            $id_lang = \Configuration::get('PS_LANG_DEFAULT');

            // Chargement des id de attributs values existantes sur le système
            $res = $db->executeS('SELECT a.`id_attribute`, al.`name` FROM `'._DB_PREFIX_.'attribute` AS a LEFT JOIN `'._DB_PREFIX_.'attribute_lang` AS al USING (`id_attribute`) WHERE al.`id_lang` = ' . $id_lang . ' AND a.`id_attribute_group` = '.$id_attribute_group);
            if ($res === FALSE)
                throw new \Exception('Erreur lors de la récupération des valeurs d\'attributs possibles de produits : '.$db->getMsgError());

            self::$attributeValues[(int) $id_attribute_group] = [];
            foreach ($res as $row)
                self::$attributeValues[(int) $id_attribute_group][$row['name']] = (int) $row['id_attribute'];
        }
    }

    /** 
     * Instancie un nouveau gestionnaire d'attributs
     * 
     * @param array<int> $productsId produits gérés, ou aucun pour tout charger
    */
    public function __construct(Syncer $syncer, ?array $productsId = null)
    {
        $db = \Db::getInstance();        
                
        $this->color_mapped_attribute_groups_id = [];
        foreach ($syncer->module->getCfg()->get(SyncConfiguration::CFG_MAP_COLOR_ATTRS) as $group_id)
        {
            $this->color_mapped_attribute_groups_id[(int) $group_id] = (int) $group_id;
        }

        if (count($this->color_mapped_attribute_groups_id) > 0)
        {            
            // Chargement du mappage couleur
            $this->colorMap = $syncer->module->getCfg()->get(SyncConfiguration::CFG_COLOR_MAP);
            if (!isset($this->colorMap))
                $this->colorMap = [];
            if (!isset($this->colorMap["strict"]))
                $this->colorMap["strict"] = [];
        }

        // Chargement des combinaisons des produits        
        $sql = 'SELECT pa.`id_product`, pa.`id_product_attribute`, a.id_attribute_group, a.`id_attribute` FROM `'._DB_PREFIX_.'product_attribute_combination` AS comb
                LEFT JOIN `'._DB_PREFIX_.'product_attribute` AS pa ON (comb.`id_product_attribute` = pa.`id_product_attribute`)
                LEFT JOIN `'._DB_PREFIX_.'attribute` AS a ON (a.`id_attribute` = comb.`id_attribute`)
                ';
        if ($productsId)
            $sql .= ' WHERE pa.`id_product` IN ('.implode(',', $productsId).')';
        $res = $db->executeS($sql);
        if ($res === FALSE)
            throw new \Exception('Erreur lors de la récupération des déclinaisons de produits : '.$db->getMsgError());        

        foreach ($res as $row)
        {
            $id_product = (int) $row['id_product'];
            $id_product_attribute = (int) $row['id_product_attribute'];
            $id_attribute_group = (int) $row['id_attribute_group'];
            $id_attribute = (int) $row['id_attribute'];
            
            if (!array_key_exists($id_product, $this->productsCombinations))
                $this->productsCombinations[$id_product] = [];

            if (!array_key_exists($id_product_attribute, $this->productsCombinations[$id_product]))
                $this->productsCombinations[$id_product][$id_product_attribute] = [];

            $this->productsCombinations[$id_product][$id_product_attribute][$id_attribute_group] = $id_attribute;
        }

        // On rempli le cache des attributs des produits        
        foreach ($this->productsCombinations as $id_product => $combinations)
        {
            $this->productAttributesCache[$id_product] = [];
            foreach (array_keys($combinations) as $id_product_attribute)
                $this->productAttributesCache[$id_product][] = $id_product_attribute;
        }        
    }

    /**
     * Obtient la liste des id_product_attribute pour un produit
     * 
     * @param int $id_product identifiant du produit
     * @return array<int> tableau des id_product_attribute
     */
    public function getProductAttributes(int $id_product) : array
    {
        return $this->productAttributesCache[$id_product] ?? [];
    }

    /**
     * Enregistre un nouveau id_product_attribute pour un produit
     * 
     * @param int $id_product identifiant du produit
     */
    public function registerProductAttribute(int $id_product, int $id_product_attribute)
    {
        if (!isset($this->productAttributesCache[$id_product]))
            $this->productAttributesCache[$id_product] = [];
        $this->productAttributesCache[$id_product][] = $id_product_attribute;
    }

    /**
     * Obtient la couleur mappée
     */
    public function getMappedColor(string $color) : string
    {
        // Cas facile
        if (array_key_exists($color, $this->colorMap["strict"]))
            return $this->colorMap["strict"][$color];

        // Pas trouvé, on regarde si c'est pas un multicolore : s'il contient / s'en est un
        if (strpos($color, '/') !== false || strpos($color, '&') !== false || strpos($color, ',') !== false)
            return "multicolore";        

        // Pas trouvé, on va essayer de trouver une couleur proche
        // On prend la couleur qui contient la couleur et le plus de caractères
        $best = null;
        foreach ($this->colorMap["strict"] as $c => $m)
        {            
            if (strpos($color, $c) !== false && (!$best || strlen($c) > strlen($best)))
                $best = $c;
        }        
        if ($best)
            return $this->colorMap["strict"][$best];

        // Pas trouvé, catch-all        
        if (array_key_exists("*", $this->colorMap["strict"]))
            return $this->colorMap["strict"]["*"];

        // pas reconnu
        return "autre";
    }

    /**
     * Obtient le mappage pour l'attribut
     * 
     * @param Syncer $syncer
     * @param int $id_attribute_group id du groupe d'attributs où chercher la valeur
     * @param string $attribute_value valeur recherchée
     */
    public function getAttributeFor(Syncer $syncer, int $id_attribute_group, string $attribute_value) : int
    {
        self::loadAttributeValues($id_attribute_group);

        $colorMapped = false;
        if ($this->color_mapped_attribute_groups_id[$id_attribute_group] ?? false)
        {
            $attribute_value = $this->getMappedColor($attribute_value);
            $colorMapped = true;
        }

        if (!array_key_exists($attribute_value, self::$attributeValues[$id_attribute_group]))
        {
            // La valeur n'existe pas encore, il faut la créée                
            $new_a = new \ProductAttribute();
            $new_a->id_attribute_group = (int) $id_attribute_group;
            $new_a->name = array_fill_keys(\Language::getIDs(false), $attribute_value);
            
            // C'est un attribut de couleur mappé, on recherche la couleur
            if ($colorMapped)
            {
                if (isset($this->colorMap["primary"][$attribute_value]))
                    $new_a->color = $this->colorMap["primary"][$attribute_value];
            }

            $new_a->add();

            $syncer->audit('Création de l\'attribut `'.$attribute_value.'` pour le groupe d\'attributs #'.$id_attribute_group);

            // Valeur créée, on l'ajoute à notre cache
            $this->mustRecomputeOrders[$id_attribute_group] = true;
            self::$attributeValues[$id_attribute_group][$attribute_value] = (int) $new_a->id;                
        }

        return self::$attributeValues[$id_attribute_group][$attribute_value];
    }

    /**
     * Obtient la combinaison de produit avec l'identifiant ou null si elle n'existe pas
     * 
     * @param int $id_product identifiant du produit
     * @param int $id_product_attribute identifiant de la combinaison
     */
    public function getCombinationWithId(int $id_product, int $id_product_attribute) : array|null
    {
        if (isset($this->productsCombinations[$id_product]))
            if (isset($this->productsCombinations[$id_product][$id_product_attribute]))
                return $this->productsCombinations[$id_product][$id_product_attribute];
        return null;
    }

    /**
     * Obtient l'identifiant de la combinaison de produit avec les valeurs d'attributs
     * 
     * @param int $id_product identifiant du produit
     * @param int $id_product_attribute identifiant de la combinaison
     */
    public function getCombinationIdWithValues(int $id_product, array $attributesId) : ?int
    {
        if (isset($this->productsCombinations[$id_product]))
        {
            foreach ($this->productsCombinations[$id_product] as $id_product_attribute => & $attributes)
            {                
                if (count($attributes) == count($attributesId))
                {
                    // Bon point, on a déjà le même nombre d'attributs
                    $found = true;
                    foreach ($attributesId as $id_attribute_group => $id_attribute)
                    {
                        if (!array_key_exists($id_attribute_group, $attributes) || $attributes[$id_attribute_group] != $id_attribute)
                        {
                            $found = false;
                            break;
                        }
                    }

                    if ($found)
                        return $id_product_attribute;
                }
            }
        }

        return null;
    }

    /**
     * Recalcule l'ordre des attributs si nécessaire par nom naturel, pour chaque groupe modifié
     * 
     */
    public function rebuildAttributeOrderIfNeed()
    {
        $db = \Db::getInstance();
        foreach ($this->mustRecomputeOrders as $id_attribute_group => $recompute)
        {
            if ($recompute)
            {
                // On recalcul les attributs par ordre naturel : alphabétique, numérique et taille (XS - S - M - L - XL)
                // directement avec MariaDB
                if ($db->execute('CREATE TEMPORARY TABLE tmp_new_positions (id_attribute INT PRIMARY KEY, new_position INT);') === FALSE)
                    throw new \Exception('Erreur lors de la création de la table temporaire pour la mise à jour de l\'ordre des attributs : '.$db->getMsgError());

                // On construit la table temporaire
                if ($db->execute('INSERT INTO tmp_new_positions (id_attribute, new_position) SELECT a.`id_attribute`, ROW_NUMBER() OVER(
                                        PARTITION BY a.`id_attribute_group`
                                        ORDER BY FIELD(al.`name`, \'XXXS\', \'XXS\', \'XS\', \'S\', \'M\', \'L\', \'XL\', \'XXL\', \'XXXL\', \'XXXXL\'), 
                                                 CAST(al.`name` AS FLOAT),
                                                 al.`name`
                                    ) AS new_position
                            FROM `'._DB_PREFIX_.'attribute` a
                            LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute`
                                    AND al.`id_lang` = '.\Configuration::get('PS_LANG_DEFAULT').')                            
                            WHERE a.`id_attribute_group` = '.$id_attribute_group) === FALSE)
                    throw new \Exception('Erreur lors de la mise à jour de l\'ordre des attributs : '.$db->getMsgError()); 

                // Mise à jour des positions
                if ($db->execute('UPDATE `'._DB_PREFIX_.'attribute` a
                            JOIN tmp_new_positions np ON (a.`id_attribute` = np.`id_attribute`)
                            SET a.`position` = np.`new_position`
                            WHERE np.`id_attribute` IS NOT NULL') === FALSE)
                    throw new \Exception('Erreur lors de la mise à jour de l\'ordre des attributs : '.$db->getMsgError());

                // Suppression de la table temporaire
                $db->execute('DROP TEMPORARY TABLE IF EXISTS tmp_new_positions;');                    

                $this->mustRecomputeOrders[$id_attribute_group] = false;
            }
        }        
    }
}