/**
 * @file commandBroker.js
 * @module commandBroker
 * @description Executes commands by calling the appropriate command-function from the commandLibrary,
 * which will actually be stored functions on the D-Data structure.
 * @requires module:ruleBroker
 * @requires module:commandsLibrary
 * @requires module:colorizer
 * @requires module:configurator
 * @requires module:loggers
 * @requires module:data
 * @requires module:queue
 * @requires module:stack
 * @requires {@link https://www.npmjs.com/package/@haystacks/constants|@haystacks/constants}
 * @requires {@link https://www.npmjs.com/package/path|path}
 * @author Seth Hollingsead
 * @date 2022/02/02
 * @copyright Copyright © 2022-… by Seth Hollingsead. All rights reserved
 */

// Internal imports
import ruleBroker from './ruleBroker.js';
import commandsLibrary from '../commandsBlob/commandsLibrary.js';
import colorizer from '../executrix/colorizer.js';
import configurator from '../executrix/configurator.js';
import loggers from '../executrix/loggers.js';
import D from '../structures/data.js';
import queue from '../structures/queue.js';
import stack from '../structures/stack.js';
// External imports
import hayConst from '@haystacks/constants';
import path from 'path';

const {bas, biz, clr, cfg, gen, msg, num, sys, wrd} = hayConst;
const baseFileName = path.basename(import.meta.url, path.extname(import.meta.url));
// framework.brokers.commandBroker.
const namespacePrefix = wrd.cframework + bas.cDot + wrd.cbrokers + bas.cDot + baseFileName + bas.cDot;

/**
 * @function bootStrapCommands
 * @description Captures all of the commands string-to-function cal map data in the commandsLibrary and migrates that dat a to the D-data structure.
 * This is important now because we are going to allow the client to define their own commands separate from the system defined commands.
 * So we need a way to merge al the client defined and system defined commands into one location.
 * Then the command broker will execute commands rom the D-Data structure and not the commands library per-say.
 * This will allow the system to expand much more dynamically and even be user-defined & flexible to client needs.
 * @return {void}
 * @author Seth Hollingsead
 * @date 2022/02/02
 */
async function bootStrapCommands() {
  let functionName = bootStrapCommands.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  await commandsLibrary.initCommandsLibrary();
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
}

/**
 * @function addClientCommands
 * @description Merges client defined commands with the system defined commands.
 * @param {array<object>} clientCommands The client rules that should be merged with the system rules.
 * @return {void}
 * @author Seth Hollingsead
 * @date 2022/02/02
 */
async function addClientCommands(clientCommands) {
  let functionName = addClientCommands.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // Object.assign(D[wrd.cCommands], clientCommands);
  // D[wrd.cCommands] = {...D[wrd.cCommands], Object.keys(clientCommands): clientCommands[Object.keys(clientCommands)]};
  for (const [key, value] of Object.entries(clientCommands)) {
    // console.log('%%%%%%%%%%%%%%%%%% ---- >>>>>>>>> key is: ' + key);
    D[wrd.cCommands] = {...D[wrd.cCommands], [`${key}`]: value};
  } // End-for (const [key, value] of Object.entries(clientCommands))
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
}

/**
 * @function addPluginCommands
 * @description Merges plugin defined commands with the system defined commands.
 * @param {string} pluginName The name of the current plugin these commands belong to.
 * @param {array<object>} pluginCommands The plugin commands that should be merged with the system commands.
 * @return {boolean} True or False to indicate if the merge was successful or not.
 * @author Seth Hollingsead
 * @date 2022/10/24
 */
