/**
 * @file Ce fichier regroupe les différentes classes avec différents utilisateurs. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver.
 * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
 * @author hawkspar
 */

import LDAP from './basics.js';
import {SmartSearch, Tests} from './utilities.js';
import {Admin, SuperAdmin} from './admins.js';
// Essentiels pour le fichier de config
import path from 'path';
import fs from 'fs';

// Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement
var configPath = path.resolve('./','ldap_config.json');
var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

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

export class Open {
    /**
     * @class Cette classe est la classe exportable de base permettant à un utilisateur non connecté de faire des petites recherches simples.
     * @summary Constructeur vide.
    */
    constructor() {}

    //------------------------------------------------------------------------------------------------------------------------
    // Fonctions de lecture
    //------------------------------------------------------------------------------------------------------------------------
    /**
     * @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) {
        try {
            return LDAP.search(config.key_id+uid+config.dn_users, config.user.groups)[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 config.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) {
        try {
            return LDAP.search(config.key_id+gid+config.dn_users, config.group.member)[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 config.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) {
        try {
            return LDAP.search(config.key_id+gid+config.dn_users, config.group.admin)[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, gid) {
        try {
            let lg = await this.getGroups(uid);
            let lm = await this.getMembers(gid);
            if (lg.includes(gid) && lm.includes(uid)) {
                return true;
            }
        }
        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, gid) {
        try {
            let la = await this.getAdmins(gid);
            if (la.includes(uid)) {
                return true;
            }
        }
        catch(err) {
            throw "Erreur lors du test d'appartenance au bureau d'administration un groupe.";
        }
    }
     
    /**
     * @summary Fonction qui renvoit toutes les infos relatives à un utilisateur particulier.
     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
     * @arg {string} uid - Identifiant de l'utilisateur
     * @return {Promise(Object[])} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet de l'utilisateur ;
     * voir `ldap_config.json`(..\..\ldap_config.json) pour les clés exactes.
     * @static
     */
    static peekUser(uid) {
        try {
            return LDAP.search(config.key_id+uid+config.dn_users, config.user.profil);
        }
        catch(err) {
            throw "Erreur lors d'une recherche d'informations sur un individu.";
        }
    }
     
    /**
     * @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(Object[])} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ;
     * voir `ldap_config.json`(..\..\ldap_config.json) pour les clés exactes.
     * @static
     */
    static peekGroup(gid) {
        try {
            return LDAP.search(config.key_id+gid+config.dn_groups, config.group.profil);
        }
        catch(err) {
            throw "Erreur lors d'une recherche d'informations sur un groupe.";
        }
    }

    //------------------------------------------------------------------------------------------------------------------------
    // Fonctions de recherche
    //------------------------------------------------------------------------------------------------------------------------

    /**
     * @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 SmartSearch.groups}.
     * @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 findGroups(input) {
        try {
            // Trucs intelligents faits dans ./utilities
            return SmartSearch.groups(input, [config.key_id, config.group.type]);
        }
        catch(err) {
            throw "Erreur lors de la recherche approximative d'un groupe.";
        }
        /**
        let gidtyList = [];
        gList.forEach(g => {
            // Si le groupe est du bon type on rajoute son gid
            if (g[config.group.type]==type) { gidtyList.push(g[config.key_id]); }
        });
        return gidtyList;
        */
    }

    /**
     * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Autre étape vers vrai TOL (Trombino On Line). Doit être préféré à repliquerTOL
     * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos).
     * @desc Cette fonction utilise {@link SmartSearch.users}.
     * @arg {Object} data - Dictionnaire contenant les données nécessaires à {@link SmartSearch.groups}
     * @return {Promise(string[])} gids des profils qui "match" les critères proposés.
     * @static @async
     */
    static async findUsers(data) {
        try {
            return SmartSearch.users(data, config.key_id);
        }
        catch(err) {
            throw "Erreur lors de la recherche approximative d'un utilisateur.";
        }
    }
}

export class User extends Open {
    /**
     * @class Cette classe est la classe de l'utilisateur connecté qui peut déjà créer un groupe et changer son profil.
     * Techniquement, c'est la première classe qui a vraiment besoin de méthodes dynamiques dans l'arborescence, puisque c'est à partir du niveau User
     * qu'on peut commencer à vouloir tracer les actions de l'utilisateur. 
     * @summary Ce constructeur appelle simplement {@link LDAP.bind}.
     * @arg {Object} user - Utilisateur de la forme nécessaire à {@link LDAP.bind}.
     * @async
    */
    constructor(user) {
        super();
        try {
            LDAP.bind(user);
        }
        catch(err) {
            throw "Erreur lors de la connexion à un compte utilisateur.";
        }
    }

    //------------------------------------------------------------------------------------------------------------------------
    // Fonction de création TBT
    //------------------------------------------------------------------------------------------------------------------------

