import cockpit from 'cockpit';
import React from 'react';
import {
    Label,
} from '@patternfly/react-core';
import {
    InfoCircleIcon,
} from '@patternfly/react-icons';
import {
    getTimeLimit,
    getSizeLimit
} from './options.jsx';
import {
    LDAP_PING_TIME_LIMIT,
    LDIF_MAX_CHAR_PER_LINE
} from './constants.jsx';
import { log_cmd } from "../../tools.jsx";

export function generateUniqueId () {
    return Math.random().toString(36)
            .substring(2, 15);
}

const _ = cockpit.gettext;

function getModDate (modDate, format) {
    if (!modDate) {
        // Some entries ( for instance "cn=plugins,cn=config" )
        // don't have the modifyTimestamp attribute present.
        return '';
    }
    const FORMAT_OPTIONS = {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: true,
    };
    const y = modDate.substring(0, 4);
    const m = modDate.substring(4, 6);
    const d = modDate.substring(6, 8);
    const h = modDate.substring(8, 10);
    const min = modDate.substring(10, 12);
    const sec = modDate.substring(12, 14);
    const value = `${y}-${m}-${d}T${h}:${min}:${sec}Z`;
    const date = new Date(value);

    // Apply the same format, just change the timezone
    if (format === "local") {
        return date.toLocaleString('en-US', FORMAT_OPTIONS);
    } else {
        return date.toLocaleString('en-US', {
            ...FORMAT_OPTIONS,
            timeZone: 'UTC'
        });
    }
}

export function getUserSuffixes (serverId, suffixCallback) {
    const suffixCmd = [
        'dsconf',
        '-j',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        'backend',
        'suffix',
        'list',
        '--suffix'
    ];
    log_cmd("getUserSuffixes", "list suffixes", suffixCmd);
    cockpit
            .spawn(suffixCmd, { superuser: true, err: "message" })
            .done(content => {
                const suffList = JSON.parse(content);
                suffixCallback(suffList.items);
            })
            .fail(_ => {
                console.log('FAIL ' + _.toString());
            });
}

export function ldapPing (serverId, pingCallback) {
    const cmd = [
        'ldapsearch',
        '-LLL',
        '-Y',
        'EXTERNAL',
        '-b',
        'cn=config', // params.baseDn,
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        '-s',
        'base',
        '-l',
        LDAP_PING_TIME_LIMIT,
        '1.1'
    ];

    log_cmd("ldapPing", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(() => {
                pingCallback(true);
            })
            .fail(err => {
                console.log('FAIL err.exit_status ==> ' + err.exit_status);
                console.log('FAIL err.message ==> ' + err.message);
                pingCallback(false,
                             {
                                 errCode: err.exit_status,
                                 errMsg: err.message
                             });
            });
}

export function getRootSuffixEntryDetails (params, entryDetailsCallback) {
    // This is a base scope search. No need for a size limit.
    const timeLimit = getTimeLimit();

    const cmd = [
        'ldapsearch',
        '-LLL',
        '-o',
        'ldif-wrap=no',
        '-Y',
        'EXTERNAL',
        '-b',
        params.baseDn,
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + params.serverId + '.socket',
        '-s',
        'base',
        '-l',
        timeLimit,
        '*',
        'numSubordinates',
        'modifyTimestamp'
    // 'entryId' // To uniquely identify the node in the treee view ==> <instanceId>_<entryId>
    // Doesn't work!! Some entries don't have an entryID (cn=config and its children for instance).
    ];

    log_cmd("getRootSuffixEntryDetails", "", cmd);

    let dn = null; // The Root DSE DN is an empty one.
    let numSubordinates = '';
    let modifyTimestamp = '';
    // let entryId = '';
    let result = {};
    const entryArray = []; // Will contain the entry but the numSubordinates and modifyTimestamp.
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                // console.log('typeof data = ' + typeof data);
                // console.log('SUCCESS ' + data);
                const lines = data.split('\n');
                lines.map(currentLine => {
                    if (isAttributeLine(currentLine, 'dn:')) {
                        entryArray.push(splitAttributeValue(currentLine));
                        // console.log('currentLine = ' + currentLine);
                        // console.log('currentLine.split()[0]  = ' + currentLine.split(':')[0]);
                        // console.log('currentLine.split()[1]  = ' + currentLine.split(':')[1]);
                        // Convert base64-encoded DNs

                        const pos = currentLine.indexOf(':');
                        if (currentLine.startsWith('dn::')) {
                            dn = b64DecodeUnicode(currentLine.substring(pos + 2).trim());
                        } else {
                            dn = currentLine.substring(pos + 1).trim();
                        }
                        // console.log('The DN is ' + dn);
                    } else if (isAttributeLine(currentLine, 'numSubordinates:')) {
                        numSubordinates = (currentLine.split(':')[1]).trim();
                        // console.log('numSubordinates = ' + numSubordinates);
                    } else if (isAttributeLine(currentLine, 'modifyTimestamp:')) {
                        modifyTimestamp = (currentLine.split(':')[1]).trim();
                    } else {
                        entryArray.push(splitAttributeValue(currentLine));
                    }
                    return [];
                });
                result = JSON.stringify(
                    {
                        dn,
                        numSubordinates,
                        modifyTimestamp: getModDate(modifyTimestamp, "utc"),
                        modifyTimestampLocal: getModDate(modifyTimestamp, "local"),
                        parentId: params.parentId,
                        fullEntry: entryArray,
                        errorCode: 0
                    });
            })
            .fail(err => {
                // console.log('FAIL err.exit_status ==> ' + err.exit_status);
                // console.log('FAIL err.message ==> ' + err.message);
                result = JSON.stringify(
                    {
                        dn: params.baseDn,
                        numSubordinates: '???',
                        modifyTimestamp: '???',
                        // entryId: (Math.floor(Math.random() * Math.floor(1000000))).toString(), // Choose a random number between 0 and 1 million.
                        parentId: params.parentId,
                        fullEntry: [{ attribute: '???: ', value: '???' }],
                        errorCode: err.exit_status
                    });
            })
            .always(() => entryDetailsCallback(result));
}

