/**
 * @file Ce fichier regroupe les fonctions simples de recherche et de test utiles, mais trop puissantes pour être exportées directement.
 * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
 * @author hawkspar
 */

import LDAP from './basics.js';
// Essentiels pour le fichier de config
import path from 'path';
import fs from 'fs';

// Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement
var configPath = path.resolve('./','ldap_config.json');
var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

//------------------------------------------------------------------------------------------------------------------------
// Fonctions de recherche
//------------------------------------------------------------------------------------------------------------------------

export class SmartSearch {
    /**
     * @class Cette classe contient des fonctions de recherche génériques trop puissantes pour être exportées tel quel.
     * @summary Constructeur vide.
     * @author hawkspar
    */
    constructor() {}

    /**
     * @summary Fonction qui interroge le LDAP et retrouve les groupes (voir LDAP) qui ressemblent
     *  à l'entrée. Etape 0 vers un vrai TOL (Trombino On Line).
     * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. 
     * Accepte des champs exacts ou incomplets mais pas approximatifs
     * et ne gère pas l'auto-complete. Cette fonction utilise aussi config.json. MEF Timeout pour
     * des recherches trop vagues. Renvoit une liste d'uid.
     * Elle utilise LDAPEscape pour éviter les injections.
     * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe.
     * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final
     * @return {Promise(string[])} Liste des uid de groupes dont le nom ressemble à l'input
     * @static @async
     */
    static async groups(input, return_attributes) {
        // Construction du filtre custom
        let filter= "(|("+config.key_id+"="+ input+")" +    // On cherche la valeur exacte
                    "(|("+config.key_id+"=*"+input+")" +    // La valeur finale avec des trucs avant ; wildcard *
                    "(|("+config.key_id+"=*"+input+"*)"+    // La valeur du milieu avec des trucs avant et après
                    "("+  config.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après

        // Appel rechercheLDAP avec filtre de l'espace 
        try {
            return LDAP.search(config.dn_groups, return_attributes, filter);
        }
        catch(err) {
            throw "Erreur lors de la recherche intelligente d'un groupe.";
        }
    }

    /**
     * @summary Fonction qui renvoit les attributs demandés des paxs validant les critères de recherche. Première étape vers vrai TOL (Trombino On Line).
     * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs
     * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Elle utilise LDAPEscape pour éviter les injections.
     * Utiliser trouverGroupesParTypes pour chaque champ relié à groups.
     * @arg {Object} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse
     * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exempl pour chercher un membre
     * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
     * @arg {string} data[givenName] - Prénom 
     * @arg {string} data[lastName] - Nom
     * @arg {string} data[nickname] - Surnom
     * @arg {string} data[nationality] - Nationalité (non implémentée pour l'instant, pas de format spécifique)
     * @arg {string} data[promotion] - String de l'année de promo
     * @arg {string} data[phone] - String du numéro de portable
     * @arg {string} data[mail] - Adresse mail
     * @arg {string} data[ips] - Une ou des adresses ip
     * @arg {string} data[school] - Ecole d'appartenance (pour l'instant instable). Doit être exact.
     * @arg {string} data[groups] - Un ou plusieurs groupes (pas de différence entre membre simple et admin). Doit être exact.
     * @arg {string} data[studies] - PA ou autre. Doit être exact.
     * @arg {string} data[sport] - Section sportive ou autre Doit être exact.
     * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final
     * @return {Promise(Object[])} Liste de dictionnaires de profils en cohérence avec l'input avec pour clés les attributs des profils.
     * @static @async
     */
    static async users(data, return_attributes) {
        let filter="";
        // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans config encore
        for (var key in data) {
            if ((data[key]!= undefined) & (data[key] != '')) {                    // Si il y a qque chose à chercher pour ce filtre
                if (!Array.isArray(data[key])) { data[key]=[data[key]]; }        // Gestion d'une liste de valeurs à rechercher
                // Iteration pour chaque valeur fournie par l'utilisateur
                data[key].forEach(val => {
                    // Traduction en language LDAP
                    let attribute = config.user[key];
                    // Creation incrémentale du filtre
                    filter="(&"+filter+ "(|("+attribute+"="+ val+")"+      // On cherche la valeur exacte
                                        "(|("+attribute+"=*"+val+")"+      // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs)
                                        "(|("+attribute+"=*"+val+"*)"+     // La valeur du milieu avec des trucs avant et après
                                        "("+  attribute+"="+ val+"*)))))"; // La valeur du début avec des trucs après
                });
            }
        }
        // Appel rechercheLDAP avec filtre de l'espace 
        try {
            return LDAP.search(config.dn_users, return_attributes, filter);
        }
        catch(err) {
            throw "Erreur lors de la recherche intelligente d'un utilisateur.";
        }
    }
}