    /**
     * @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 {Object} data - Dictionnaire des informations utilisateurs (voir détail des champs dans config.json)
     * @arg {string} data[name] - Nom du groupe
     * @arg {string} data[ns] - Statut du groupe ; 'binet' ou 'free', càd ouvert à tous
     * @arg {string[]} data[members] - Liste des membres du groupe
     * @arg {string[]} data[admins] - Liste des admins du groupe ; supposée être une sous-liste de la précédente
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     */
    async addGroup(data) {
        // Calcul d'un dictionnaire d'ajout
        let vals = {};

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

        // Ecriture de toutes les valeurs directement inscrites dans le LDAP (in pour input)
        config.group.direct_input.forEach(key_att => vals[config.group[key_att]]=data[key_att]);

        // Appel à la fonction de base
        if (!await LDAP.add(config.key_id+"="+vals[config.group['name']]+","+config.dn_groups, 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={};

        // Sauvegarde du nom (pour le cas où gid != data['name'])
        vals2[config.group["name"]]=data['name'];

        // ?!
        vals2[config.user['password']] = '';

        // Génération id aléatoire et test contre le LDAP
        try {
            Tests.generateId(config.groups["idNumber"], config.dn_groups).then(id => { vals2[config.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[config.group['idNumber2']]=vals2[config.group['idNumber']];
        
        // Stockage machine ; dépend du prénom
        vals2[config.group['directory']] = '/hosting/groups/'+vals[config.key_id];

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

        // Inscription des valeurs calculées par effet de bord
        if (!await LDAP.change(config.key_id+"="+vals[config.key_id]+","+config.dn_groups, "add", vals2)) {
            throw "Erreur lors de l'ajout des valeurs intelligentes du nouveau groupe.";
        }

        ["posixAccount", "posixGroup", "brAccount"].forEach(cst => {
            let vals3={};
            vals3[config.group['class']]=cst;
            LDAP.change(config.key_id+"="+vals[config.key_id]+","+config.dn_groups, "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 => {
            Admin.addGroupMember(uid, vals[config.key_att]).then(res => {
                if (!res) { throw "Erreur de l'ajout d'un membre au groupe."; }
            });
        });
        data['admins'].forEach(uid => {
            Admin.addGroupAdmin( uid, vals[config.key_att]).then(res => {
                if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; }
            });
        });

        return true;
    }

    //------------------------------------------------------------------------------------------------------------------------
    // Fonctions d'édition TBT
    //------------------------------------------------------------------------------------------------------------------------
    
    /**
     * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur}
     * @desc Appelle simplement {@link creerUtilisateur} et {@link supprimerUtilisateur} en godmode, plus {@link renseignerSurUtilisateur} pour les champs non fournis.
     * Ce choix a pour conséquence que l'ordre du dictionnaire de correspondance dans ldap_config est important. Une version "nerfée" de cette fonction est envisageable ; elle donne bcp de pouvoir à l'utilisateur.
     * @arg {string} uid - Utilisateur à modifier (le plus souvent le même, mais root possible)
     * @arg {Object} data - Dictionnaire des informations utilisateurs au même format que pour {@link creerUtilisateur} avec tous les champs optionnels ;
     * Attention toutes les clés de cette entrée seront modifiées dans le LDAP ; les nouveaux résultats écrasant les précédents, sauf 'readPerm','writePerm','forlifes','ips','groups' et 'groupsIsAdmin'
     * qui sont censurés pour cette fonction)
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     */
    async editUser(uid, data) {
        // Récupération des anciennes données
        let profil = await this.peekUser(uid);
        // Reecriture de profil avec les bons champs
        Object.keys(profil).forEach(keyLDAP => {
            Object.keys(config.user).forEach(keyAlias => {
                config.user[keyAlias]=keyLDAP;
                profil[keyAlias]=profil[keyLDAP];
            });
        });
        // Régénération du champ manquant dans profil
        try {
            let lg = await this.getGroups(uid);
            profil['groupsIsAdmin']=[];
            lg.forEach(gid => {
                this.isGroupAdmin(uid, gid).then(res => {
                    if (res) { profil['groupsIsAdmin'].push(gid); }
                });
            });
            // Surcharge des champs à modifier selon data
            Object.keys(data).forEach(key => {
                // Some fields the user cannot change (groups and groupsIsAdmin must be changed through addGroupMember and addGroupAdmin in Admin)
                if (!['readPerm','writePerm','forlifes','ips','groups','groupsIsAdmin'].includes(key)) { profil[key]=data[key]; }
            });
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // TBM : rajouter god passwd. Moche mais bonne façon de faire
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // Passage en godmode
            var god = SuperAdmin({"uid":"", "password":""});
            // Modification propre par effet de bord
            if (!(await god.delUser(uid) && await god.createUser(profil))) {
                throw "Erreur dans la destruction/création du compte.";
            } else {
                return true;
            }
        }
        catch(err) {
            throw "Erreur lors de la modification des groupes où un utilisateur est admin.";
        }
    }
    
    destr() { LDAP.unbind(); }
}