function getResourceLimits () {
    const timeLimit = getTimeLimit();
    const sizeLimit = getSizeLimit();
    const limits = [];
    if (timeLimit > 0) {
        limits.push('-l', timeLimit);
    }
    if (sizeLimit > 0) {
        limits.push('-z', sizeLimit);
    }

    return limits;
}

export function getSearchEntries (params, resultCallback) {
    /*
     params.serverId,
     params.searchBase
     params.searchFilter
     params.searchScope
     params.sizeLimit,
     params.timeLimit
  */
    const cmd = [
        'ldapsearch',
        '-LLL',
        '-o',
        'ldif-wrap=no',
        '-Y',
        'EXTERNAL',
        '-b',
        params.searchBase,
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + params.serverId + '.socket',
        '-s',
        params.searchScope,
        '-l',
        params.timeLimit,
        '-z',
        params.sizeLimit,
        params.searchFilter,
        '*',
        '+'
    ];

    log_cmd("getSearchEntries", "", cmd);
    let dn = '';
    let numSubordinates = '0';
    let modifyTimestamp = '';
    let searchResult = null;
    const allEntries = [];
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' }) // string.split("\n\r")
            .done(data => {
                searchResult = data;
            })
            .catch((err, data) => {
                if (err.exit_status === 4) {
                    console.log('Size limit hit'); // Use the partial data.
                    searchResult = data;
                    params.addNotification(
                        "info",
                        cockpit.format(_("Size limit of $0 was exceeded. The child entries of $1 have been truncated."), params.sizeLimit, params.searchBase)
                    );
                } else {
                    searchResult = null;
                    resultCallback(null, { status: err.exit_status, msg: err.message });
                }
            })
            .finally(() => {
                if (searchResult === null) {
                    return;
                }
                const lines = searchResult.split('\n');
                let ldapsubentry = false;
                let isRole = false;
                let isLockable = false;
                lines.map(currentLine => {
                    const accountObjectclasses = ['nsaccount', 'nsperson', 'simplesecurityobject',
                        'organization', 'person', 'account', 'organizationalunit',
                        'netscapeserver', 'domain', 'posixaccount', 'shadowaccount',
                        'posixgroup', 'mailrecipient', 'nsroledefinition'];
                    if (isAttributeLine(currentLine, 'dn:')) {
                        // Convert base64-encoded DNs
                        const pos = currentLine.indexOf(':');
                        if (currentLine.startsWith('dn::')) {
                            dn = b64DecodeUnicode(currentLine.substring(pos + 2).trim());
                        } else {
                            dn = currentLine.substring(pos + 1).trim();
                        }
                        ldapsubentry = false;
                        isRole = false;
                        isLockable = false;
                    } else if (isAttributeLine(currentLine, 'numSubordinates:')) {
                        numSubordinates = (currentLine.split(':')[1]).trim();
                    } else if (isAttributeLine(currentLine, 'modifyTimestamp:')) {
                        modifyTimestamp = (currentLine.split(':')[1]).trim();
                    } else if (isAttributeLine(currentLine, 'objectclass: ldapsubentry')) {
                        ldapsubentry = true;
                    } else if (isAttributeLine(currentLine, 'objectclass: nsroledefinition')) {
                        isRole = true;
                    }
                    for (const accountOC of accountObjectclasses) {
                        if (isAttributeLine(currentLine, `objectclass: ${accountOC}`)) {
                            isLockable = true;
                        }
                    }

                    if (currentLine === '' && dn !== '') {
                        const result = JSON.stringify(
                            {
                                dn,
                                numSubordinates,
                                modifyTimestamp: getModDate(modifyTimestamp, "utc"),
                                modifyTimestampLocal: getModDate(modifyTimestamp, "local"),
                                ldapsubentry,
                                isRole,
                                isLockable,
                            });
                        allEntries.push(result);

                        // Reset the variables:
                        dn = '';
                        numSubordinates = '0';
                        modifyTimestamp = '';
                    }
                    return [];
                });
                // Process the list of entries.
                resultCallback(allEntries, null);
            });
}

