/**
 * @file Ce fichier contient la classe de l'API du LDAP qui gère les opérations sur les groupes.
 * @author hawkspar
 */

import { ldapConfig } from './config';
import {LDAP} from './basics';
import {Tests} from './utilities';

/**
 * @interface groupData
 * @var {string} gid - Identifiant du groupe
 * @var {string} name - Nom du groupe
 * @var {string} type - Statut du groupe ; binet, section sportive... (actuellement juste 'binet' ou 'free')
 * @var {string[]} members - Liste des membres du groupe
 * @var {string[]} admins - Liste des admins du groupe ; supposée être une sous-liste de la précédente
 * @var {string} description - Description du groupe (facultatif)
 */
export interface groupData {
    "gid": string,
	"name": string,
	"type": string,
    "members": string[],
    "admins": string[],
    "description"?: string
}

//------------------------------------------------------------------------------------------------------------------------
// Classes à exporter TBT
//------------------------------------------------------------------------------------------------------------------------

export class Group {
    /**
     * @class Cette classe est une des deux classes exportables permettant de faire des opérations sur les groupes.
     * @summary Constructeur vide.
    */
    constructor() {}
     
    /**
     * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier.
     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
     * @arg {string} gid - Identifiant du groupe
     * @return {Promise(groupData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ;
     * voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes.
     * @static
     * @async
     */
    static async peek(gid: string) : Promise<groupData> {
        try {
            let fields = [];
            fields.push(ldapConfig.group.values());
            let LDAPGroupData = await LDAP.search("gr", fields, gid);
            let cleanGroupData : groupData;
            // Rename output
            for (let uncleanKey in LDAPGroupData) {
                for (let cleanKey in cleanGroupData) {
                    if (uncleanKey==ldapConfig.group[cleanKey]) { cleanGroupData[cleanKey] = LDAPGroupData[uncleanKey]; }
                }
            }
            return cleanGroupData;
        }
        catch(err) {
            throw "Erreur lors d'une recherche d'informations sur un groupe.";
        }
    }

    /**
     * @summary Fonction qui retrouve le groupe qui ressemblent à l'input et qui correspond au type fourni. 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.
     * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe.
     * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input.
     * @static
     * @async
    */
    static async search(input: string) : Promise<string[]> {
        try {
            // 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 
            return LDAP.search("gr", [ldapConfig.key_id], null, filter);
        }
        catch(err) {
            throw "Erreur lors de la recherche approximative d'un groupe.";
        }
    }

    static async addMember(uid: string, gid: string) : Promise<boolean> {
        try {
            // Vérifie que l'utilisateur est pas déjà membre pour groupes
            let lm = await Tests.getMembers(gid);
            if (!lm.includes(uid)) {
                let vals = {};
                vals[ldapConfig.group.members] = uid;
                // Erreur si pb lors de la modification
                if (!await LDAP.change("gr", gid, "add", vals)) {
                    throw "Erreur lors de la modification dans l'arbre des groupes pour ajouter un membre.";
                }
            }
        }
        catch(err) {
            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
        }
        try {
            // Vérifie que l'utilisateur est pas déjà membre pour users
            let lg = await Tests.getGroups(uid);
            if (!lg.includes(gid)) {
                let vals2 = {};
                vals2[ldapConfig.user.groups] = gid;
                // Erreur si pb lors de la modification
                if (!await LDAP.change("us", uid, "add", vals2)) {
                    throw "Erreur lors de la modification dans l'arbre des utilisateurs pour ajouter un membre.";
                }
            }
            return true;
        }
        catch(err) {
            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
        }
    }

