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

/**
 * @interface searchUserFields
 * @desc Interface permettant la recherche d'un utilisateur avec des champs incomplets. Plusieurs valeurs sont possibles pour le même champ.
 * Aucun de ces champs n'est obligatoire, mais certains de ces champs doivent être exacts pour obtenir un bon résultat.
 * @var {string|string[]} givenName - Prénom(s)
 * @var {string|string[]} lastName - Nom(s)
 * @var {string|string[]} nickname - Surnom(s)
 * @var {string|string[]} nationality - Nationalité(s) (à implémenter)
 * @var {string|string[]} promotion - Année(s) de promo
 * @var {string|string[]} phone - Numéro(s) de téléphone
 * @var {string|string[]} mail - Adresse(s) courriel
 * @var {string|string[]} ip - Adresse(s) ip
 * @var {string|string[]} adress - Adresse(s)
 * @var {string} school - Ecole d'appartenance (instable, doit être exact)
 * @var {string|string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (doit être exact).
 * @var {string} course - PA ou autre. Doit être exact.
 */
export interface searchUserFields {
    givenName: string,
    lastName: string,
    nickname: string,
    nationality: string,
    promotion: string,
    phone: string,
    mail: string,
    ip: string,
    adress: string,
    school: string,
    groups: string[],
    studies: string,
    sport: string
}

//------------------------------------------------------------------------------------------------------------------------
// 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 ldapConfig.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: string, return_attributes: string[]) : Promise<string[]> {
        // Construction du filtre custom
        let filter= "(|("+ldapConfig.key_id+"="+ input+")" +    // On cherche la valeur exacte
                    "(|("+ldapConfig.key_id+"=*"+input+")" +    // La valeur finale avec des trucs avant ; wildcard *
                    "(|("+ldapConfig.key_id+"=*"+input+"*)"+    // La valeur du milieu avec des trucs avant et après
                    "("+  ldapConfig.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après

        // Appel rechercheLDAP avec filtre de l'espace 
        try {
            return LDAP.search(ldapConfig.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 {searchUserFields} 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[]} 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: searchUserFields, return_attributes: string[]) : 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]]; }          // 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 = ldapConfig.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 avec filtre de l'espace 
        try {
            return LDAP.search(ldapConfig.dn_users, return_attributes, filter);
        }
        catch(err) {
            throw "Erreur lors de la recherche intelligente d'un utilisateur.";
        }
    }
}

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

export class Tests {
    /**
     * @class Cette classe contient des fonctions de test d'unicité trop puissantes pour être exportées tel quel.
     * @summary Constructeur vide.
     * @author hawkspar
    */
    constructor() {}

    /**
     * @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 {string} dn - *Domain Name* 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, dn: string, changeValue: (string, number) => string, n=0) : Promise<string> {
        // Recherche d'autres occurences de l'id
        try {
            return LDAP.search(dn, ldapConfig.key_id, "("+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 Tests.ensureUnique(changeValue(value, n+1), attribute, 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: string, lastName: string, promotion: string) : Promise<string> {
        try {
            // normalize et lowerCase standardisent le format
            return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_users, (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 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: string) : Promise<string> {
        try {
            // normalize et lowerCase standardisent le format
            return this.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_groups, (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 {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: string, dn: string) : Promise<string> {
        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.";
        }
    }
}