async function addPluginCommands(pluginName, pluginCommands) {
  let functionName = addPluginCommands.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  // pluginCommands is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginCommandsIs + JSON.stringify(pluginCommands));
  let returnData = false;
  try {
    // if (D[wrd.cCommands][wrd.cplugins] === undefined) {
    //   D[wrd.cCommands][wrd.cplugins] = {};
    // }
    // D[wrd.cCommands][wrd.cplugins][pluginName] = {};
    // for (const [key, value] of Object.entries(pluginCommands)) {
    //   console.log('&&&&&&&&&&&&&&&&& ---- >>>>>>>> key is: ' + key);
    //   D[wrd.cCommands][wrd.cplugins][pluginName] = {...D[wrd.cplugin + wrd.cCommands], [`${key}`]: value};
    // } // End-for (const [key, value] of Object.entries(pluginCommands));

    // NOTE: The commands system was never designed to have a hierarchy storage, so when calling commands,
    // its basically calling a flat list. So rather than adding the plugin commands according to the above structure.
    // We will need to just add them to the flat list. If a plugin is unloaded,
    // then each of its commands will need to be individually searched out and removed from the flat list.
    for (const [key, value] of Object.entries(pluginCommands[wrd.ccommands])) {
      // console.log('&&&&&&&&&&&&&&&&& ---- >>>>>>>> key is: ' + key);
      D[wrd.cCommands] = {...D[wrd.cCommands], [`${key}`]: value};
    } // End-for (const [key, value] of Object.entries(pluginCommands))
    returnData = true;
  } catch (err) {
    // ERROR: Failure to merge the plugin commands for plugin:
    console.log(msg.cErrorAddPluginCommandsMessage01 + pluginName);
    console.log(msg.cERROR_Colon + err);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + returnData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function addPluginCommandAliases
 * @description Merges plugin defined command aliases with the system defined command aliases.
 * @param {string} pluginName The name of the current plugin these command aliases belong to.
 * @param {object} pluginCommandAliases A JSON object that contains the plugin command aliases that
 * should be merged with the system command aliases.
 * @return {boolean} True or False to indicate if the merge was successful or not.
 * @author Seth Hollingsead
 * @date 2022/10/24
 */
async function addPluginCommandAliases(pluginName, pluginCommandAliases) {
  let functionName = addPluginCommandAliases.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  // pluginCommandAliases is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginCommandAliasesIs + JSON.stringify(pluginCommandAliases));
  let returnData = false;
  try {
    if (D[sys.cCommandsAliases][wrd.cPlugins] === undefined) {
      D[sys.cCommandsAliases][wrd.cPlugins] = {};
    }
    D[sys.cCommandsAliases][wrd.cPlugins][pluginName] = {};
    D[sys.cCommandsAliases][wrd.cPlugins][pluginName] = pluginCommandAliases;
    returnData = true;
  } catch (err) {
    // ERROR: Failure to merge the plugin command aliases for plugin:
    console.log(msg.cErrorAddPluginCommandAliasesMessage01 + pluginName);
    console.log(msg.cERROR_Colon + err);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + returnData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function getValidCommand
 * @description Parses the command string and returns an array that can be used to
 * enqueue or execute that command. Useful for determining if a command is a valid command and
 * working with multiple levels of delimiters for nested command calls, looking up a command alias, etc...
 * @param {string} commandString The command string that should be parsed for a valid command.
 * @param {string} commandDelimiter The delimiter that should be used to parse the command line.
 * @return {boolean|string} False if the command is not valid, otherwise it returns the command string.
 * @author Seth Hollingsead
 * @date 2022/02/02
 */
async function getValidCommand(commandString, commandDelimiter) {
  let functionName = getValidCommand.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandString is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringIs + commandString);
  // commandDelimiter is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandDelimiterIs + commandDelimiter);
  let returnData = false;
  let foundValidCommand = false;
  let commandToExecute, commandArgs;
  let commandArgsDelimiter = commandDelimiter;
  if (commandDelimiter === null || commandDelimiter !== commandDelimiter || commandDelimiter === undefined) {
    commandArgsDelimiter = bas.cSpace;
  }
  if (commandString && commandString.includes(commandArgsDelimiter) === true) {
    commandArgs = commandString.split(commandArgsDelimiter);
    commandToExecute = commandArgs[0];
  } else {
    commandToExecute = commandString;
  }
  // commandString is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringIs + commandString);
  // commandToExecute is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandToExecuteIs + commandToExecute);
  if (commandString) {
    if (D[wrd.cCommands][commandToExecute] !== undefined) {
      returnData = commandToExecute;
    } else { // else-clause if (D[wrd.cCommands][commandToExecute] !== undefined)
      // else-clause looking for command aliases.
      await loggers.consoleLog(namespacePrefix + functionName, msg.celseClauseLookingForCommandAliases);
      // NOTE: It could be that the user entered a command alias, so we will need to search through all of the command aliases,
      // to see if we can find a match, then get the actual command that should be executed.
      let allCommandAliases = D[sys.cCommandsAliases];
      // allCommandAliases is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.callCommandAliasesIs + JSON.stringify(allCommandAliases));
      // Search through the data structure recursively to see if we can find the command or command alias.
      foundValidCommand = await searchCommandAlias(allCommandAliases, commandToExecute);
      // foundValidCommand is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cfoundValidCommandIs + JSON.stringify(foundValidCommand));
      // Check if we found a valid command and return it if we did,
      // or pop a message to indicate the command was not found.
      if (foundValidCommand === false) {
        // WARNING: The specified command:
        // does not exist, please try again!
        console.log(msg.cWarningTheSpecifiedCommand + commandToExecute + msg.cdoesNotExistPleaseTryAgain + bas.cSpace + num.c1);
      } else {
        returnData = foundValidCommand[wrd.cName];
      }
    } // End-else
  } else {
    // Looks like the user entered something undefined: Pop the standard error message:
    // WARNING: The specified command:
    // does not exist, please try again!
    console.log(msg.cWarningTheSpecifiedCommand + commandToExecute + msg.cdoesNotExistPleaseTryAgain);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function countMatchingCommandAlias
 * @description This is a recursive function that searches through all teh command aliases
 * data structures and counts the number of command aliases that match the input alias.
 * @param {object} commandAliasData The command alias data that should be searched recursively for the specified command alias.
 * @param {string} commandAliasName The command alias name/string that should be searched for and counted when matches are found.
 * @return {integer} The count of the number of command aliases that match the given input alias.
 * @author Seth Hollingsead
 * @date 2022/06/06
 */
async function countMatchingCommandAlias(commandAliasData, commandAliasName) {
  let functionName = countMatchingCommandAlias.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandAliasData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasDataIs + JSON.stringify(commandAliasData));
  // commandAliasName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasNameIs + commandAliasName);
  let commandAliasCount = 0;
  let blackColorArray = await colorizer.getNamedColorData(clr.cBlack, [0,0,0]);
  let redColorArray = await colorizer.getNamedColorData(clr.cRed, [255,0,0]);
  if (typeof commandAliasData === wrd.cobject) {
    for (let commandAliasEntity in commandAliasData) {
      // commandAliasEntity is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityIs + JSON.stringify(commandAliasEntity));
      // commandAliasEntityValue is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityValueIs + JSON.stringify(commandAliasData[commandAliasEntity]));
      if (commandAliasEntity.toUpperCase() != commandAliasName.toUpperCase()) {
        if (commandAliasData[commandAliasEntity] != undefined && commandAliasData[commandAliasEntity][wrd.cAliases] != undefined) {
          let aliasList = commandAliasData[commandAliasEntity][wrd.cAliases];
          let arrayOfAliases = aliasList.split(bas.cComa);
          for (const element of arrayOfAliases) {
            let currentAlias = element;
            await loggers.consoleLog(namespacePrefix + functionName, msg.ccurrentAliasIs + currentAlias);
            await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasNameIs + commandAliasName);
            if (commandAliasName === currentAlias) {
              // Found a matching alias entry! 1
              await loggers.consoleLog(namespacePrefix + functionName, msg.cFoundMatchingAliasEntry1);
              commandAliasCount = commandAliasCount + 1;
              // Don't break, continue searching, so we get a full count of any duplicates found.
            }
          } // End-for (const element of arrayOfAliases)
        } else {
          if (commandAliasData[commandAliasEntity] != undefined) {
            let tempCommandAliasCount = await countMatchingCommandAlias(commandAliasData[commandAliasEntity], commandAliasName);
            // tempCommandAliasCount is:
            await loggers.consoleLog(namespacePrefix + functionName, msg.ctempCommandAliasCountIs + tempCommandAliasCount);
            if (tempCommandAliasCount > 0) {
              // adding commandAliasCount:
              await loggers.consoleLog(namespacePrefix + functionName, msg.caddingCommandAliasCount + commandAliasCount);
              commandAliasCount = commandAliasCount + tempCommandAliasCount;
              // After adding commandAliasCount and tempCommandAliasCount:
              await loggers.consoleLog(namespacePrefix + functionName, msg.cAfterAddingCommandAliasCountAndTempCommandAliasCount + commandAliasCount);
              // Don't break, continue searching, so we get a full count of any duplicates found.
            } // End-if (tempCommandAliasCount > 0)
          } else {
            // ERROR: A command is missing command aliases definitions. Data:
            let errorMessage = msg.cErrorCountMatchingCommandAliasMessage01 + JSON.stringify(commandAliasData);
            errorMessage = await colorizer.colorizeMessageSimple(errorMessage, blackColorArray, true);
            errorMessage = await colorizer.colorizeMessageSimple(errorMessage, redColorArray, false);
            console.log(errorMessage);
            // Its an error, but not a duplicate error, however, we need to report it some how.
            commandAliasCount = commandAliasCount + 1; // We've console logged it as best we can, we need to raise the flag anyway.
          }          
        }
      } else if (commandAliasEntity.toUpperCase() === commandAliasName.toUpperCase()) {
        // Found a matching entry! 2
        await loggers.consoleLog(namespacePrefix + functionName, msg.cFoundMatchingAliasEntry2);
        commandAliasCount = commandAliasCount + 1;
        // Don't break, continue searching, so we get a full count of any duplicates found.
      }
    } // End-for (let commandAliasEntity in commandAliasData)
  } // End-if (typeof commandAliasData === wrd.cobject)
  // commandAliasCount is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasCountIs + JSON.stringify(commandAliasCount));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return commandAliasCount;
}

/**
 * @function searchCommandAlias
 * @description This is a recursive function that searches through all the command aliases
 * data structures and returns the one command data object that matches the input name.
 * @param {object} commandAliasData The command alias data that should be searched recursively for the specified command alias.
 * @param {string} commandAliasName The command alias name/string that should be found.
 * @return {object} The command object that corresponds to the input command alias name.
 * @author Seth Hollingsead
 * @date 2022/05/27
 */
async function searchCommandAlias(commandAliasData, commandAliasName) {
  let functionName = searchCommandAlias.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandAliasData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasDataIs + JSON.stringify(commandAliasData));
  // commandAliasName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasNameIs + commandAliasName);
  let commandAliasObject = false;
  if (typeof commandAliasData === wrd.cobject) {
    for (let commandAliasEntity in commandAliasData) {
      // commandAliasEntity is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityIs + JSON.stringify(commandAliasEntity));
      // commandAliasEntityValue is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityValueIs + JSON.stringify(commandAliasData[commandAliasEntity]));
      if (commandAliasEntity.toUpperCase() != commandAliasName.toUpperCase() && commandAliasData[commandAliasEntity]) {
        if (commandAliasData[commandAliasEntity][wrd.cAliases] != undefined) {
          let aliasList = commandAliasData[commandAliasEntity][wrd.cAliases];
          let arrayOfAliases = aliasList.split(bas.cComa);
          for (const element of arrayOfAliases) {
            let currentAlias = element;
            if (commandAliasName === currentAlias ||
            commandAliasName === bas.cDash + currentAlias ||
            commandAliasName === bas.cDoubleDash + currentAlias ||
            commandAliasName === bas.cForwardSlash + currentAlias ||
            commandAliasName === bas.cBackSlash + currentAlias ||
            commandAliasName.toUpperCase() === currentAlias.toUpperCase() ||
            commandAliasName.toUpperCase() === bas.cDash + currentAlias.toUpperCase() ||
            commandAliasName.toUpperCase() === bas.cDoubleDash + currentAlias.toUpperCase() ||
            commandAliasName.toUpperCase() === bas.cForwardSlash + currentAlias.toUpperCase() ||
            commandAliasName.toUpperCase() === bas.cBackSlash + currentAlias.toUpperCase() ||
            commandAliasName.toLowerCase() === currentAlias.toLowerCase() ||
            commandAliasName.toLowerCase() === bas.cDash + currentAlias.toLowerCase() ||
            commandAliasName.toLowerCase() === bas.cDoubleDash + currentAlias.toLowerCase() ||
            commandAliasName.toLowerCase() === bas.cForwardSlash + currentAlias.toLowerCase() ||
            commandAliasName.toLowerCase() === bas.cBackSlash + currentAlias.toLowerCase()) {
              // Found a matching alias entry!
              await loggers.consoleLog(namespacePrefix + functionName, msg.cFoundMatchingAliasEntry1);
              commandAliasObject = commandAliasData[commandAliasEntity];
              break;
            }
          } // End-for (const element of arrayOfAliases)
        } else {
          let commandAliasesObjectTemp = await searchCommandAlias(commandAliasData[commandAliasEntity], commandAliasName);
          // commandAliasesObjectTemp is:
          await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasesObjectTempIs + JSON.stringify(commandAliasesObjectTemp));
          if (commandAliasesObjectTemp) {
            commandAliasObject = commandAliasesObjectTemp;
            break;
          } // End-if (commandAliasesObjectTemp)
        }
      } else if (commandAliasEntity.toUpperCase() === commandAliasName.toUpperCase()) {
        // Found a matching entry!
        await loggers.consoleLog(namespacePrefix + functionName, msg.cFoundMatchingAliasEntry2);
        commandAliasObject = commandAliasData[commandAliasEntity];
        break;
      }
    } // End-for (let commandAliasEntity in commandAliasData)
  } // End-if (typeof commandAliasData === wrd.cobject)
  // commandAliasObject is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasObjectIs + JSON.stringify(commandAliasObject));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return commandAliasObject;
}

/**
 * @function getAllCommandAliasData
 * @description Recursively gets all of the commands alias data from all levels and flattens them into a single array for printing out to the help command.
 * @param {object} commandAliasDataStructure The command alias data structure that should be recursively flattened into a single array for output.
 * If the input is undefined then the main CommandsAliases data structure will be used at the root of the command aliases data hive.
 * @return {array<string>|boolean} An array of all the command aliases currently needing to be flattened or
 * a boolean True or False to indicate that a leaf-node has been found by the recursive caller.
 * @author Seth Hollingsead
 * @date 2022/05/27
 */
async function getAllCommandAliasData(commandAliasDataStructure) {
  let functionName = getAllCommandAliasData.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandAliasDataStructure is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasDataStructureIs + JSON.stringify(commandAliasDataStructure));
  let allCommandsData = false;
  let internalCommandAliasDataStructure;
  if (commandAliasDataStructure === undefined) {
    internalCommandAliasDataStructure = JSON.parse(JSON.stringify(D[sys.cCommandsAliases]));
  } else {
    internalCommandAliasDataStructure = JSON.parse(JSON.stringify(commandAliasDataStructure));
  }
  // internalCommandAliasDataStructure is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cinternalCommandAliasDataStructureIs + JSON.stringify(internalCommandAliasDataStructure));
  if (typeof internalCommandAliasDataStructure === wrd.cobject) {
    allCommandsData = [];
    for (let commandAliasEntity in internalCommandAliasDataStructure) {
      // commandAliasEntity is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityIs + JSON.stringify(commandAliasEntity));
      // internalCommandAliasDataStructure[commandAliasEntity] is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cinternalCommandAliasDataStructureCommandAliasEntityIs + JSON.stringify(internalCommandAliasDataStructure[commandAliasEntity]));
      if (typeof internalCommandAliasDataStructure[commandAliasEntity] === wrd.cobject) {
        // internalCommandAliasDataStructure[commandAliasEntity] is of type object!
        await loggers.consoleLog(namespacePrefix + functionName, msg.cgetAllCommandAliasDataMessage01);
        let allCommandAliasesTemp;
        allCommandAliasesTemp = await getAllCommandAliasData(internalCommandAliasDataStructure[commandAliasEntity]);
        // allCommandAliasesTemp returned from the recursive call is:
        await loggers.consoleLog(namespacePrefix + functionName, msg.callCommandAliasesTempReturnedFromRecursiveCallIs + JSON.stringify(allCommandAliasesTemp));
        if (allCommandAliasesTemp === false) {
          // The recursive call returned false, so push the current entity to the output array!
          await loggers.consoleLog(namespacePrefix + functionName, msg.cgetAllCommandAliasDataMessage02);
          allCommandsData.push(internalCommandAliasDataStructure);
          // allCommandsData after pushing to the array is:
          await loggers.consoleLog(namespacePrefix + functionName, msg.callCommandsDataAfterPushingToTheArrayIs + JSON.stringify(allCommandsData));
          break;
        } else {
          allCommandsData = await ruleBroker.processRules([allCommandsData, allCommandAliasesTemp], [biz.cobjectDeepMerge]);
        }
      } else {
        allCommandsData = false; // Reset it, because it was reinitalized to an array.
      }
    } // End-for (let commandAliasEntity in internalCommandAliasDataStructure)
  } // End-if (typeof internalCommandAliasDataStructure === wrd.cobject)
  // allCommandsData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.callCommandsDataIs + JSON.stringify(allCommandsData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return allCommandsData;
}