    /**
     * @summary Fonction qui permet de supprimer un membre existant d'un groupe.
     * @desc Cette fonction fait essentiellement appel à {@link LDAP.search}, {@link LDAP.change}, {@link Open.getGroups} et {@link Open.getMembers}.
     * @arg {string} uid - Identifiant de l'ex-membre
     * @arg {string} gid - Identifiant du groupe
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async delMember(uid: string, gid: string): Promise<boolean> {
        try {
            // Vérifie que l'utilisateur est pas déjà viré pour groupes
            let lm = await Tests.getMembers(gid);
            if (lm.includes(uid)) {
                // Supprime tous les utilisateurs
                if (!await LDAP.change("gr", gid, "del", ldapConfig.group.members)) {
                    throw "Erreur lors de la suppression de tous les membres du groupe.";
                }
                // Les rajoute un par un, sauf pour le supprimé
                lm.forEach(id => {
                    if (id!=uid) {
                        this.addMember(id, gid).then(res => {
                            if (!res) { throw "Erreur lors du ré-ajout des autres membres"; }
                        });
                    }
                });
            }
        }
        catch(err) {
            throw "Erreur pour obtenir une liste de membres d'un groupe pour supprimer un membre du groupe.";
        }
        try {
            let lg = await Tests.getGroups(uid);
            // Vérifie que l'utilisateur est pas déjà viré pour users
            if (lg.includes(gid)) {
                // Supprime tous les groupes
                if (!await LDAP.change("us", uid, "del", ldapConfig.user.groups)) {
                    throw "Erreur lors de la suppression de tous les groupes du membre.";
                }
                // Les rajoute un par un, sauf pour le supprimé
                lg.forEach(id => {
                    if (id!=gid) {
                        this.addMember(uid, id).then(res => {
                            if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; }
                        });
                    }
                });
            }
            return true;
        }
        catch(err) {
            throw "Erreur pour obtenir une liste de groupes d'un membres pour le supprimer du groupe.";
        }
    }

    /**
     * @summary Fonction qui permet de promouvoir membre au stade d'administrateur d'un groupe.
     * @desc Cette fonction fait essentiellement appel à {@link Admin.addGroupMember} {@link LDAP.change} et {@link Open.getAdmins}. Elle n'autorise pas
     * les doublons et opère dans les deux dns users et groups.
     * @arg {string} uid - Identifiant du futur membre
     * @arg {string} gid - Identifiant du groupe
     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async addAdmin(uid: string, gid: string): Promise<boolean> {
        // Ajoute le membre au groupe avant d'en faire un admin
        if (!await Group.addMember(uid,gid)) { throw "Erreur lors de l'ajout du futur admin en tant que membre."; }
        try {
            let la = await Tests.getAdmins(gid);
            if (!la.includes(uid)) {
                // Finalement modification, uniquement dans groups
                let vals = {};
                vals[ldapConfig.group.admins] = uid;
                if (!await LDAP.change("gr", gid, "add", vals)) {
                    throw "Erreur lors de l'ajout de l'admin dans l'arbre des groupes.";
                }
            }
            return true;
        }
        catch(err) {
            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
        }
    }

    /**
     * @summary Fonction qui permet de rétrograder un membre du stade d'administrateur d'un groupe au stade d'utilisateur.
     * @desc Cette fonction fait essentiellement appel à {@link LDAP.change}.
     * @arg {string} uid - Identifiant du futur membre
     * @arg {string} gid - Identifiant du groupe
     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async delAdmin(uid: string, gid: string): Promise<boolean> {
        // Peut paraître absurde mais permet de s'assurer que le membre est bien présent et que ses champs sont comme il faut
        if (!(await Group.delMember(uid, gid) && Group.addMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission du futur admin."; }
        try {
            // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember)
            let la = await Tests.getAdmins(gid);
            if (la.includes(uid)) {
                // Supprime tous les administrateurs
                if (!await LDAP.change("gr", gid, "del", ldapConfig.group.admins)) { throw "Erreur dans la suppression de tous les admins pour en supprimer un."; }
                // Les rajoute un par un, sauf pour le supprimé
                la.forEach(id => {
                    if (id!=uid) { Group.addAdmin(id, gid).then(res => {
                        if (!res) { throw "Erreur dans le réajout d'un des autres admins."; }
                    }); }
                });
            }
            return true;
        }
        catch(err) {
            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
        }
    }
    /**
     * @summary Fonction qui créé un nouveau groupe dans le LDAP.
     * @desc Cette fonction fait une utilisation massive d'eval pour anonymiser son code ; c'est mal et cela suppose que beaucoup de soins ont été pris lors de
     * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Admin.addMemberGroup} et {@link Admin.addAdminGroup}
     * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans.
     * @arg {groupData} data - Dictionnaire des informations utilisateurs (voir détail des champs dans ldapConfig.json)
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async create(data: groupData) : Promise<boolean> {
        // Calcul d'un dictionnaire d'ajout
        let vals = {};

        // gid de base généré à partir du nom standardisé, pas à partir de l'entrée 'gid' !
        try {
            Tests.generateReadableId(data['name']).then(id => {
                vals[ldapConfig.key_id]=id;
                vals[ldapConfig.group['name']]=id;
            });
        }
        catch(err) {
            throw "Erreur lors de la génération d'un hruid pour créer un nouveau groupe.";
        }

        let gid : string = vals[ldapConfig.key_id];

        // Ecriture de toutes les valeurs directement inscrites dans le LDAP
        for (let key_att in data) { vals[ldapConfig.group[key_att]]=data[key_att] };

        // Appel à la fonction de base
        if (!await LDAP.add("gr", vals)) {
            throw "Erreur lors de la création d'une nouvelle feuille dans l'arbre des groupes.";
        }
        // Certains champs nécessitent de petits calculs
        let vals2={};

        // Encore un cahmp redondant
        vals2[ldapConfig.group['adress']] = gid;

        // ?!
        vals2[ldapConfig.group['password']] = '';

        // Génération id aléatoire et test contre le LDAP
        try {
            Tests.generateId(ldapConfig.group["idNumber"], "gr").then(id => { vals2[ldapConfig.group['idNumber']]=id; });
        }
        catch(err) {
            throw "Erreur lors de la génération d'un id numérique pour créer un nouveau groupe.";
        }
        // FOIREUX : Hypothèse sur la structure du reste des données mais évite un test.assurerUnicite à deux variables
        vals2[ldapConfig.group['idNumber2']]=vals2[ldapConfig.group['idNumber']];
        
        // Stockage machine ; dépend du prénom
        vals2[ldapConfig.group['directory']] = '/hosting/groups/'+gid;

        // Code root
        vals2[ldapConfig.group['cleanFullName']]=data['name'].replace(':', ';').toLowerCase().normalize('UFD');
        
        // Adressage root
        vals2[ldapConfig.group['login']] = "/sbin/nologin";
        
        // Permissions BR
        vals2[ldapConfig.group['readPerm']] = '!*';
        vals2[ldapConfig.group['writePerm']] = '!*';

        // Inscription des valeurs calculées par effet de bord
        if (!await LDAP.change("gr", gid, "add", vals2)) {
            throw "Erreur lors de l'ajout des valeurs intelligentes du nouveau groupe.";
        }

        ["posixAccount", "posixGroup", "brAccount"].forEach(cst => {
            let vals3={};
            vals3[ldapConfig.group['classes']]=cst;
            LDAP.change("gr", gid, "add", vals3).then(res => {
                if (!res) { throw "Erreur lors de l'ajout des valeurs constantes du nouveau groupe."; }
            });
        });

        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
        data['members'].forEach(uid => {
            Group.addMember(uid, gid).then(res => {
                if (!res) { throw "Erreur de l'ajout d'un membre au groupe."; }
            });
        });
        data['admins'].forEach(uid => {
            Group.addAdmin(uid, gid).then(res => {
                if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; }
            });
        });

        return true;
    }

    /**
     * @summary Fonction qui supprime un groupe du LDAP.
     * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement.
     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant.
     * @arg {string} gid - gid de la victime
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async delete(gid): Promise<boolean> {
        try {
            // Gestion des membres et administrateurs d'abord
            let profil = await Group.peek(gid);
            // Ordre important
            profil[ldapConfig.group['admin']].forEach( id => {
                Group.delAdmin( id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un admin d'un groupe en cours de suppression."; } });
            });
            profil[ldapConfig.group['member']].forEach(id => {
                Group.delMember(id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un membre."; } });
            });
            // Elimination
            if (!await LDAP.clear("gr",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; }
            return true;
        }
        catch(err) {
            throw "Erreur lors de l'obtention du profil d'un groupe pour le supprimer.";
        }
    }

    /**
     * @summary Fonction qui édite un groupe existant dans le LDAP. Très similaire à {@link User.addGroup}
     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Admin.addGroupMember} et {@link Admin.addGroupAdmin} en godmode pour gérer les groupes du nouvel utilisateur.
     * @arg {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 edit(data: groupData) : Promise<boolean> {
        try {
            let gid = data['gid'];
            // Récupération des anciennes données
            let profil = await Group.peek(gid);
            // Surcharge des champs à modifier selon data
            for (let key in data) { profil[key]=data[key]; }
            // Modification propre
            if (!await Group.delete(gid) && await Group.create(profil)) { throw "Erreur de la destruction/recréation du groupe pour le modifier."; }
            return true;
        }
        catch(err) {
            throw "Erreur lors de l'obtention du profil d'un groupe pour le modifier.";
        }
    }
}