/**
 * @file Ce fichier regroupe les fonctions fondamentales aux interactions avec le LDAP.
 * C'est ici que tout le filtrage est opéré, au plus bas niveau.
 * Toutes les fonctions écrites ici sont asynchrones et renvoient des Promises ce qui nécessite de les appeler avec la synthaxe
 * un peu particulière `f(args).then(res => ...)` pour exploiter leur résultat.
 * @author hawkspar
 */

import ldap from 'ldapjs';
// Toutes les entrées utilisateur sont escapées par sécurité
import ldapEscape from 'ldap-escape';
// Fichier de ldapConfig du ldap
import {ldapConfig, credentialsLdapConfig} from './config';

// Connection au serveur LDAP avec des temps de timeout arbitraires
var client = ldap.createClient({ url: ldapConfig.server});

// Interface pratique pour que Typescript comprenne ce qu'est un dictionnaire simple
interface dic {
    [Key: string]: string | string[];
}

//------------------------------------------------------------------------------------------------------------------------
// Fonctions de base agissant sur le LDAP
//------------------------------------------------------------------------------------------------------------------------

export class Basics {
    /**
     * @class Basics
     * @classdesc Cette classe est la brique de base du fichier tout entier puisqu'elle contient les functions qui agissent directement sur le LDAP.
     * @memberof LDAP
     * @summary Constructeur vide.
     */
    constructor() {}

    /**
     * @memberof LDAP
     * @summary Fonction qui sert à s'identifier sur le LDAP.
     * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès. Méthode ldapjs 
     * (voir [`Client API`](http://ldapjs.org/client.html) méthode bind).
     * @arg {string} dn - Nom de domaine ; identifiant de l'utilisateur cherchant à se connecter
     * @arg {string} password - Mot de passe de l'utilisateur cherchant à se connecter
     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
     * @static
     * @async
     */
    static async bind(dn: string, password: string) : Promise<boolean> {
        // Escape DN as everywhere in this file, but password is taken as is
        client.bind(dn, password, res => {
            // Gestion erreur
            try { res; }
            catch(err) {
                throw "Erreur lors de la connection au LDAP.";
            }
        });
        // End with a boolean
        return true;
    }

