/**
 * @file chiefPlugin.js
 * @module chiefPlugin
 * @description Contains all of the functions to manage the loading, unloading, reloading,
 * registering, unregistering of plugins and plugin metaData.
 * @requires module:pluginBroker
 * @requires module:loggers
 * @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/09/02
 * @copyright Copyright © 2022-… by Seth Hollingsead. All rights reserved
 */

// Internal imports
import pluginBroker from '../brokers/pluginBroker.js';
import loggers from '../executrix/loggers.js'
import stack from '../structures/stack.js'
// External imports
import hayConst from '@haystacks/constants';
import path from 'path';

const {bas, msg, sys, wrd} = hayConst;
const baseFileName = path.basename(import.meta.url, path.extname(import.meta.url));
// controllers.chiefPlugin.
const namespacePrefix = wrd.ccontrollers + bas.cDot + baseFileName + bas.cDot;

/**
 * @function loadPluginRegistryData
 * @description Loads the plugin registry meta data which includes paths to plugins.
 * @param {string} pluginRegistryPath 
 * @return {object} A JSON object loaded from the specified path with the plugin paths and plugin metaData.
 * @author Seth Hollingsead
 * @date 2022/09/13
 */
async function loadPluginRegistryData(pluginRegistryPath) {
  let functionName = loadPluginRegistryData.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginRegistryPath
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginRegistryPathIs + pluginRegistryPath);
  let returnData = {};
  returnData = await pluginBroker.loadPluginRegistry(pluginRegistryPath);
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function persistPluginRegistryToDataStructure
 * @description Saves the plugin registry data to the D-data structure.
 * @param {object} pluginRegistryData
 * @return {boolean} True or False to indicate if the persistense was successful or not.
 * @author Seth Hollingsead
 * @date 2022/09/13
 */