export function getBaseLevelEntryAttributes (serverId, baseDn, entryAttributesCallback) {
    /* const cmd = [
    'ldapsearch',
    '-LLL',
    '-o',
    'ldif-wrap=no',
    '-Y',
    'EXTERNAL',
    '-b',
    baseDn,
    '-H',
    'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
    '-s',
    'base',
    '(|(objectClass=*)(objectClass=ldapSubEntry))',
    '*'
  ]; */

    // This is a base scope search. No need for a size limit.
    const timeLimit = getTimeLimit();
    const optionTimeLimit = timeLimit > 0 ? `-l ${timeLimit}` : '';

    const cmd = [
        '/usr/bin/sh',
        '-c',
        `ldapsearch -LLL -o ldif-wrap=no -Y EXTERNAL -b "${baseDn}"` +
    ` -H ldapi://%2fvar%2frun%2fslapd-${serverId}.socket` +
    ` ${optionTimeLimit}` +
    ' -s base "(|(objectClass=*)(objectClass=ldapSubEntry))" nsRoleDN nsAccountLock \\*' // +
    // ' | /usr/bin/head -c 150001' // Taking 1 additional character to check if the
    // the entry was indeed bigger than 150K.
    ];

    // TODO: The return code will always be 0 because of the ' | /usr/bin/head -c 150001' part.
    // Need to find a way to retrieve the LDAP return code...
    /*
    [root@cette ~]# ldapsearch -LLL -o ldif-wrap=no -Y EXTERNAL -b "o=empty" -H ldapi://%2fvar%2frun%2fslapd-ALPS_Grenoble.socket -s base "(|(objectClass=*)(objectClass=ldapSubEntry))" \* | /usr/bin/head -c 150001 2>/dev/null
    SASL/EXTERNAL authentication started
    SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
    SASL SSF: 0
    No such object (32)
    [root@cette ~]# echo $?
    0
    [root@cette ~]#
  */

    log_cmd("getBaseLevelEntryAttributes", "", cmd);
    const entryArray = [];
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                // TODO: Make this configurable ( option to keep X number of characters )
                const lines = data.split('\n');
                lines.map(currentLine => {
                    if (currentLine !== '') {
                        // Never truncate values for certificate
                        if (currentLine.length < 1000 || currentLine.substring(0, 9).toLowerCase()
                                .startsWith("jpegphoto") || currentLine.match(/.*certificate.*/i)) {
                            entryArray.push(splitAttributeValue(currentLine));
                        } else {
                            const myTruncatedValue = (
                                <div name="truncated">
                                    <Label icon={<InfoCircleIcon />} color="blue">
                                        {_("Value is too large to display")}
                                    </Label>
                                </div>
                            );
                            entryArray.push(myTruncatedValue);
                        }
                    }
                    return [];
                });
                entryAttributesCallback(entryArray);
            })
            .catch(err => {
                // console.log('getBaseLevelEntryAttributes in catch()');
                if (err.message === undefined) { // No error. Send the result array.
                    // entryAttributesCallback(entryArray);
                } else { /* if (msg.includes("Can't contact LDAP server")) {
                    // TODO: Handle the LDAP down status with a specific message? */
                    entryAttributesCallback([{
                        attribute: '???: ',
                        value: '???',
                        // errMsg: err.message
                        errorCode: err.exit_status
                    }]);
                    console.log('getBaseLevelEntryAttributes failed with exit_status = ' + err.exit_status);
                    console.log('getBaseLevelEntryAttributes error message = ' + err.message);
                }
            });
}