//------------------------------------------------------------------------------------------------------------------------
// Fonctions intermédiaires TBT
//------------------------------------------------------------------------------------------------------------------------

export class Tests {
    /**
     * @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique.
     * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier 
     * qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut
     * dans le dn fourni.
     * @param {string} value - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération
     * @param {string} attribute - Attribut à tester
     * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique
     * @param {function} changeValue - Fonction qui prend uniquement en argument l'id courant et 
     * le nombre d'itérations et qui renvoit la prochaine valeur de l'attribut 
     * @param {int} n [0] - Nombre d'itérations (à initialiser à 0)
     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
     * @static @async
     */
    static async ensureUnique(value, attribute, dn, changeValue, n=0) {
        // Recherche d'autres occurences de l'id
        try {
            return LDAP.search(dn, config.key_id, "("+attribute+"="+value+")").then(matches => {
                if (!matches) { throw ""; }
                // On renvoit la valeur si elle est bien unique
                else if (matches.length==0) { return value; }
                // Sinon, on tente de nouveau notre chance avec la valeur suivante
                else { return this.ensureUnique(changeValue(value, n+1), dn, changeValue, n+1); }
            });
        }
        catch(err) {
            throw "Erreur lors de la recherche d'une valeur pour assurer son unicité.";
        }
    }

    /**
     * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique.
     * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
     * @param {string} givenName - Prénom
     * @param {string} lastName - Nom
     * @param {string} promotion - Année de promotion
     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
     * @static @async
     */
    static async generateUid(givenName, lastName, promotion) {
        try {
            // normalize et lowerCase standardisent le format
            return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), config.key_id, config.dn_users, (id,n) => {
                if (n==1) { id+='.'+promotion; }    // Si prénom.nom existe déjà, on rajoute la promo
                else if (n==2) { id+='.'+n-1; }     // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1
                else if (n>2) { id+=n; }           // Ensuite on continue .123, .1234, etc...
                return id;
            });
        }
        catch(err) {
            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
        }
    }

    /**
     * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
     * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
     * @param {string} name - Nom
     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
     * @static @async
     */
    static async generateReadableId(name) {
        try {
            // normalize et lowerCase standardisent le format
            return this.ensureUnique(name.toLowerCase().normalize('UFD'), config.key_id, config.dn_groups, (id,n) => {
                if (n==1) { id+='.'+n; }    // Si nom existe déjà, on essaie nom.1
                else if (n>1) { id+=n; }   // Ensuite on continue .12, .123, etc...
                return id;
            });
        }
        catch(err) {
            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
        }
    }

    /**
     * @summary Cette fonction teste une valeur dummy (0) pour un identifiant numérique puis le fait évoluer aléatoirement (entre 1 et 100 000) jusqu'à ce qu'il soit unique.
     * @param {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}.
     * @param {string} attribut - Intitulé exact de l'id concerné
     * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique
     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
     * @static @async
     */
    static async generateId(attribut, dn) {
        try {
            return this.ensureUnique("0", attribut, dn, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); });
        }
        catch(err) {
            throw "Erreur lors de l'assurance de l'unicité d'un unique identifier numérique.";
        }
    }
}