/**
 * @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.
 * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
 * @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';

// Important ; permet de vérifier que l'utilisateur reste connecté.
//var ensureLoggedin =  require('connect-ensure-login').ensureLoggedIn; //hawkspar->manifold : est-ce encore utile ? je ne crois pas

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

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

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

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

    /**
     * @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 LDAP.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password); }

    /**
     * @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 LDAP.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 LDAP.bind("", ""); }

    /**
     * @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.<Object>)|Promise(Array.Object.<string, Object>))} 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 search(domain: 'gr'|'us', attributes: string[], id=null, filter="(objectClass=*)") : Promise<Array<any>> {
        LDAP.adminBind();
        if (domain == "gr") { var dn = ldapConfig.dn_groups; }
        else                { var dn = ldapConfig.dn_users; }
        if (id != null) { dn=ldapConfig.key_id+'='+id+','+dn; }
        let vals=[];
        // Interrogation LDAP selon ldapConfiguration fournie en argument
        client.search(ldapEscape.dn("${txt}", { txt: dn}), {
            "scope": "sub",
            "filter": ldapEscape.filter("${txt}", { txt: filter}),
            "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 => {
                    // Cas un seul attribut où le résultat est une liste directement
                    if (!Array.isArray(attributes)) {  vals.push(entry.object[attributes]); }
                    else if (attributes.length == 1) { vals.push(entry.object[attributes[0]]); }
                    // Cas plusieurs attributs donc résultat dictionnaire
                    else {
                        vals.push({});
                        attributes.forEach(attribute => {
                            vals.slice(-1)[0][attribute]=entry.object[attribute];
                        });
                    }
                });
                // Si la recherche renvoie une erreur, on renvoit
                res.on('error', resErr => { throw resErr; });
                // Si la recherche est finie on se déconnecte
                res.on('end', _ => { LDAP.unbind(); });
            }
        });
        // On renvoit le résultat
        return vals;
    }

    //TBT
    /**
     * @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
     * @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 {Object.<string, string>} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs.
     * @arg {Object} 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: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod) : Promise<boolean> {
        LDAP.adminBind();
        let dn = ldapConfig.key_id+'='+id+','
        if (domain == "gr") { dn+=ldapConfig.dn_groups }
        else                { dn+=ldapConfig.dn_users }
        // Modification LDAP selon ldapConfiguration 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.";
        });
        LDAP.unbind();
        return true;
    }

    //TBT
    /**
     * @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).
     * @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.key_id)
     * @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: 'gr'|'us', vals) : Promise<boolean> {
        LDAP.adminBind();
        if (domain == "gr") { var dn = ldapConfig.dn_groups }
        else                { var dn = ldapConfig.dn_users }
        // Ajout LDAP selon la ldapConfiguration en argument
        client.add(ldapEscape.dn(ldapConfig.key_id+"="+vals[ldapConfig.key_id]+",${txt}", { txt: dn}), vals, err => {
            throw "Erreur lors d'une opération d'ajout sur le LDAP.";
        });
        LDAP.unbind();
        return true;
    }

    //TBT
    /**
     * @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
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @static
     * @async
     */
    static async clear(domain: 'gr'|'us', id: string) : Promise<boolean> {
        LDAP.adminBind();
        let dn = ldapConfig.key_id+'='+id+','
        if (domain == "gr") { dn+=ldapConfig.dn_groups }
        else                { dn+=ldapConfig.dn_users }
        // Suppression LDAP
        client.del(ldapEscape.dn("${txt}", {txt: dn}), err => {
            throw "Erreur lors d'une opération de suppression sur le LDAP.";
        });
        LDAP.unbind();
        return true;
    }
}