// Called to get an LDAP backup of the entry during a deletion.
// TODO: Use getBaseLevelEntryAttributes() with an additonal option to retrieve operational attributes.
export function getBaseLevelEntryFullAttributes (serverId, baseDn, entryAttributesCallback) {
    // This is a base scope search. No need for a size limit.
    const timeLimit = getTimeLimit();

    const cmd = [
        'ldapsearch',
        '-LLL',
        '-o',
        'ldif-wrap=no',
        '-Y',
        'EXTERNAL',
        '-b',
        baseDn,
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        '-s',
        'base',
        '-l',
        timeLimit,
        '(|(objectClass=*)(objectClass=ldapSubEntry))',
        '*',
        '+'
    ];

    log_cmd("getBaseLevelEntryFullAttributes", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                entryAttributesCallback(data);
            })
            .fail(err => {
                console.log('getBaseLevelEntryFullAttributes failed with exit_status = ' + err.exit_status);
                console.log('getBaseLevelEntryFullAttributes error message = ' + err.message);
                entryAttributesCallback('');
            });
}

function isAttributeLine (line, attr) {
    return (line.toLowerCase().startsWith(attr.toLowerCase()));
}

// TODO: Handle base64-encoded attributes where the separator is '::'
function splitAttributeValue (attrVal) {
    const index = attrVal.indexOf(':');
    if (index === -1) {
    // console.log('No colon found in attrVal = ' + attrVal);
        return { attribute: '', value: '' };
    }
    const attr = attrVal.substring(0, index);
    const val = attrVal.substring(index);
    return { attribute: attr, value: val };
}

// TODO: Optimize this function to return only a subset of the entry.
// Then on user expansion, search ( base level ) and return the whole entry.
// ==> Done.
export function getOneLevelEntries (params, oneLevelCallback) {
    const limits = getResourceLimits();

    const filter = params.filter
        ? params.filter
        : '(|(objectClass=*)(objectClass=ldapSubEntry))';

    const cmd = [
        'ldapsearch',
        '-LLL',
        '-o',
        'ldif-wrap=no',
        '-Y',
        'EXTERNAL',
        '-b',
        params.baseDn,
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + params.serverId + '.socket',
        '-s',
        'one',
        filter,
        /* '-l',
    timeLimit,
    '-z',
    sizeLimit, */
        ...limits,
        '1.1',
        'numSubordinates',
        'modifyTimestamp',
        'objectclass'
    ];

    log_cmd("getOneLevelEntries", "", cmd);
    let dn = '';
    let numSubordinates = '';
    let modifyTimestamp = '';
    let searchResult = null;
    const allEntries = [];
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' }) // string.split("\n\r")
            .done(data => {
                searchResult = data;
            })
            .catch((err, data) => {
                console.log('FAIL err.exit_status ==> ' + err.exit_status);
                console.log('FAIL err.message ==> ' + err.message);
                if (err.exit_status === 4) {
                    // Use the partial data.
                    searchResult = data;
                    const size_limit = getSizeLimit();
                    params.addNotification(
                        "info",
                        cockpit.format(_("Size limit of $0 was exceeded.  The child entries of $1 have been truncated. Use the 'Search' feature if you want to adjust the size limit and retrieve more entries"), size_limit, params.baseDn)
                    );
                } else {
                    searchResult = null;
                    oneLevelCallback(null, params, { status: err.exit_status, msg: err.message });
                }
            })
            .finally(() => {
                if (searchResult === null) {
                    return;
                }
                const lines = searchResult.split('\n');
                let ldapsubentry = false;
                let isRole = false;
                let isLockable = false;
                lines.map(currentLine => {
                    const accountObjectclasses = ['nsaccount', 'nsperson', 'simplesecurityobject',
                        'organization', 'person', 'account', 'organizationalunit',
                        'netscapeserver', 'domain', 'posixaccount', 'shadowaccount',
                        'posixgroup', 'mailrecipient', 'nsroledefinition'];
                    if (isAttributeLine(currentLine, 'dn:')) {
                        // Convert base64-encoded DNs
                        const pos = currentLine.indexOf(':');
                        if (currentLine.startsWith('dn::')) {
                            dn = b64DecodeUnicode(currentLine.substring(pos + 2).trim());
                        } else {
                            dn = currentLine.substring(pos + 1).trim();
                        }
                        ldapsubentry = false;
                        isRole = false;
                        isLockable = false;
                    } else if (isAttributeLine(currentLine, 'numSubordinates:')) {
                        numSubordinates = (currentLine.split(':')[1]).trim();
                    } else if (isAttributeLine(currentLine, 'modifyTimestamp:')) {
                        modifyTimestamp = (currentLine.split(':')[1]).trim();
                    } else if (isAttributeLine(currentLine, 'objectclass: ldapsubentry')) {
                        ldapsubentry = true;
                    } else if (isAttributeLine(currentLine, 'objectclass: nsroledefinition')) {
                        isRole = true;
                    }
                    for (const accountOC of accountObjectclasses) {
                        if (isAttributeLine(currentLine, `objectclass: ${accountOC}`)) {
                            isLockable = true;
                        }
                    }

                    if (currentLine === '' && dn !== '') {
                        const result = JSON.stringify(
                            {
                                dn,
                                numSubordinates,
                                modifyTimestamp: getModDate(modifyTimestamp,
                                                            "utc"),
                                modifyTimestampLocal: getModDate(modifyTimestamp,
                                                                 "local"),
                                ldapsubentry,
                                isRole,
                                isLockable,
                            });
                        allEntries.push(result);

                        // Reset the variables:
                        dn = '';
                        numSubordinates = '0';
                        modifyTimestamp = '';
                    }
                    return [];
                });
                // Process the list of entries.
                oneLevelCallback(allEntries, params, null);
            });
}

