/**
 * @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 {ldapConfig} from './config';
import {LDAP} from './basics';
import {Tools} from './utilities';
import {Group} from './group';

/**
 * @interface userData
 * @desc Interface avec toutes les données extractables pour un utilisateur.
 * @var {string} uid - Identifiant utilisateur
 * @var {string} givenName - Prénom
 * @var {string} lastName - Nom
 * @var {string} nickname - Surnom
 * @var {string} photo - Bytestring de la photo de l'utilisateur
 * @var {string} birthdate - Date d'anniversaire
 * TBA @var {string} nationality - Nationalité d'origine
 * @var {string} promotion - Année(s) de promo
 * @var {string} phone - Numéro(s) de téléphone
 * @var {string[]} adresses - Adresse(s)
 * @var {string[]} mails - Adresse(s) courriel
 * @var {string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (inclus section sportive, binet, PA...)
 * @var {string} password - Mot de passe généré en amont
 * @var {string[]} ips - Adresse(s) ip
 * @var {string} directory - Adresse soft des données utilisateurs
 * @var {string} login - Astuce de root flemmard
 * @arg {string} readPerm - Permissions spéciales BR
 * @var {string} writePerm - Permissions spéciales BR
 * @var {string[]} forlifes - Alias BR (attention le filtre .fkz n'est plus fonctionnel)
 * @var {string[]} admins - Liste des gid dont l'utilisateur est admin ; supposé sous-liste de groups
 * TBA @var {string[]} likes - Liste des gid dont l'utilisateur est sympathisant
 */
export interface userData {
    "uid": string,
    "groups": string[],
    "groupsIsAdmin": string[],
    "password"?: string,
    "givenName"?: string,
    "lastName"?: string,
    "nickname"?: string,
    "promotion"?: string,
    "photo"?: string,
    "birthdate"?: string,
    //"nationality"?: string,
    "phone"?: string,
    "adress"?: string,
    "mail"?: string,
    "ips"?: string[],
    "directory"?: string,
    "login"?: string,
    "readPerm"?: string,
    "writePerm"?: string,
    "forlifes"?: string[]
    //"likes"?: string[]
}

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

export class User {
    /**
     * @class Cette classe est une des deux classes exportables permettant de faire des opérations sur les utilisateurs.
     * @summary Constructeur vide.
     */
    constructor() {}
     
    /**
     * @summary Fonction qui renvoit les infos de base relatives à un utilisateur particulier.
     * @desc Cette fonction utilise {@link Tools.genericPeek} avec l'interface {@link userData}.
     * @arg {string} uid - Identifiant de l'utilisateur
     * @return {Promise(userData)} Informations recueillies.
     * @static
     * @async
     */
    static async peek(uid: string) : Promise<userData> {
        try { 
            return Tools.genericPeek<userData>("us", uid);
        }
        catch(err) {
            throw "Error while peeking a user.";
        }
    }
    
    /**
     * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Utiliser {@link peek} au cas par cas après pour obtenir les vraies infos.
     * @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. Va crasher si un champ n'est pas dans ldapConfig.
     * Utiliser trouverGroupesParTypes pour chaque champ relié à groups.
     * @arg {userData} 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.
     * @return {Promise(string[])} gids des profils qui "match" les critères proposés.
     * @static
     * @async
     */
    static async search(data: userData) : Promise<string[]> {
        try {
            return Tools.genericSearch("us", data);
        }
        catch(err) {
            throw "Erreur lors de la recherche approximative d'un utilisateur.";
        }
    }
    