async function persistPluginRegistryToDataStructure(pluginRegistryData) {
  let functionName = persistPluginRegistryToDataStructure.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginRegistryData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginRegistryDataIs + JSON.stringify(pluginRegistryData));
  let returnData = false;
  returnData = await pluginBroker.storePluginRegistryInDataStructure(pluginRegistryData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function getAllPluginsInRegistry
 * @description This is a wrapper function for pluginBroker.listPluginsInRegistry.
 * @return {array<string>} A list array of the names of the plugins in the plugin registry.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function getAllPluginsInRegistry() {
  let functionName = getAllPluginsInRegistry.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = [];
  returnData = await pluginBroker.listPluginsInRegistry();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function getAllPluginsPathsInRegistry
 * @description This is a wrapper function for pluginBroker.listPluginsPathsInRegistry
 * The purpose is to get the array of paths for all of the plugins that must be loaded.
 * @return {array<string>} A list array of all the paths of all the plugins in the plugin registry.
 * @author Seth Hollingsead
 * @date 2022/09/20
 */
async function getAllPluginsPathsInRegistry() {
  let functionName = getAllPluginsPathsInRegistry.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = [];
  returnData = await pluginBroker.listPluginsPathsInRegistry();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function getAllPluginsInRegistryPath
 * @description This is a wrapper function for pluginBroker.listPluginsInRegistryPath.
 * @return {array<string>} A list array of the names of the plugins located at the specified path on
 * the local system from the plugins registry data hive.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function getAllPluginsInRegistryPath() {
  let functionName = getAllPluginsInRegistryPath.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = [];
  returnData = await pluginBroker.listPluginsInRegistryPath()
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function countAllPluginsInRegistry
 * @description This is a wrapper function for pluginBroker.countPluginsInRegistry.
 * @return {integer} The count of the number of plugins listed in the plugin registry data hive.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function countAllPluginsInRegistry() {
  let functionName = countAllPluginsInRegistry.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = 0;
  returnData = await pluginBroker.countPluginsInRegistry();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function countAllPluginsInRegistryPath
 * @description This is a wrapper function for pluginBroker.countPluginsInRegistryPath.
 * @return {integer} The count of the number of plugin sub-folders in the plugins path listed in the plugin registry data hive.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function countAllPluginsInRegistryPath() {
  let functionName = countAllPluginsInRegistryPath.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = 0;
  returnData = await pluginBroker.countPluginsInRegistryPath();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function registerNamedPlugin
 * @description This is a wrapper function for pluginBroker.registerPlugin.
 * @param {string} pluginName The name of the plugin that should be registered. 
 * @param {string} pluginPath The path to the plugin, to be added to the plugin registry.
 * This should be the path to the plugin/package.json file, but not including the package.json as part of the path URI.
 * @return {boolean} True or False to indicate if the plugin was added to the plugin registry successfully or not.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function registerNamedPlugin(pluginName, pluginPath) {
  let functionName = registerNamedPlugin.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  // pluginPath is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginPathIs + pluginPath);
  let returnData = false;
  returnData = await pluginBroker.registerPlugin(pluginName, pluginPath);
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function unregisterNamedPlugin
 * @description This is a wrapper function for pluginBroker.unregisterPlugin.
 * @param {string} pluginName The name of the plugin that should be removed from the plugin registry.
 * @return {boolean} True or False to indicate if the plugin was removed from the plugin registry successfully or not.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function unregisterNamedPlugin(pluginName) {
  let functionName = unregisterNamedPlugin.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginName is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginNameIs + pluginName);
  let returnData = false;
  returnData = await pluginBroker.unregisterPlugin(pluginName);
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function synchronizePluginRegistryWithPath
 * @description This is a wrapper function for pluginBroker.syncPluginRegistryWithPluginRegistryPath.
 * @return {boolean} True or False to indicate if the synchronization was performed successfully or not.
 * @author Seth Hollingsead
 * @date 2022/09/15
 * @NOTE It is expected that the number of plugins loaded at any one time will not be crazy high.
 * The function that is called in the pluginBroker.syncPluginRegistryWithPluginRegistryPath will
 * do a O(n^2) brute force search,
 * if the number of plugins needed at any one time ever grows much over 100, then this solution will need to be re-evaluated!
 */
async function synchronizePluginRegistryWithPath() {
  let functionName = synchronizePluginRegistryWithPath.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = false;
  returnData = await pluginBroker.syncPluginRegistryWithPluginRegistryPath();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function clearPluginRegistry
 * @description This is a wrapper function for pluginBroker.unregisterAllPlugins.
 * @return {boolean} True or False to indicate if the plugin registry data hive was cleared successfully or not.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function clearPluginRegistry() {
  let functionName = clearPluginRegistry.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = false;
  returnData = await pluginBroker.unregisterAllPlugins();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function savePluginRegistryDisk
 * @description This is a wrapper function for pluginBroker.savePluginRegistry.
 * @return {boolean} True or False to indicate if the plugin registry was successfully saved to the disk or not.
 * @author Seth Hollingsead
 * @date 2022/09/15
 */
async function savePluginRegistryDisk() {
  let functionName = savePluginRegistryDisk.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = false;
  returnData = await pluginBroker.savePluginRegistry();
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function loadAllPluginsMetaData
 * @description Loads the plugin meta data for all of the plugins in the input plugins path array.
 * @param {array<string>} pluginsPaths
 * @return {array<object>} An array that contains all of the meta-data,
 * for each of the plugins in the input pluginsPaths array. 
 * @author Seth Hollingsead
 * @date 2022/09/02
 */
async function loadAllPluginsMetaData(pluginsPaths) {
  let functionName = loadAllPluginsMetaData.name
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginPaths are:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginsPathsAre + JSON.stringify(pluginsPaths));
  let returnData = [];
  let index = 0;
  if (pluginsPaths && pluginsPaths.length > 0) {
    for (let pluginKey in pluginsPaths) {
      // pluginKey is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginKeyIs + pluginKey);
      let pluginPath = pluginsPaths[pluginKey];
      // pluginPath is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginPathIs + pluginPath);
      returnData[index] = {};
      if (pluginPath != '') {
        returnData[index] = await pluginBroker.loadPluginMetaData(pluginPath);
      } // End-if (pluginPath != '')
      // loadedPluginMetaData at index:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cloadedPluginMetaDataAtIndex +
        index + ' data: ' + JSON.stringify(returnData[index]));
      index = index + 1;
    } // End-for (let pluginPath in pluginsPaths)
  } else {
    // ERROR: No plugin paths provided, please provide paths and try again.
    let errorMessage = msg.cErrorLoadAllPluginsMetaDataMessage01;
    console.log(errorMessage); // Make sure to output the error!
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function loadAllPluginsExecutionPaths
 * @description Extracts the path to the main entry point for each plugin and does a path join to form a
 * fully qualified path, then converts the fully qualified path to a file URI so it can be imported.
 * @param {array<object>} pluginsMetaData An array of JSON objects that contain all of the package.json data
 * loaded for each plugin that should be imported.
 * @param {array<string>} pluginsPaths An array of fully qualified paths where each plugin was loaded from,
 * used to path.join with the execution path entry point file for each plugin.
 * @return {array<string>} An array of fully qualified path URI's for each of the main entry points for
 * all of the plugins that should be loaded.
 * @author Seth Hollingsead
 * @date 2022/09/02
 */
async function loadAllPluginsExecutionPaths(pluginsMetaData, pluginsPaths) {
  let functionName = loadAllPluginsExecutionPaths.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginsMetaData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginsMetaDataIs + JSON.stringify(pluginsMetaData));
  // pluginPaths are:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginsPathsAre + JSON.stringify(pluginsPaths));
  let returnData = [];
  let index = 0;
  if (pluginsMetaData && pluginsMetaData.length > 0) {
    for (let pluginMetaDataKey in pluginsMetaData) {
      // pluginMetaDataKey is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginMetaDataKeyIs + pluginMetaDataKey);
      let pluginMetaData = pluginsMetaData[pluginMetaDataKey];
      // pluginMetaData is:
      await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginMetaDataIs + JSON.stringify(pluginMetaData));
      returnData[index] = '';
      if (pluginMetaData) {
        returnData[index] = await pluginBroker.extractAndProcessPluginEntryPointURI(pluginMetaData, pluginsPaths[index]);
        // pluginPathURI at index:
        await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginPathUriAtIndex + index + bas.cSpace + wrd.cData + bas.cColon + bas.cSpace + returnData[index]);
      }
      index = index + 1;
    } // End-for (let pluginMetaData in pluginsMetaData)
  } else {
    // ERROR: No plugin meta data was found. Please ensure the correct path is provided and try again.
    let errorMessage = msg.cErrorLoadAllPluginsExecutionPathsMessage01;
    console.log(errorMessage); // Make sure to output the error!
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function loadAllPlugins
 * @description This is a wrapper function that iterates over the list of plugin URI paths to import and calls the
 * loadPlugin function on the pluginBroker to actually load the plugin data, metaData, business rules, commands,
 * constants, constants validation, command aliases, configuration settings, debug settings and workflows.
 * @param {array<string>} pluginsExecutionPaths An array of fully qualified path URI's to be imported and executed for each plugin.
 * @param {array<object>} pluginsMetaData An array of JSON objects that contain all of the package.json data
 * loaded for each plugin that should be imported, we are passing this along here,
 * to have access to the name of each plugin as it is loaded.
 * @return {object} An object containing objects that contain all of the data and functions from each plugin,
 * these will need to be decomposed and merged with the D-data structure as appropriate.
 * @author Seth Hollingsead
 * @date 2022/09/01
 */
async function loadAllPlugins(pluginsExecutionPaths, pluginsMetaData) {
  let functionName = loadAllPlugins.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  // pluginsExecutionPaths is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginsExecutionPathsIs + JSON.stringify(pluginsExecutionPaths));
  // pluginsMetaData is:
  await loggers.consoleLog(namespacePrefix + functionName, msg.cplugnsMetaDataIs + JSON.stringify(pluginsMetaData));
  let returnData = {};
  let index = 0;
  if (pluginsExecutionPaths && pluginsMetaData && pluginsExecutionPaths.length > 0 && pluginsMetaData.length > 0) {
    stack.initStack(sys.cpluginsLoaded);
    for (let pluginExecutionPathKey in pluginsExecutionPaths) {
      let pluginExecutionPath = pluginsExecutionPaths[pluginExecutionPathKey];
      let pluginMetaData = pluginsMetaData[index];
      if (pluginExecutionPath && pluginMetaData) {
        // pluginExectuionPath is:
        await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginExecutionPathIs + pluginExecutionPath);
        // pluginMetaData is:
        await loggers.consoleLog(namespacePrefix + functionName, msg.cpluginMetaDataIs + JSON.stringify(pluginMetaData));
        returnData[pluginMetaData[wrd.cname]] = {}; // Initialize the data structure
        // Load the data and add it.
        try {
          returnData[pluginMetaData[wrd.cname]] = await pluginBroker.loadPlugin(pluginExecutionPath);
        } catch (err) {
          // Failed to load the plugin:
          console.log(msg.cERROR_Colon + namespacePrefix + functionName + msg.cloadAllPluginsMessage01 + pluginMetaData[wrd.cname]);
          // plugin entry point path:
          console.log(msg.cloadAllPluginsMessage02 + pluginExecutionPath);
          console.log(msg.cERROR_Colon + err);
          stack.push(sys.cpluginsLoaded, [pluginMetaData[wrd.cname], false]);
        }
        // Push to a stack that we have successfully loaded the currently named plugin,
        // so that all the plugins loaded can be verified.
        stack.push(sys.cpluginsLoaded, [pluginMetaData[wrd.cname], true]);
      } else {
        // Failed to load the plugin:
        console.log(msg.cERROR_Colon + namespacePrefix + functionName + msg.cloadAllPluginsMessage01 + pluginMetaData[wrd.cname]);
        // plugin entry point path:
        console.log(msg.cloadAllPluginsMessage02 + pluginExecutionPath);
        stack.push(sys.cpluginsLoaded, [pluginMetaData[wrd.cname], false]);
      }
    } // End-for (let pluginExecutionPath in pluginsExecutionPaths)
  } else {
    // ERROR: No plugin execution paths or plugins metaData was specified:
    console.log(msg.cloadAllPluginsMessage03 + namespacePrefix + functionName);
  }
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + JSON.stringify(returnData));
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

/**
 * @function verifyAllPluginsLoaded
 * @description Examins the pluginsLoaded stack to confirm that none of the plugins which
 * were supposed to have been loaded failed to load.
 * @return {boolean} True or False to indicate if all of the plugins were loaded successfully or not.
 * @author Seth Hollingsead
 * @date 2022/09/06
 */
 async function verifyAllPluginsLoaded() {
  let functionName = verifyAllPluginsLoaded.name;
  await loggers.consoleLog(namespacePrefix + functionName, msg.cBEGIN_Function);
  let returnData = false;
  let foundFailedPlugin = false;
  let stackLength = stack.length(sys.cpluginsLoaded);
  for (let i = 0; i <= stackLength; i++) {
    let pluginLoadedObject = stack.pop(sys.cpluginsLoaded);
    if (Array.isArray(pluginLoadedObject)) {
      if (pluginLoadedObject[1] === false) {
        foundFailedPlugin = true;
      }
    } // End-if (Array.isArray(pluginLoadedObject))
  } // End-for (let i = 0; i <= stackLength; i++)
  returnData = !foundFailedPlugin; // Invert the logic
  await loggers.consoleLog(namespacePrefix + functionName, msg.creturnDataIs + returnData);
  await loggers.consoleLog(namespacePrefix + functionName, msg.cEND_Function);
  return returnData;
}

export default {
  loadPluginRegistryData,
  persistPluginRegistryToDataStructure,
  getAllPluginsInRegistry,
  getAllPluginsPathsInRegistry,
  getAllPluginsInRegistryPath,
  countAllPluginsInRegistry,
  countAllPluginsInRegistryPath,
  registerNamedPlugin,
  unregisterNamedPlugin,
  synchronizePluginRegistryWithPath,
  clearPluginRegistry,
  savePluginRegistryDisk,
  loadAllPluginsMetaData,
  loadAllPluginsExecutionPaths,
  loadAllPlugins,
  verifyAllPluginsLoaded
};