// Generic search that returns an array of LDAP entries.
// Returns an empty array in case of failure.
export function runGenericSearch (params, searchCallback) {
    /* const cmd = [
    'ldapsearch',
    '-LLL',
    '-o',
    'ldif-wrap=no',
    '-Y',
    'EXTERNAL',
    '-b',
    params.baseDn,
    '-H',
    'ldapi://%2fvar%2frun%2fslapd-' + params.serverId + '.socket',
    '-s',
    params.scope,
    params.filter,
    params.attributes
  ]; */

    const cmd = [
        '/usr/bin/sh',
        '-c',
        'ldapsearch -LLL -o ldif-wrap=no -Y EXTERNAL -b "' + params.baseDn +
    '" -H ldapi://%2fvar%2frun%2fslapd-' + params.serverId + '.socket' +
    ' -s ' + params.scope +
    ' "' + params.filter + '" ' +
    params.attributes
    ];

    log_cmd("runGenericSearch", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                const resulEntries = data.split('\n\n'); // Split by empty line.
                // console.log(`resulEntries = ${resulEntries}`);
                resulEntries.pop(); // Remove the last empty line.
                searchCallback(resulEntries);
                // console.log(`resulEntries after pop() = ${resulEntries}`);
            })
            .fail(err => {
                console.log('FAIL err.exit_status ==> ' + err.exit_status);
                console.log('FAIL err.message ==> ' + err.message);
                searchCallback([]);
            });
}

// Compute the size of the logs.
function computeLogSize (size) {
    if (size > 0) {
    // const exponent = Math.floor(Math.log(size) / Math.log(1024));
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log
        const value = 1 / Math.log(1024);
        const exponent = Math.floor(Math.log(size) * value);
        return (size / Math.pow(1024, exponent)).toFixed(2) * 1 +
    ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][exponent];
    } else {
        return '0 B';
    }
}

export function listAccessLogs (logDirectory, logListCallback) {
// export function listAccessLogs (logDirectory) {
    const cmd = [
        '/usr/bin/sh',
        '-c',
        'cd ' + logDirectory + ' ; ' +
    'stat -c "%n %s %U %a" ' +
    './access* | grep -v access.rotationinfo'
    ];

    log_cmd("listAccessLogs", "", cmd);

    const logDataArray = [];

    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                // console.log('SUCCESS ' + data);
                const lines = data.split('\n');
                lines.map(currentLine => {
                    // console.log('currentLine = ' + currentLine);
                    if (currentLine.trim() !== '') {
                        const info = currentLine.split(' ');
                        // console.log(info[1] + ' ==> ' + computeLogSize(info[1]));
                        logDataArray.push({
                            name: info[0].substring(2), // Remove the './' part.
                            size: computeLogSize(info[1]),
                            owner: info[2],
                            rights: info[3]
                        });
                    }
                    return [];
                });
            })
            .fail((err) => {
                console.log('FAIL - listAccessLogs() ==> ' + err.message);
            })
            .always(() => {
                logListCallback(logDataArray);
            });
}