/**
 * @function getCommandNamespaceDataObject
 * @description Recursively scans through the entire command alias data structure looking for an object that matches the input namespace name.
 * When that namespace is found, the entire object is returned.
 * @param {object} commandAliasDataStructure The command alias data structure that should be recursively searched for the namespace specified.
 * if the input is undefined then the main cCommandsAliases data structure will be used at the root of the CommandAliases data hive.
 * @param {string} namespaceToFind The namespace to look for in the command alias metaData data structure.
 * @return {object|boolean} The namespace object if it is found, or False if the namespace object was not found.
 * @author Seth Hollingsead
 * @date 2022/05/27
 */
async function getCommandNamespaceDataObject(commandAliasDataStructure, namespaceToFind) {
  let functionName = getCommandNamespaceDataObject.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandAliasDataStructure is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasDataStructureIs + JSON.stringify(commandAliasDataStructure));
  // namespaceToFind is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cnamespaceToFindIs + namespaceToFind);
  let namespaceCommandsObject = false;
  if (commandAliasDataStructure === undefined) {
    commandAliasDataStructure = D[sys.cCommandsAliases];
  }
  for (let commandAliasEntity in commandAliasDataStructure) {
    // commandAliasEntity is:
    await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasEntityIs + JSON.stringify(commandAliasEntity));
    // commandAliasDataStructure[commandAliasEntity] is:
    await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandAliasDataStructureCommandAliasEntityIs + JSON.stringify(commandAliasDataStructure[commandAliasEntity]));
    if (commandAliasEntity === namespaceToFind) {
      namespaceCommandsObject = commandAliasDataStructure[commandAliasEntity];
      break;
    } else if (typeof commandAliasDataStructure[commandAliasEntity] === wrd.cobject) {
      // Search recursively
      let namespaceCommandsTempObject = await getCommandNamespaceDataObject(commandAliasDataStructure[commandAliasEntity], namespaceToFind);
      if (namespaceCommandsTempObject !== false) {
        // Then we must have found the namespace object we were looking for in the recursion call.
        // Just return it, and skip out of the loop.
        namespaceCommandsObject = namespaceCommandsTempObject;
        break;
      }
    }
  } // End-for (let commandAliasEntity in commandAliasDataStructure)
  // namespaceCommandsObject is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cnamespaceCommandsObjectIs + JSON.stringify(namespaceCommandsObject));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return namespaceCommandsObject;
}

