/**
 * @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 {ldapConfig} from './config';
import {LDAP} from './basics';
import { userData } from './user';
import { groupData } from './group';

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

export class Tools {
    /**
     * @class Cette classe contient des fonctions intermédiaires qui ne sont pas destinées à être utilisées dans les resolvers.
     * @summary Constructeur vide.
     * @author hawkspar
    */
    constructor() {}
     
    /**
     * @summary Fonction qui renvoit toutes les infos relatives à un groupe ou un utilisateur particulier.
     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
     * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
     * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe)
     * @arg {string} id - Identifiant de la feuille cherchée (uid ou gid)
     * @return {Promise(T)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe tel que défini par le paramètre T.
     * @static
     * @async
     */
    static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> {
        if (domain='gr') {
            var dirtyKeys = ldapConfig.group;
        }
        else {
            var dirtyKeys = ldapConfig.user;
        }
        let cleanData : T;
        let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id);
        // Rename output
        for (let uncleanKey in dirtyData) {
            for (let cleanKey in cleanData) {
                if (uncleanKey=dirtyKeys[cleanKey]) { cleanData[cleanKey] = dirtyData[uncleanKey]; }
            }
        }
        return cleanData;
    }

    
    /**
     * @summary Fonction qui retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line).
     * Utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos.
     * @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. Va crasher si un champ n'est pas dans ldapConfig.
     * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
     * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe)
     * @arg {userData | groupData} 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 exemple pour chercher un membre
     * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
     * @return {Promise(string[])} ids des profils qui "match" les critères proposés.
     * @static
     * @async
     */
    static async genericSearch(domain : "us"|"gr", data : userData|groupData) : Promise<string[]> {
        let filter="";
        // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig 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]]; }          // Génération systématique d'une liste de valeurs à rechercher
                // Iteration pour chaque valeur fournie par l'utilisateur
                data[key].forEach(val => {
                    // Traduction en language LDAP
                    let attribute = "";
                    if (domain="us")    { attribute = ldapConfig.user[key]; }
                    else                { attribute = ldapConfig.group[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 avec filtre de l'espace 
        return LDAP.search(domain, [ldapConfig.key_id], null, filter);
    }
    
    /**
     * @summary Fonction qui édite un groupe existant dans le LDAP.
     * @desc Appelle {@link LDAP.change}.
     * @arg {userData | groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels...
     * Sauf 'gid', qui permet de savoir quel groupe modifier et qui est donc inchangeable. On peut modifier nickname par contre.
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async genericEdit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> {
        if (domain = "us") {
            var id=data['uid'];
            var dirtyKeys=ldapConfig.user;
        }
        else {
            var id=data['gid'];
            var dirtyKeys=ldapConfig.group;
        }
        // Renommage LDAP-friendly
        let dirtyData = {};
        Object.keys(data).forEach(function(key: string) {
            if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) {
                dirtyData[dirtyKeys.key]=data[key];
            }
        });
        return LDAP.change(domain,id,"replace",dirtyData);
    }

    /**
     * @callback changeValueCallback
     * @param {string} id - Id à modifier
     * @param {number} n - Nombre d'itérations
     * @return {string} Nouveau id
     */
    /**
     * @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 {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique
     * @param {changeValueCallback} 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: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n=0) : Promise<string> {
        // Recherche d'autres occurences de l'id
        try {
            return LDAP.search(domain, [ldapConfig.key_id], null, "("+attribute+"="+value+")").then(function (matches: string[]) {
                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 Tools.ensureUnique(changeValue(value, n+1), attribute, domain, 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 Tools.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: string, lastName: string, promotion: string) : Promise<string> {
        try {
            // normalize et lowerCase standardisent le format
            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (id: string, n: number) => {
                if (n=1) { id+='.'+promotion; }                // Si prénom.nom existe déjà, on rajoute la promo
                else if (n=2) { id+='.'+(n-1).toString(); }    // 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 Tools.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: string) : Promise<string> {
        try {
            // normalize et lowerCase standardisent le format
            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (id: string, n: number) => {
                if (n=1)        { id+='.'+n.toString(); }   // Si nom existe déjà, on essaie nom.1
                else if (n>1)   { id+=n.toString(); }       // 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 {string} attribut - Intitulé exact de l'id concerné
     * @param {"gr"|"us"} domain - Domaine 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: string, domain: "gr"|"us") : Promise<string> {
        try {
            return Tools.ensureUnique("0", attribut, domain, (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.";
        }
    }

    /**
     * @summary Fonction qui retrouve les groupes dont un individu est membre.
     * @desc Cette fonction utilise {@link LDAP.search} va directement à la feuille de l'utilisateur.
     * @arg {string} uid - Identifiant de l'individu à interroger (le plus souvent prenom.nom, parfois l'année, supposé valide)
     * @return {Promise(string[])} Liste des uid de groupes (noms flat des groupes) où l'id fourni est membre
     * @static
     * @async
     */
    static async getGroups(uid: string) : Promise<string[]> {
        try {
            return LDAP.search("us", [ldapConfig.user.groups], uid)[0];
        }
        catch(err) {
            throw "Erreur lors de la recherche des groupes d'un individu.";
        }
    }
    
    /**
     * @summary Fonction qui retrouve la liste des membres d'un groupe.
     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
     * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
     * @static
     * @async
     */
    static async getMembers(gid: string) : Promise<string[]> {
        try {
            return LDAP.search("gr", [ldapConfig.group.members], gid)[0];
        }
        catch(err) {
            throw "Erreur lors de la recherche des membres d'un groupe.";
        }
    }
    
    /**
     * @summary Fonction qui retrouve la liste des admins d'un groupe.
     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
     * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
     * @static
     * @async
     */
    static async getAdmins(gid: string) : Promise<string[]> {
        try {
            return LDAP.search("gr", [ldapConfig.group.admins], gid)[0];
        }
        catch(err) {
            throw "Erreur lors de la recherche des admins d'un groupe.";
        }
    }

    /**
     * @summary Cette fonction teste si un utilisateur est membre d'un groupe.
     * @desc Utilise les méthodes statiques {@link open.getGroups} et {@link open.getMembers}
     * @param {string} uid - Identifiant de l'utilisateur à tester 
     * @param {string} gid  - Identification du groupe à tester
     * @returns {Promise(boolean)} True si l'utilisateur est membre
     * @static
     * @async
     */
    static async isGroupMember(uid: string, gid: string) : Promise<boolean> {
        try {
            let lg = await Tools.getGroups(uid);
            let lm = await Tools.getMembers(gid);
            if (lg.includes(gid) && lm.includes(uid)) {
                return true;
            }
            else { return false; }
        }
        catch(err) {
            throw "Erreur lors du test d'appartenance à un groupe.";
        }
    }

    /**
     * @summary Cette fonction teste si un utilisateur est admin d'un groupe.
     * @desc Utilise la méthode statique {@link Open.getAdmins}
     * @param {string} uid - Identifiant de l'utilisateur à tester 
     * @param {string} gid  - Identification du groupe à tester
     * @returns {Promise(boolean)} True si l'utilisateur est administrateur
     * @static
     * @async
     */
    static async isGroupAdmin(uid: string, gid: string) : Promise<boolean> {
        try {
            let lm = await Tools.getMembers(gid);
            let la = await Tools.getAdmins(gid);
            if (la.includes(uid) && lm.includes(uid)) { return true; }
            else { return false; }
        }
        catch(err) {
            throw "Erreur lors du test d'appartenance au bureau d'administration un groupe.";
        }
    }
}