// Modify ( or create ) an LDAP entry.
export function modifyLdapEntry (params, ldifArray, modifyEntryCallback) {
    const serverId = params.serverId;
    const addOption = params.isAddOp ? '-a' : '';
    let ldifData = '';
    let logLdifData = '';
    if (ldifArray.length === 0) {
        console.log('modifyLdapEntry() ==> Empty array! Nothing to do.');
        return;
    }
    for (const line of ldifArray) {
        ldifData += `${line}\n`;
        if (line.toLowerCase().startsWith("userpassword")) {
            logLdifData += `userPassword: ********\n`;
        } else {
            logLdifData += `${line}\n`;
        }
    }
    const cmd = [
        '/usr/bin/sh',
        '-c',
        `ldapmodify ${addOption} -Y EXTERNAL -H ` +
    `ldapi://%2fvar%2frun%2fslapd-${serverId}.socket `
    ];
    const cmd_copy = [
        '/usr/bin/sh',
        '-c',
        `ldapmodify ${addOption} -Y EXTERNAL -H ` +
    `ldapi://%2fvar%2frun%2fslapd-${serverId}.socket\n`,
        logLdifData // hides userpassword value from console log
    ];

    let result = {};
    log_cmd("modifyLdapEntry", "", cmd_copy);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .input(ldifData, true)
            .input()
            .done(data => {
                result = { errorCode: 0, output: data };
            })
            .fail((err) => {
                console.log('FAIL - modifyLdapEntry() ==> ' + err.message);
                console.log('Error code (err.exit_status) = ' + err.exit_status);
                let errMessage = '';
                for (const msg of err.message.split('\n')) {
                    if (!msg.startsWith('SASL')) {
                        errMessage += msg;
                    }
                }
                result = { errorCode: err.exit_status, output: errMessage };
            })
            .always(() => {
                modifyEntryCallback(result);
            });
}

// Create an LDAP entry.
export function createLdapEntry (serverId, ldifArray, createEntryCallback) {
    const params = {
        serverId,
        isAddOp: true
    };

    modifyLdapEntry(params, ldifArray, createEntryCallback);
}

// Retrieve all objectClasses along with their required and optional attributes.
export function getAllObjectClasses (serverId, allOcCallback) {
    const cmd = [
        'dsconf',
        '--json',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        'schema',
        'objectclasses',
        'list',
        '--include-sup'
    ];
    const result = [];
    log_cmd("getAllObjectClasses", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                const myObject = JSON.parse(data);
                for (const oc of myObject.items) {
                    result.push({
                        name: oc.name[0], // name[0] is the attribute name in lower case.
                        required: oc.must,
                        optional: oc.may
                    });
                }
            })
            .fail((err) => {
                console.log('FAIL - getAllObjectClasses - err.message ==> ' + err.message);
            })
            .always(() => {
                allOcCallback(result);
            });
}

// Retrieve the list of attributes that are single-valued.
export function getSingleValuedAttributes (serverId, svCallback) {
    const cmd = [
        'dsconf',
        '--json',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        'schema',
        'attributetypes',
        'list'
    ];
    const result = [];
    log_cmd("getSingleValuedAttributes", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                const myObject = JSON.parse(data);
                for (const attr of myObject.items) {
                    if (attr.single_value) {
                        result.push(attr.name[0]); // name[0] is the attribute name in lower case.
                    }
                }
            })
            .fail((err) => {
                console.log('FAIL - getAttributeList - err.message ==> ' + err.message);
            })
            .always(() => {
                svCallback(result);
            });
}

// Retrieve the names and OID of all attributes.
export function getAttributesNameAndOid (serverId, attrCallback) {
    const cmd = [
        'dsconf',
        '--json',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        'schema',
        'attributetypes',
        'list'
    ];
    const result = [];
    log_cmd("getAttributesNameAndOid", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                const myObject = JSON.parse(data);
                for (const attr of myObject.items) {
                    result.push([attr.names[0], attr.oid[0]]);
                }
            })
            .fail((err) => {
                console.log('FAIL - getAttributesNameAndOid - err.message ==> ' + err.message);
            })
            .always(() => {
                attrCallback(result);
            });
}