/**
 * @function getCommandArgs
 * @description Gets the arguments of the current command.
 * @param {string} commandString The command string that should be parsed fro command arguments.
 * @param {string} commandDelimiter The delimiter that should be used to parse the command line.
 * @return {array<boolean|string|integer>} Any array of arguments, some times these might actually be nested command calls.
 * @author Seth Hollingsead
 * @date 2022/02/02
 */
async function getCommandArgs(commandString, commandDelimiter) {
  let functionName = getCommandArgs.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandString is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringIs + commandString);
  // commandDelimiter is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandDelimiterIs + commandDelimiter);
  let returnData = false;
  let commandArgsDelimiter = commandDelimiter;
  let isOddRule = [biz.cisOdd];
  let replaceCharacterAtIndexRule = [biz.creplaceCharacterAtIndex];
  let replaceTildesWithSingleQuoteRule = [biz.creplaceCharacterWithCharacter];
  let stringLiteralCommandDelimiterAdded = false;
  let secondaryCommandArgsDelimiter = await configurator.getConfigurationSetting(wrd.csystem, cfg.csecondaryCommandDelimiter);
  if (commandDelimiter === null || commandDelimiter !== commandDelimiter || commandDelimiter === undefined) {
    commandArgsDelimiter = bas.cSpace;
  }
  if (commandString.includes(commandArgsDelimiter) === true) {
    // NOTE: All commands that enqueue or execute commands need to pass through this function.
    // There is a case where the user might pass a string with spaces or other code/syntax.
    // So we need to split first by single character string delimiters and parse the
    // non-string array elements to parse command arguments without accidentally parsing string literal values as command arguments.
    if (commandString.includes(bas.cBackTickQuote) === true) {
      // commandString contains either a singleQuote or a backTickQuote
      await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringContainsEitherSingleQuoteOrBackTickQuote);
      let preSplitCommandString;
      if (commandString.includes(bas.cBackTickQuote) === true) {
        // commandString contains a singleQuote!
        await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringContainsSingleQuote);
        // NOTE: We cannot actually just replace ach single quote, we need to tag each single quote in pairs of 2.
        // The first one should be post-tagged, i.e. replace "'" with "'~" and the second should be pre-tagged i.e. replace "'" with "~'".
        // Then if there are more single quotes, the thirst post-tagged, i.e. replace "'" with "'~", etc...
        let numberOfSingleQuotes = commandString.split(bas.cBackTickQuote).length - 1;
        // Determine if the number of single quotes is odd or even?
        // About to call the rule broker to process on the number of single quotes and determine if it-be even or odd.
        await loggers.consoleLog(namespacePrefix + functionName, msg.cgetCommandArgsMessage1 + sys.cgetCommandArgsMessage2);
        if (numberOfSingleQuotes >= 2 && await ruleBroker.processRules([numberOfSingleQuotes, ''], isOddRule) === false) {
          // numberOfSingleQuotes is >= 2 & the numberOfSingleQuotes is EVEN! YAY!
          await loggers.consoleLog(namespacePrefix + functionName, msg.cnumberOfSingleQuotesIsEven);
          let indexOfStringDelimiter;
          for (let i = 0; i < numberOfSingleQuotes; i++) {
            // Iterate over each one and if they are even or odd we will change how we replace ach single quote character as described above.
            if (i === 0) {
              // Get the index of the first string delimiter.
              indexOfStringDelimiter = commandString.indexOf(bas.cBackTickQuote, 0);
              // First index is:
              await loggers.consoleLog(namespacePrefix + functionName, msg.cFirstIndexIs + indexOfStringDelimiter);
              // commandString.replace(bas.cBackTickQuote, bas.cBackTickQuote + bas.cTilde);
              // Rather than use the above, we will make a business rule o replace at index, the above replaces all isntances and we don't want that!
              commandString = await ruleBroker.processRules([commandString, [indexOfStringDelimiter, bas.cBackTickQuote + bas.cTilde]], replaceCharacterAtIndexRule);
              stringLiteralCommandDelimiterAdded = true;
              // commandString after tagging the first string delimiter:
              await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringAfterTaggingTheFirstStringDelimiter + commandString);
            } else {
              indexOfStringDelimiter = commandString.indexOf(bas.cBackTickQuote, indexOfStringDelimiter + 1);
              // Additional index is:
              await loggers.consoleLog(namespacePrefix + functionName, msg.cAdditionalIndexIs + indexOfStringDelimiter);
              // Determine if it is odd or even.
              // NOTE: We start our count with 0 which would technically be our odd, then 1 should be even, but 1 is an odd number, so the logic here should actually be backwards.
              // an even value for "i" would be the odd i-th delimiter value.
              if (await ruleBroker.processRules([i.toString(), ''], isOddRule) === true) {
                // We are on the odd index, 1, 3, 5, etc...
                // odd index
                await loggers.consoleLog(namespacePrefix + functionName, msg.coddIndex);
                commandString = await ruleBroker.processRules([commandString, [indexOfStringDelimiter, bas.cTilde + bas.cBackTickQuote]], replaceCharacterAtIndexRule);
                stringLiteralCommandDelimiterAdded = true;
                // commandString after tagging an odd string delimiter:
                await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringAfterTaggingAnOddStringDelimiter + commandString);
              } else {
                // We are on the even index 2, 4, 6, etc...
                // even index
                await loggers.consoleLog(namespacePrefix + functionName, msg.cevenIndex);
                commandString = await ruleBroker.processRules([commandString, [indexOfStringDelimiter, bas.cBackTickQuote + bas.cTilde]], replaceCharacterAtIndexRule);
                stringLiteralCommandDelimiterAdded = true;
                // commandString after tagging an even string delimiter:
                await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringAfterTaggingAnEvenStringDelimiter + commandString);
              }
            }
          } // End-for (let i = 0; i < numberOfSingleQuotes; i++)
          preSplitCommandString = commandString.split(bas.cBackTickQuote);
          // Now we can check which segments of the array contain our Tilde character, since we used that to tag our single quotes.
          // And the array element that contains the Tilde tag we wil not split.
          // Ultimately everything needs to be returned as an array, make sure we trim the array elements so we don't get any empty array elements.
          // preSpitCommandString is:
          await loggers.consoleLog(namespacePrefix + functionName, msg.cpreSplitCommandStringIs + JSON.stringify(preSplitCommandString));
          for (let j = 0; j < preSplitCommandString.length; j++) {
            let preSplitCommandStringElement = preSplitCommandString[j];
            preSplitCommandStringElement = preSplitCommandStringElement.trim(); // Make sure to get rid of any white space on the ends of the string.
            let postSplitCommandString;
            if (j === 0) { // Make sure we re-initialize our return value to an array, since it was set first to a boolean value.
              returnData = [];
            }
            if (preSplitCommandStringElement.includes(bas.cTilde) === false) {
              postSplitCommandString = preSplitCommandStringElement.split(commandArgsDelimiter);
              for (const element of postSplitCommandString) {
                if (element !== '') {
                  // postSplitCommandString[k] is:
                  await loggers.consoleLog(namespacePrefix + functionName, msg.cpostSplitCommandStringIs + JSON.stringify(element));
                  returnData.push(element);
                  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
                } // End-if (postSplitCommandString[k] !== '')
              } // End-for (const element of postSplitCommandString)
            } else {
              // NOTE: We cannot just push the quoted string array back onto the array. Well we might be able to,
              // but if the last character on the last element of the returnData array is a secondaryCommandArgsDelimiter
              // then we need to just append our string to that array element, after we remove the tilde string tags,
              // and replace them with our single quotes again.
              if (returnData[returnData.length - 1].slice(-1) === secondaryCommandArgsDelimiter) {
                preSplitCommandStringElement = await ruleBroker.processRules([preSplitCommandStringElement, [/~/g, bas.cBackTickQuote]], replaceTildesWithSingleQuoteRule);
                returnData[returnData.length - 1] = returnData[returnData.length - 1] + preSplitCommandStringElement;
              } else {
                // preSplitCommandSringElement is:
                await loggers.consoleLog(namespacePrefix + functionName, msg.cpreSplitCommandStringElementIs + JSON.stringify(preSplitCommandStringElement));
                returnData.push(preSplitCommandStringElement); // Add the string now.
              }
              await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
            }
          } // End-for (let j = 0; j < preSplitCommandString.length; j++)
        } // End-if (numberOfSingleQuotes >= 2 && ruleBroker.processRules(numberOfSingleQuotes, '', isOddRule) === false)
      } // End-if (commandString.includes(bas.cBackTickQuote) === true)
      // We might need much additional logic to manage the case that the string contains multiple levels of commands with strings....in that case:
      // The command system will probably need to implement a re-assignment of the string delimiter, also using the bas.cBackTickQuote.
      // I have started to lay out some of that logic above, but we are FAR from it, and there isn't any business need for it right now.
      // So I will handle that case if & when I come to it.
    } else {
      // Doing a straight split of the commandString:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cDoingStraightSplitCommandString + commandString);
      returnData = commandString.split(commandArgsDelimiter);
      await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
    }
  } // End-if (commandString.includes(commandArgsDelimiter) === true)
  if (stringLiteralCommandDelimiterAdded === true) {
    // This means we need to remove some bas.cTilde from one or more of the command args.
    await ruleBroker.processRules([returnData, ''], [biz.cremoveStringLiteralTagsFromArray]);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function executeCommand
 * @description Takes a command string with all its associated arguments, data & meta-data.
 * This function will parse all of that out of the command lien variable that is passed in.
 * And finally pass all of that data on the execution of the actual command.
 * @param {string} commandString The command to execute along with all the associated command arguments, data & meta-data.
 * @return {array<boolean,string|integer|boolean|object|array>} An array with a boolean True or False value to
 * indicate if the application should exit or not exit, followed by the command output.
 * @author Seth Hollingsead
 * @date 2022/02/02
 */
async function executeCommand(commandString) {
  // Here we need to do all of the parsing for the command.
  // Might be a good idea to rely on business rules to do much of the parsing for us!
  // Also don't forget this is where we will need to implement the command performance
  // tracking & command results processing such as pass-fail,
  // so that when a chain of commands has completed execution we can evaluate command statistics and metrics.
  let functionName = executeCommand.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // commandString is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandStringIs + commandString);
  let returnData = [];
  let commandToExecute = await getValidCommand(commandString, await configurator.getConfigurationSetting(wrd.csystem, cfg.cprimaryCommandDelimiter));
  // commandToExecute is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandToExecuteIs + commandToExecute);
  let commandArgs = await getCommandArgs(commandString, await configurator.getConfigurationSetting(wrd.csystem, cfg.cprimaryCommandDelimiter));
  // commandArgs is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.ccommandArgsIs + commandArgs);
  let commandMetricsEnabled = await configurator.getConfigurationSetting(wrd.csystem, cfg.cenableCommandPerformanceMetrics);
  let commandStartTime = '';
  let commandEndTime = '';
  let commandDeltaTime = '';

  await loggers.consoleLog(namespacePrefix + functionName, 'commandQueue is: ' + await queue.queuePrint(sys.cCommandQueue));

  if (commandMetricsEnabled === true) {
    // Here we will capture the start time of the command we are about to execute.
    // After executing we wil capture the end time and then
    // compute the difference to determine how many milliseconds it took to run the command.
    commandStartTime = await ruleBroker.processRules([gen.cYYYYMMDD_HHmmss_SSS, ''], [biz.cgetNowMoment]);
    // Command Start time is:
    await loggers.consoleLog(namespacePrefix + functionName, msg.cCommandStartTimeIs + commandStartTime);
  } // End-if (commandMetricsEnabled === true)
  try {
    if (commandToExecute !== false && commandArgs !== false) {
      // console.log('commandToExecute is: ' + commandToExecute);
      returnData = await D[wrd.cCommands][commandToExecute](commandArgs, '');
    } else if (commandToExecute !== false && commandArgs === false) {
      // This could be a command without any arguments.
      // console.log('This could be a command without any arguments.');
      returnData = await D[wrd.cCommands][commandToExecute]('', '');
    } else {
      // This command does not exist, nothing to execute, but we don't want the application to exit.
      // An error message should have already been thrown, but we should throw another one here.
      // WARNING: Command does not exist, please enter a valid command and try again!
      console.log(msg.cexecuteCommandMessage1);
      returnData = [true, false];
    }
  } catch (err) {
    console.log(msg.cERROR_Colon + bas.cSpace + err);
    console.log(msg.cexecuteCommandMessage1);
    returnData = [true, false];
  }
  if (commandMetricsEnabled === true && commandToExecute !== '' && commandToExecute !== false) {
    let performanceTrackingObject = {};
    commandEndTime = await ruleBroker.processRules([gen.cYYYYMMDD_HHmmss_SSS, ''], [biz.cgetNowMoment]);
    // Command End time is:
    await loggers.consoleLog(namespacePrefix + functionName, msg.cCommandEndTimeIs + commandEndTime);
    // Now compute the delta tme so we know how long it took to run that command.
    commandDeltaTime = await ruleBroker.processRules([commandStartTime, commandEndTime], [biz.ccomputeDeltaTime]);
    // Command run-time is:
    await loggers.consoleLog(namespacePrefix + functionName, msg.cCommandRunTimeIs + commandDeltaTime);
    // Check to make sure the command performance tracking stack exists or does not exist.
    if (D[cfg.ccommandsPerformanceTrackingStack] === undefined) {
      await stack.initStack(cfg.ccommandsPerformanceTrackingStack);
    }
    if (D[cfg.ccommandNamesPerformanceTrackingStack] === undefined) {
      await stack.initStack(cfg.ccommandNamesPerformanceTrackingStack);
    }
    performanceTrackingObject = {Name: commandToExecute, RunTime: commandDeltaTime};
    if (await stack.contains(cfg.ccommandNamesPerformanceTrackingStack, commandToExecute) === false) {
      await stack.push(cfg.ccommandNamesPerformanceTrackingStack, commandToExecute);
    }
    await stack.push(cfg.ccommandsPerformanceTrackingStack, performanceTrackingObject);
    // stack.print(cfg.ccommandNamesPerformanceTrackingStack);
    // stack.print(cfg.ccommandsPerformanceTrackingStack);
  } // End-if (commandMetricsEnabled === true && commandToExecute !== '' && commandToExecute !== false)
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function removePluginCommands
 * @description Parses through the commands and finds the commands associated with the named plugin.
 * Then removes that data shredding it from existence at the edge of a black hole.
 * @param {string} pluginName The name of the plugin that should have its commands removed from the D-data structure.
 * @return {boolean} True or False to indicate if the removal of the data was completed successfully or not.
 * @author Seth Hollingsead
 * @date 2023/02/01
 */
async function removePluginCommands(pluginName) {
  let functionName = removePluginCommands.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  let returnData = false;
  let allCommands = D[wrd.cCommands];
  // NOTE: We are going to have to get the names of the individual commands for the plugin,
  // from the plugin constants validation for the commands,
  // then iterate over them to remove all of the plugin business rules one by one.
  let pluginConstantsValidation = D[sys.cConstantsValidationData][wrd.cPlugins][pluginName];
  let pluginConstantsValidationCommands = {};
  if (pluginConstantsValidation) {
    pluginConstantsValidationCommands = pluginConstantsValidation[sys.cpluginCommandConstantsValidation];
  } else {
    // ERROR: Constants validation data for the specified plugin was not found. Plugin:
    console.log(msg.cremovePluginBusinessRulesMessage01 + pluginName);
  }
  if (pluginConstantsValidationCommands) {
    try {
      for (const pluginCommandsRuleKey in pluginConstantsValidationCommands) {
        let pluginCommandConstValidationObject = pluginConstantsValidationCommands[pluginCommandsRuleKey];
        // pluginCommandConstValidationObject is:
        await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginCommandsConstValidationObjectIs + JSON.stringify(pluginCommandConstValidationObject));
        // Removing plugin command:
        await loggers.consoleLog(namespacePrefix + functionName, msg.cremovePluginCommandsMessage01 + pluginCommandConstValidationObject[wrd.cActual]);
        delete allCommands[pluginCommandConstValidationObject[wrd.cActual]];
      }
      returnData = true;
    } catch (err) {
      // ERROR: Failure attempting to delete the plugin commands for plugin:
      console.log(msg.cremovePluginCommandsMessage02 + pluginName);
      console.log(msg.cerrorMessage + err.message);
    }
  } else {
    // ERROR: Plugin command constants validation data for the specified plugin was not found. Plugin:
    console.log(msg.cremovePluginCommandsMessage03 + pluginName);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + returnData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function removePluginCommandAliases
 * @description Parses through the command aliases and finds the command aliases associated with the named plugin.
 * Then removes that data shredding it from existence at the edge of a black hole.
 * @param {string} pluginName The name of the plugin that should have its command aliases removed from the D-data structure.
 * @return {boolean} True or False to indicate if the removal of the data was completed successfully or not.
 * @author Seth Hollingsead
 * @date 2023/02/01
 */
async function removePluginCommandAliases(pluginName) {
  let functionName = removePluginCommandAliases.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  let returnData = false;
  let allPluginsCommandAliases = D[sys.cCommandsAliases][wrd.cPlugins];
  if (allPluginsCommandAliases) {
    try {
      delete allPluginsCommandAliases[pluginName];
      returnData = true;
    } catch (err) {
      // ERROR: Unable to remove the plugin command aliases for the specified plugin:
      console.log(msg.cremovePluginCommandAliasesMessage01 + pluginName);
      // ERROR:
      console.log(msg.cerrorMessage + err.message);
    }
  } else {
    // ERROR: Unable to locate the plugins command aliases data. Plugin: 
    console.log(msg.cremovePluginCommandAliasesMessage02 + pluginName);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + returnData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

export default {
  bootStrapCommands,
  addClientCommands,
  addPluginCommands,
  addPluginCommandAliases,
  getValidCommand,
  countMatchingCommandAlias,
  searchCommandAlias,
  getAllCommandAliasData,
  getCommandNamespaceDataObject,
  getCommandArgs,
  executeCommand,
  removePluginCommands,
  removePluginCommandAliases
};