    /**
     * @memberof LDAP
     * @summary Fonction qui sert à s'identifier sur le LDAP avec plein pouvoirs.
     * @desc Appelle {@link bind} avec un utilisateur tout puissant.
     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
     * @static
     * @async
     */
    static async adminBind() : Promise<boolean> { return Basics.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password); }

    /**
     * @memberof LDAP
     * @summary Fonction qui sert à se déconnecter du LDAP.
     * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès.
     * Fait appel à {@link Basics.bind} avec deux champs vides.
     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
     * @static
     * @async
     */
    static async unbind() : Promise<boolean> { return Basics.bind("", ""); }
    

    /**
     * @callback entryHandler
     * @arg entry {*} - Convoluted ldap.js search result object
     */

    /**
     * @memberof LDAP
     * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et modifie une liste pour y insérer les valeurs trouvées.
     * @desc Cette fonction utilise ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode search). Cette fonction fait une demande au LDAP
     * qu'elle filtre selon un schéma prédéfini dans `filter` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end).
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {string[]} attributes - Attributs à renvoyer
     * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1)
     * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape
     * @arg {entryHandler} handler - Wrapper pour gérer les requêtes simples ou multiples
     * @return {void} Utilise handler pour gérer ses résultats au fur et à mesure
     * @static
     * @async
     */
    static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<void> {
        Basics.adminBind();
        let dn ="";
        if (id != null)     {
            if (domain == "group")  dn += ldapConfig.group.gid;
            else                    dn += ldapConfig.user.uid;
            dn += '=' + ldapEscape.dn("${txt}", { txt: id }) + ',';
        }
        dn+=ldapConfig.dn[domain];
        console.log("Searching dn= " + dn + ", filter : " + filter);
        // Interrogation LDAP selon filter
        let promise = new Promise<void>(function(resolve, reject) {
            client.search(dn, {            // Must be escaped in case of a malignious false id
                "scope": "sub",
                "filter": filter,      // Must be escaped in case of a malignious search arg
                "attributes": attributes
            }, (err, res) => {
                // Gestion erreur ; pb car pas simple true / autre en sortie
                if (err) {
                    throw "Erreur lors de la recherche sur le LDAP.";
                } else {
                    // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse
                    res.on('searchEntry', entry => handler(entry));
                    // Si la recherche renvoie une erreur, on renvoit
                    res.on('error', resErr => { throw resErr; });
                    // Quand la recherche est finie on se déconnecte
                    res.on('end', _ => { Basics.unbind(); resolve(); });
                }
            });
        });

        return promise;
    }
    
    /**
     * @memberof LDAP
     * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit une liste de valeurs trouvées.
     * @desc Cette fonction utilise {@link LDAP.search} directement.
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {string} attribute - Attribut unique à renvoyer
     * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1)
     * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape
     * @return {Promise(string[])} Résultats de la recherche ; soit une liste de valeurs d'attributs, 
     * soit une liste de dictionnaires si on veut plus d'un attribut (les clés du dictionnaire sont celles du LDAP)
     * @static
     * @async
     */
    static async searchSingle(domain: 'group'|'user', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> {
        let vals=[];
        await Basics.search(domain, [attribute], id, filter,  entry => {
            // Cas un seul attribut où le résultat est une liste directement
            console.log("searchSingle found " + entry.object[(domain == 'group' ? ldapConfig['group']['gid'] : ldapConfig['user']['uid'])]);
            vals.push(entry.object[attribute]);
        });
        return vals;
    }
    
    /**
     * @memberof LDAP
     * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit les valeurs trouvées.
     * @desc Cette fonction utilise ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode search). Cette fonction fait une demande au LDAP
     * qu'elle filtre selon un schéma prédéfini dans `filter` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end).
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {string[]} attributes - Liste des attributs qui figureront dans le résultat final ; peut aussi être un seul élément
     * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1)
     * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape
     * @return {Promise(Array<dic>)} Résultats de la recherche ; soit une liste de valeurs d'attributs, 
     * soit une liste de dictionnaires si on veut plus d'un attribut (les clés du dictionnaire sont celles du LDAP)
     * @static
     * @async
     */
    static async searchMultiple(domain: 'group'|'user', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> {
        let vals=[];
        await Basics.search(domain, attributes, id, filter,  entry => {
            // Cas plusieurs attributs donc résultat dictionnaire
            vals.push({});
            console.log("searchMultiple found " + entry.object[(domain == 'group' ? ldapConfig['group']['gid'] : ldapConfig['user']['uid'])]);
            attributes.forEach(attribute => {
                vals.slice(-1)[0][attribute]=entry.object[attribute];
            });
        });
        return vals;
    }

    /**
     * @memberof LDAP
     * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet.
     * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode modify).
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {string} id - Identifiant unique de la feuille à modifier ; passé par ldapEscape dans cette fonction
     * @arg {"add"|"del"|"replace"} op - Operation à réaliser sur le LDAP. Trois opération sont possibles ; "add", qui rajoute des attributs et qui peut créer des doublons,
     * "del" qui en supprime, et "replace" qui remplace du contenu par un autre. 
     * @arg {dic} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs.
     * @arg {string} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut.
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, `false` sinon.
     * @static
     * @async
     */
    static async change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> {
        Basics.adminBind();
        let dn ="";
        if (domain == 'group')  dn += ldapConfig.group.gid;
        else                    dn += ldapConfig.user.uid;
        dn+='='+ldapEscape.dn("${txt}", { txt: id })+','+ldapConfig.dn[domain];
        // Modification LDAP selon dn fourni en argument (pourrait prendre une liste de Changes)
        client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({
            operation: op,
            modification: mod,
        // Gestion erreur 
        }), err => { throw "Erreur lors d'une opération de modification sur le LDAP."; });
        Basics.unbind();
        return true;
    }

    /**
     * @memberof LDAP
     * @summary Fonction qui permet de rajouter un élément sur le LDAP.
     * @desc  Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode add).
     * On notera le rôle particulier de vals[uid/gid] qui sert à identifier la feuille à changer ; passé par ldapEscape dans cette fonction.
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {Object.<string, string>} vals - Dictionnaire contenant les valeurs à créer (contient un champ en ldapConfig)
     * @arg {Object} vals[key] - Nouvelle valeur pour le champ key
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon.
     * @static
     * @async
     */
    static async add(domain: 'group'|'user', vals) : Promise<boolean> {
        Basics.adminBind();
        let dn = "";
        if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.group.gid] });
        else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.user.uid] });
        dn += ldapConfig.dn[domain];
        // Ajout LDAP selon la ldapConfiguration en argument
        client.add(ldapEscape.dn("${txt}", { txt: dn }), vals, err => {
            throw "Erreur lors d'une opération d'ajout sur le LDAP.";
        });
        Basics.unbind();
        return true;
    }

    /**
     * @memberof LDAP
     * @summary Fonction qui permet de supprimer une feuille du LDAP.
     * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode del).
     * Elle est différente de modify avec "del" car elle affecte directement une feuille et pas un attribut.
     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
     * @arg {string} id - Identifiant unique de la cible, passé par ldapEscape dans cette fonction
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @static
     * @async
     */
    static async clear(domain: 'group'|'user', id: string) : Promise<boolean> {
        Basics.adminBind();
        let dn = "";
        if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: id });
        else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: id });
        dn+=ldapConfig.dn[domain];
        // Suppression LDAP
        client.del(ldapEscape.dn("${txt}", {txt: dn}), err => {
            throw "Erreur lors d'une opération de suppression sur le LDAP.";
        });
        Basics.unbind();
        return true;
    }
}