export function deleteLdapData (serverId, entryDN, numSubordinates, deleteCallback) {
    const cmd = [
        'ldapdelete',
        '-Y',
        'EXTERNAL',
        '-H',
        'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
        entryDN
    ];
    if (numSubordinates > 0) {
        cmd.push('-r'); // Recursive deletion.
    }

    let result = {};
    log_cmd("deleteLdapData", "", cmd);
    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
            .done(data => {
                // console.log('SUCCESS - deleteLdapData() ==> ' + data);
                result = { errorCode: 0, output: data };
            })
            .fail((err) => {
                console.log('FAIL - deleteLdapData() ==> ' + err.message);
                let errMessage = '';
                for (const msg of err.message.split('\n')) {
                    if (!msg.startsWith('SASL')) {
                        errMessage += msg;
                    }
                }
                result = { errorCode: err.exit_status, output: errMessage };
            })
            .always(() => {
                deleteCallback(result);
            });
}

export function showCertificate (certificate, showCertCallback) {
    const cmd = [
        '/usr/bin/sh',
        '-c',
        'echo ' + certificate +
    ' | base64 --decode | openssl x509 -inform DER -noout -subject -issuer -dates -serial ;' +
    // ' echo HOST_TIME_GMT=`TZ=GMT date "+%Y-%m-%d %H:%M:%S %Z"` ' // Issue with Firefox and Safari.
    ' echo HOST_TIME_GMT=`date -Iminutes`'
    ];

    let result = {};
    const certDataArray = [];

    cockpit
            .spawn(cmd, { superuser: true, err: 'message' })
    // .input(decodedCert)
            .done(data => {
                // console.log('SUCCESS - showCertificate() ==> ' + data);
                const lineArray = data.split('\n');
                let certEndTime = 0;
                let hostTime = 0;
                lineArray.map(line => {
                    const pos = line.indexOf('=');
                    const param = line.substring(0, pos);
                    const paramVal = line.substring(pos + 1);
                    if (param !== 'HOST_TIME_GMT') {
                        certDataArray.push({
                            param,
                            paramVal
                        });
                        if (param === 'notAfter') {
                            certEndTime = Date.parse(paramVal);
                        }
                    } else {
                        hostTime = Date.parse(paramVal);
                    }
                    return [];
                });

                result = {
                    code: 'OK',
                    data: certDataArray,
                    timeDifference: certEndTime - hostTime
                };
            })
            .fail((err) => {
                console.log('FAIL - showCertificate() ==> ' + err.message);
                result = { code: 'FAIL' };
            })
            .always(() => {
                showCertCallback(result);
            });
}

// From https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
export function b64DecodeUnicode (str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    try {
        const result = decodeURIComponent(atob(str).split('')
                .map(c => {
                    return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
                })
                .join(''));
        return result;
    } catch (e) {
        console.debug("b64DecodeUnicode failed to decode: ", str);
        return str;
    }
}

// Takes a line with an encoded attribute.
// Returns [attribute, decodedValue].
export function decodeLine (line) {
    const pos = line.indexOf('::');
    return [
        line.substring(0, pos),
        b64DecodeUnicode(line.substring(pos + 2).trim())
    ];
}

// Quick and dirty function to retrieve RDN data.
export function getRdnInfo (entryDn) {
    let myRdn = '';
    const myArray = entryDn.split(',');
    for (const datum of myArray) {
        myRdn += datum;
        if (datum.slice(-1) !== '\\') {
            // The separator "," was not escaped. We've hopefully got the RDN.
            break;
        }
        myRdn += ',';
    }
    const myRdnData = myRdn.split('=');
    return {
        rdnAttr: myRdnData[0],
        rdnVal: myRdnData[1]
    };
}