    /**
     * @summary Fonction qui créé un nouvel utilisateur dans le LDAP.
     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link User.addGroupMember} et {@link Admin.addGroupAdmin} pour gérer les groupes du nouvel utilisateur.
     * @arg {fullUserData} data - Dictionnaire des informations utilisateurs. Des erreurs peuvent apparaître si tous les champs ne sont pas remplis.
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async create(data: userData): Promise<boolean> {
        // Calcul d'un dictionnaire d'ajout
        let vals = {};

        // uid de base généré à partir de nom et prénom, plus potentiellement promo et un offset
        // MEF mélange de Promise et de fonction standard
        try {
            Tools.generateUid(data['givenName'],data['lastName'],data['promotion']).then(id => { vals[ldapConfig.key_id]=id; } );
        }
        catch(err) {
            throw "Erreur lors de la génération d'un hruid pour un nouvel utilisateur.";
        }

        let uid = vals[ldapConfig.key_id];

        // Génère une erreur si un champ n'est pas rempli
        for (let key_att in data) {
            // Ecriture de toutes les valeurs uniques
            if (!Array.isArray(data[key_att])) { vals[ldapConfig.user[key_att]]=data[key_att]; }
        }

        // Appel à la fonction de base
        if (!await LDAP.add("us", vals)) { throw "Erreur de l'ajout de la feuille à l'arbre utilisateur."; }
        
        for (let key_att in data) {
            // Modifications multiples pour avoir plusieurs champs de même type ; boucle sur les attributs multiples
            if (Array.isArray(data[key_att])) {
                // On rajoute chaque valeur en entrée
                data[key_att].forEach(val => {
                    let vals2 = {};
                    vals2[ldapConfig.user[key_att]]=val;
                    LDAP.change("us", uid, "add", vals2).then(res => {
                        if (!res) { throw "Erreur lors de l'ajout d'une valeur pour un champ à valeurs multiples à la feuille du nouvel utilisateur."; }
                    });
                });
            }
        }
        
        // Certains champs nécessitent de petits calculs
        let vals3={};

        // Création d'un nom complet lisible
        vals3[ldapConfig.user['fullName']]=data['givenName']+' '+data['lastName'].toUpperCase();

        // ldapConfiguration du mot de passe utilisateur
        // Le préfixe {CRYPT} signifie que le mdp est hashé dans OpenLDAP voir : https://www.openldap.org/doc/admin24/security.html 
        vals3[ldapConfig.user['password']] = "{CRYPT}"+data['password'];
        
        // Ecriture d'un surnom s'il y a lieu
        if ((data['nickname']!=undefined) && (data['nickname']!='')) {
            vals3[ldapConfig.user['nickname']]=data['nickname'];
        }
        try {
            // Génération id aléatoire unique
            vals3[ldapConfig.user['id']]= await Tools.generateId(ldapConfig.user['id'], "us");
        }
        catch(err) {
            throw "Erreur lors de la génération d'un id numérique pour un nouvel utilisateur.";
        }
        
        // Stockage machine ; dépend du prénom
        vals3[ldapConfig.user['directory']] = '/hosting/users/' + data['givenName'][0];

        // Code root
        vals3[ldapConfig.user['cleanFullName']]=data['fullName'].replace(':', ';').toLowerCase().normalize('UFD');
        
        // Adressage root
        if (data['groups'].includes("on_platal")) { vals3[ldapConfig.user['login']] = "/bin/bash"; }
        else  { vals3[ldapConfig.user['login']] = "/sbin/nologin"; }
        
        // Permissions BR
        vals3[ldapConfig.user['readPerm']] = 'br.*,public.*';
        if (data['readPerm'].length>0) { vals3[ldapConfig.user['readPerm']] += ',' + data['readPerm']; }
        vals3[ldapConfig.user['writePerm']] = 'br.*,!br.blague-du-jour,public.*,!br.campagnekes';
        if (data['writePerm'].length>0) { vals3[ldapConfig.user['readPerm']] += ',' + data['writePerm']; }

        // Valeur nécessaire ASKIP mais inutile
        vals3[ldapConfig.user['idNum']] ='5000';

        // Inscription des valeurs calculées
        if (!await LDAP.change("us", uid, "add", vals3)) {
            throw "Erreur lors de l'ajout des valeurs calculées à la feuille du nouvel utilisateur.";
        }

        ["posixAccount", "shadowAccount", "inetOrgPerson", "brAccount"].forEach(cst => {
            let val3={};
            vals3[ldapConfig.user['class']]=cst;
            LDAP.change("us", uid, "add", vals3).then(res => {
                if (!res) { throw "Erreur lors de l'ajout d'une valeur constante à la feuille du nouvel utilisateur."; }
            });
        });

        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
        data['groupsIsMember'].forEach(gid => {
            Group.addMember(uid, gid).then(res => {
                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe."; }
            });
        });
        data['groupsIsAdmin'].forEach(gid => { 
            Group.addAdmin(uid, gid).then(res => {
                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe en tant qu'admin."; }
            });
        });

        return true;
    }

    //------------------------------------------------------------------------------------------------------------------------
    // Fonctions de suppression TBT
    //------------------------------------------------------------------------------------------------------------------------
    
    /**
     * @summary Fonction qui supprime un utilisateur 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} uid - uid de la victime
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async delete(uid: string): Promise<boolean> {
        try {
            // Gestion des groupes d'abord
            let profil = await User.peek(uid);
            profil[ldapConfig.user['groups']].forEach(gid => {
                // Opérations effectuées par effet de bord
                if (Tools.isGroupAdmin(uid,gid)) {
                    if (!Group.remAdmin(uid, gid)) { throw "Erreur lors de la suppression des droits d'admin de l'utilisateur."; }
                }
                if (!Group.remMember(uid, gid)) { throw "Erreur lors de la suppression de l'appartenance à un groupe de l'utilisateur."; }
            });
        }
        catch(err) {
            throw "Erreur lors de l'obtention des informations de l'utilisateur à supprimer.";
        }
        // Elimination
        if (!LDAP.clear("us", uid)) { throw "Erreur lors de la suppression de l'utilisateur."; }
        return true;
    }

    /**
     * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur}
     * @desc Appelle simplement {@link genericEdit}.
     * @arg {userData} data - Dictionnaire des informations utilisateurs
     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
     * @async
     * @static
     */
    static async edit(data : userData) : Promise<boolean> {
        try {
            let uid = data["uid"];
            // Leave old groups
            let profil = User.peek(uid);
            profil["groups"].forEach(gid => { Group.remMember(uid, gid); });
            profil["groupsIsAdmin"].forEach(gid => { Group.remAdmin(uid, gid); });
            // Join new groups
            data["groups"].forEach(gid => { Group.addMember(uid, gid); });
            data["groupsIsAdmin"].forEach(gid => { Group.addAdmin(uid, gid); });
            // Edit all other fields
            return Tools.genericEdit("us",data);
        }
        catch(err) {
            throw "Erreur lors de la modification d'un utilisateur.";
        }
    }
}