// Create the top entry LDIF data
/*
  Only a limited subset of attributes are supported to create the RDN of a top entry:
  c ==> objectclass country
  cn ==> objectclass nsContainer
  dc ==> objectclass domain
  o ==> objectclass organization
  ou ==> objectclass organizationalUnit
*/
export function generateRootEntryLdif (suffixDn, ldifCallback) {
    const rdnObj = getRdnInfo(suffixDn);
    /* let myRdn = '';
  const myArray = suffixDn.split(',');
  for (const datum of myArray) {
    myRdn += datum;
    if (datum.slice(-1) !== '\\') {
      // The separator "," was not escaped. We've got the RDN.
      break;
    }
    myRdn += ',';
  } */

    const myRdnAttr = rdnObj.rdnAttr;
    const myRdnVal = rdnObj.rdnVal;

    let myObjectClass;
    let isValid = true;
    switch (myRdnAttr) {
    case 'c':
        myObjectClass = 'country';
        break;
    case 'cn':
        myObjectClass = 'nsContainer';
        break;
    case 'dc':
        myObjectClass = 'domain';
        break;
    case 'o':
        myObjectClass = 'organization';
        break;
    case 'ou':
        myObjectClass = 'organizationalUnit';
        break;
    default:
        isValid = false;
        console.log(`The RDN attribute ${myRdnAttr} is not supported for root entry creation!`);
    }

    const ldifArray = [];
    if (isValid) {
        ldifArray.push(`dn: ${suffixDn}`);
        ldifArray.push('objectClass: top');
        ldifArray.push(`objectClass: ${myObjectClass}`);
        ldifArray.push(`${myRdnAttr}: ${myRdnVal}`);
    } else {
        ldifArray.push(`The RDN attribute "${myRdnAttr}" is not supported for root entry creation!`);
    }
    ldifCallback(ldifArray);
}

// Get all ACIs that apply to a given entry.
export function retrieveAllAcis (params, aciCallback) {
    const entryDn = params.baseDn;
    const resultArray = []; // An item will be like {entryDn:<DN>, aciArray:[aci:<ACI_1>,aci:<ACI_2>]}
    const myParams = {
        serverId: params.serverId,
        baseDn: entryDn,
        scope: 'base',
        filter: 'objectClass=*',
        attributes: '1.1 aci'
    };

    runGenericSearch(myParams, (result) => {
        if (result.length > 0) {
            const myAcis = result[0].split('\n'); // There should be a single result ( base scope search ).
            if (myAcis.length > 1) { // The array contains at least 1 ACI.
                myAcis.shift(); // Remove the DN (first element) from the array.
                const actualAcis = myAcis.map(aciLine => {
                    const pos = aciLine.indexOf(':');
                    const theAci = aciLine.startsWith('aci:: ')
                        ? b64DecodeUnicode((aciLine.substring(pos + 2)).trim())
                        : (aciLine.substring(pos + 1)).trim();
                    return theAci;
                });
                const myBaseDn = myParams.baseDn;
                const myObj = {
                    entryDn: myBaseDn,
                    aciArray: actualAcis,
                };
                resultArray.push(myObj);
            }
        }
        aciCallback(resultArray);
    });
}

/*
// Encode binary attributes.
export function base64encode (fileName, encodingCallback) {
  const cmd = [
    '/usr/bin/base64',
    fileName
  ];
  const encodedValue = null;
  console.log('Command = ' + cmd.toString());
  cockpit
    .spawn(cmd, { superuser: true, err: 'message' })
    .done(data => {
      encodingCallback(data);
    })
    .fail((err) => {
      console.log('FAIL - base64encode - err.message ==> ' + err.message);
    })
    .always(() => {
      encodingCallback(null);
    });
}
*/

// Fold long LDIF lines and return an array
// containing lines with 78 characters maximum.
export function foldLine (line) {
    if (line.length <= LDIF_MAX_CHAR_PER_LINE) {
        return [line];
    }

    const size = Math.ceil(line.length / LDIF_MAX_CHAR_PER_LINE);
    const lineArray = new Array(size);
    // Handle the first line separately since it doesn't start with a space.
    lineArray[0] = line.substring(0, LDIF_MAX_CHAR_PER_LINE);

    for (let i = 1; i < size; i++) {
    // The continuation character is a single space.
        lineArray[i] = ' ' + line.substring(i * LDIF_MAX_CHAR_PER_LINE, (i + 1) * LDIF_MAX_CHAR_PER_LINE);
    }

    return lineArray;
}

export function getBaseDNFromTree (entrydn, treeViewRootSuffixes) {
    for (const suffixObj of treeViewRootSuffixes) {
        if (entrydn.toLowerCase().indexOf(suffixObj.name.toLowerCase()) !== -1) {
            return suffixObj.name;
        }
    }
    return "";
}
