/* @copyright Itential, LLC 2023 */

// Set globals
/* global log */
/* eslint global-require:warn */
/* eslint import/no-dynamic-require:warn */

const fs = require('fs');
const path = require('path');

/* NodeJS internal utilities */

let adapterDir = '';
let id = '';

/*
 * INTERNAL FUNCTION: update device broker properties from service instance config and adapter sample properties
 */
function getDeviceBrokerArray(allProps) {
  const origin = `${id}-brokerHandler-getDeviceBrokerArray`;
  log.trace(origin);
  const brokerCallsArr = ['getDevice', 'getDevicesFiltered', 'isAlive', 'getConfig', 'getCount'];
  const deviceBrokerProps = allProps.devicebroker;
  let useSampleProps = false;

  try {
    const sampleFile = path.join(adapterDir, '/sampleProperties.json');
    let sampleProps = null;
    if (fs.existsSync(sampleFile)) {
      sampleProps = require(path.resolve(adapterDir, 'sampleProperties.json')).properties;
    }

    // go through all the device broker calls to get the call information
    for (let i = 0; i < brokerCallsArr.length; i += 1) {
      // If in service config that is primary - e.g do nothing
      if (!allProps.devicebroker || !allProps.devicebroker[brokerCallsArr[i]] || allProps.devicebroker[brokerCallsArr[i]].length === 0 || !allProps.devicebroker[brokerCallsArr[i]][0].path) {
        // if not in service config check sample props
        if (!sampleProps.devicebroker || !sampleProps.devicebroker[brokerCallsArr[i]] || sampleProps.devicebroker[brokerCallsArr[i]].length === 0 || !sampleProps.devicebroker[brokerCallsArr[i]][0].path) {
          // not in sample properties, nothing we can do
          log.info(`No device broker props found for ${brokerCallsArr[i]}`);
          deviceBrokerProps[brokerCallsArr[i]] = [];
        } else {
          useSampleProps = true;
          // use the sample properties information
          log.info(`Updating device broker props for ${brokerCallsArr[i]} from sample props`);
          deviceBrokerProps[brokerCallsArr[i]] = sampleProps.devicebroker[brokerCallsArr[i]];
        }
      }
    }

    // If sample properties doesn't have enabled flag, set it to false
    if (useSampleProps) {
      if (sampleProps.devicebroker.enabled === true) {
        deviceBrokerProps.enabled = sampleProps.devicebroker.enabled;
      } else {
        deviceBrokerProps.enabled = false;
      }
    }

    log.debug('Device broker array', JSON.stringify(deviceBrokerProps, null, 3));
    return deviceBrokerProps;
  } catch (ex) {
    return {};
  }
}

class BrokerHandler {
  /**
   * Adapter Broker Handler
   * @constructor
   */
  constructor(prongId, properties, directory, reqH) {
    this.myid = prongId;
    id = prongId;
    this.allProps = properties;
    adapterDir = directory;
    this.requestHandlerInst = reqH;

    // set up the properties I care about
    this.refreshProperties(properties);
  }

  /**
   * refreshProperties is used to set up all of the properties for the broker handler.
   * It allows properties to be changed later by simply calling refreshProperties rather
   * than having to restart the broker handler.
   *
   * @function refreshProperties
   * @param {Object} properties - an object containing all of the properties
   */
  refreshProperties(properties) {
    const origin = `${this.myid}-brokerHandler-refreshProperties`;
    log.trace(origin);

    if (!properties) {
      log.error(`${origin}: Broker Handler received no properties!`);
      return;
    }

    // update deviceBrokerProperties
    this.allProps.devicebroker = getDeviceBrokerArray(properties);

    // this.
  }

  /**
   * @summary Determines if this adapter supports any in a list of entities
   *
   * @function hasEntities
   * @param {String} entityType - the entity type to check for
   * @param {Array} entityList - the list of entities we are looking for
   *
   * @param {Callback} callback - A map where the entity is the key and the
   *                              value is true or false
   */
  hasEntities(entityType, entityList, callOptions, callback) {
    const origin = `${this.myid}-brokerHandler-hasEntities`;
    log.trace(origin);

    switch (entityType) {
      case 'Device':
        return this.hasDevices(entityList, callOptions, callback);
      default:
        return callback(null, `${this.myid} does not support entity ${entityType}`);
    }
  }

  /**
   * @summary Helper method for hasEntities for the specific device case
   *
   * @param {Array} deviceList - array of unique device identifiers, name
   * @param {Callback} callback - A map where the device is the key and the
   *                              value is true or false
   */
  hasDevices(deviceList, callOptions, callback) {
    const origin = `${this.myid}-brokerHandler-hasDevices`;
    log.trace(origin);

    let findings = {}; // map

    if (this.allProps.cache.enabled) { // a bit redundant but just in case errors in cacheHandler
      let entityTypeCached = false;
      this.requestHandlerInst.checkEntityTypeCached('Device', (res, error) => {
        if (error) {
          log.error(`${origin}: cache check failed with error ${error}`);
          // continue as if cache disabled, "try again"
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, origin, error, null, null, null);
          return callback(null, errorObj);
        }

        if (res) {
          entityTypeCached = true;
          // EntityType is cached. Retrieve devices from cache
          return this.requestHandlerInst.checkEntityCached('Device', deviceList, (results, err) => {
            if (err) {
              return callback(null, { code: 503, message: 'Unable to do device lookup.', err });
            }
            for (let i = 0; i < deviceList.length; i += 1) {
              if (results[i] === 'found') {
                findings[deviceList[i]] = true;
              } else { // notfound
                findings[deviceList[i]] = false;
              }
            }
            log.debug(`FINDINGS: ${JSON.stringify(findings)}`);
            return callback(findings);
          });
        }
        return null; // entity type not cached, exit callback and continue as if cache not enabled
      });
      if (entityTypeCached) {
        return null; // called callback earlier, exit function
      }
    }

    // manual - no cache
    findings = deviceList.reduce((map, device) => {
      // eslint-disable-next-line no-param-reassign
      map[device] = false;
      log.debug(`In reduce: ${JSON.stringify(map)}`);
      return map;
    }, {});
    const apiCalls = deviceList.map((device) => new Promise((resolve) => {
      this.getDevice(device, callOptions, (result, error) => {
        if (error) {
          log.debug(`In map error: ${JSON.stringify(device)}`);
          return resolve({ name: device, found: false });
        }
        log.debug(`In map: ${JSON.stringify(device)}`);
        return resolve({ name: device, found: true });
      });
    }));
    return Promise.all(apiCalls).then((results) => {
      results.forEach((device) => {
        findings[device.name] = device.found;
      });
      log.debug(`FINDINGS: ${JSON.stringify(findings)}`);
      log.debug('FINDINGS: ', findings);
      return callback(findings);
    }).catch((errors) => {
      log.error('Unable to do device lookup.');
      return callback(null, { code: 503, message: 'Unable to do device lookup.', error: errors });
    });
  }

  /**
   * @summary Get Appliance that match the deviceName
   *
   * @function getDevice
   * @param {String} deviceName - the deviceName to find (required)
   *
   * @param {getCallback} callback - a callback function to return the result
   *                                 (appliance) or the error
   */
  getDevice(deviceName, callOptions, callback) {
    const meth = 'brokerHandler-getDevice';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    // verify if enabled is set to true in properties, if it doesn't exist assume it's true
    if (this.allProps.devicebroker.enabled === false) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Device broker is not enabled in deviceBroker properties', null, null, null, null);
      log.info(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // make sure we are set up for device broker getDevice
    if (!this.allProps.devicebroker || !this.allProps.devicebroker.getDevice || this.allProps.devicebroker.getDevice.length === 0 || !this.allProps.devicebroker.getDevice[0].path) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Properties devicebroker.getDevice.path', null, null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    /* HERE IS WHERE YOU VALIDATE DATA */
    if (deviceName === undefined || deviceName === null || deviceName === '' || deviceName.length === 0) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Data', ['deviceName'], null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    try {
      // need to get the device so we can convert the deviceName to an id
      // !! if we can do a lookup by name the getDevicesFiltered may not be necessary
      const opts = {
        filter: {
          name: deviceName
        }
      };

      return this.getDevicesFiltered(opts, callOptions, (devs, ferr) => {
        // if we received an error or their is no response on the results return an error
        if (ferr) {
          return callback(null, ferr);
        }
        if (devs.list.length < 1) {
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, `Did Not Find Device ${deviceName}`, [], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        const callPromises = [];
        for (let i = 0; i < this.allProps.devicebroker.getDevice.length; i += 1) {
          // Perform component calls here.
          const callProps = this.allProps.devicebroker.getDevice[i];
          callProps.headers = {
            ...(callProps.headers || {}),
            ...(callOptions.headers || {})
          };
          callPromises.push(
            new Promise((resolve, reject) => {
              this.requestHandlerInst.iapMakeGenericCall(callOptions, 'getDevice', callProps, [devs.list[0]], [deviceName], (callRet, callErr) => {
                // return an error
                if (callErr) {
                  reject(callErr);
                } else {
                  // return the data
                  resolve(callRet);
                }
              });
            })
          );
        }

        // return an array of repsonses
        return Promise.all(callPromises).then((results) => {
          let myResult = {};
          results.forEach((result) => {
            myResult = { ...myResult, ...result };
          });

          return callback(myResult, null);
        }, (error) => {
          // return the error
          log.error(`Call to Get Device Failed ${JSON.stringify(error)}`);
          return callback(null, error);
        });
      });
    } catch (ex) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, ex);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Get Appliances that match the filter
   *
   * @function getDevicesFiltered
   * @param {Object} options - the data to use to filter the appliances (optional)
   *
   * @param {getCallback} callback - a callback function to return the result
   *                                 (appliances) or the error
   */
  getDevicesFiltered(options, callupdatedOptions, callback) {
    const meth = 'brokerHandler-getDevicesFiltered';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    // If no options are passed set to an empty object
    const updatedOptions = options || {};

    // verify if enabled is set to true in properties, if it doesn't exist assume it's true
    if (this.allProps.devicebroker.enabled === false) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Device broker is not enabled in deviceBroker properties', null, null, null, null);
      log.info(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // make sure we are set up for device broker getDevicesFiltered
    if (!this.allProps.devicebroker || !this.allProps.devicebroker.getDevicesFiltered || this.allProps.devicebroker.getDevicesFiltered.length === 0 || !this.allProps.devicebroker.getDevicesFiltered[0].path) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Properties devicebroker.getDevicesFiltered.path', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    log.debug(`Device Filter updatedOptions: ${JSON.stringify(updatedOptions)}`);

    if (!(updatedOptions.start === undefined || updatedOptions.start === null) && (updatedOptions.limit === undefined || updatedOptions.limit === null)) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Cannot specify start without limit.', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    if ((updatedOptions.start === undefined || updatedOptions.start === null) && !(updatedOptions.limit === undefined || updatedOptions.limit === null)) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Cannot specify limit without start.', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    if (typeof updatedOptions.start === 'string' || typeof updatedOptions.limit === 'string') {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Start and Limit must be numbers.', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    if (updatedOptions.start < 0 || updatedOptions.limit <= 0) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Limit and/or Start value is too low.', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // if caching get data from the cache
    if (this.allProps.cache.enabled) { // a bit redundant but just in case errors in cacheHandler
      let entityTypeCached = false;
      this.requestHandlerInst.checkEntityTypeCached('Device', (res, error) => {
        if (error) {
          log.error(`${origin}: cache check failed with error ${error}`);
          // continue as if cache disabled, "try again"
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, error, null, null, null);
          return callback(null, errorObj);
        }

        if (res) {
          entityTypeCached = true;
          // Retrieve devices from cache
          return this.requestHandlerInst.retrieveEntitiesCache('Device', updatedOptions, (callRet, callErr) => {
            if (callErr) {
              log.error(callErr);
              const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, callErr, null, null, null);
              return callback(null, errorObj);
            }
            // return the result
            return callback({ total: callRet.length, list: callRet });
          });
        }
        return null; // entity type not cached, exit callback and continue as if cache not enabled
      });
      if (entityTypeCached) {
        return null; // called callback earlier, exit function
      }
    }

    try {
      // const nextToken = updatedOptions.start;
      // const maxResults = updatedOptions.limit;

      // set up the filter of Device Names
      let filterName = [];
      if (updatedOptions && updatedOptions.filter && updatedOptions.filter.name) {
        // when this hack is removed, remove the lint ignore above
        if (Array.isArray(updatedOptions.filter.name)) {
          // eslint-disable-next-line prefer-destructuring
          filterName = updatedOptions.filter.name;
        } else {
          filterName = [updatedOptions.filter.name];
        }
      }

      const callPromises = [];
      for (let i = 0; i < this.allProps.devicebroker.getDevicesFiltered.length; i += 1) {
        // Perform component calls here.
        const callProps = this.allProps.devicebroker.getDevicesFiltered[i];
        callProps.headers = {
          ...(callProps.headers || {}),
          ...(callupdatedOptions.headers || {})
        };
        callPromises.push(
          new Promise((resolve, reject) => {
            this.requestHandlerInst.iapMakeGenericCall(callupdatedOptions, 'getDevicesFiltered', callProps, [{ fake: 'fakedata' }], filterName, (callRet, callErr) => {
              // return an error
              if (callErr) {
                console.debug(`in cache iap call failed with error ${callErr}`);
                reject(callErr);
              } else {
                // return the data
                resolve(callRet);
              }
            });
          })
        );
      }

      // return an array of repsonses
      return Promise.all(callPromises).then((results) => {
        let myResult = [];
        results.forEach((result) => {
          if (Array.isArray(result)) {
            myResult = [...myResult, ...result];
          } else if (Object.keys(result).length > 0) {
            myResult.push(result);
          }
        });

        // sort by name property
        if (updatedOptions && updatedOptions.sort) {
          myResult.sort((a, b) => {
            if (a.name > b.name) {
              return 1;
            }
            if (a.name < b.name) {
              return -1;
            }
            return 0;
          });
        }

        // pagination: get entities limit*start to limit*(start+1) - 1
        if (myResult.length !== 0 && updatedOptions && updatedOptions.start >= 0 && updatedOptions.limit > 0) {
          const end = Math.min(myResult.length, updatedOptions.limit * (updatedOptions.start + 1));
          if (updatedOptions.limit * updatedOptions.start >= end) {
            const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Page number too large for limit size.', null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
          myResult = myResult.slice(updatedOptions.limit * updatedOptions.start, end);
        }
        log.debug(`${origin}: Found #${myResult.length} devices.`);
        log.debug(`Devices: ${JSON.stringify(myResult)}`);
        return callback({ total: myResult.length, list: myResult });
      }, (error) => {
        // return the error
        log.error(`Call to get devices Failed: ${JSON.stringify(error)}`);
        return callback(null, error);
      });
    } catch (ex) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.id, meth, 'Caught Exception', null, null, null, ex);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Gets the status for the provided appliance
   *
   * @function isAlive
   * @param {String} deviceName - the deviceName of the appliance. (required)
   *
   * @param {configCallback} callback - callback function to return the result
   *                                    (appliance isAlive) or the error
   */
  isAlive(deviceName, callOptions, callback) {
    const meth = 'brokerHandler-isAlive';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    // verify if enabled is set to true in properties, if it doesn't exist assume it's true
    if (this.allProps.devicebroker.enabled === false) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Device broker is not enabled in deviceBroker properties', null, null, null, null);
      log.info(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // make sure we are set up for device broker isAlive
    if (!this.allProps.devicebroker || !this.allProps.devicebroker.isAlive || this.allProps.devicebroker.isAlive.length === 0 || !this.allProps.devicebroker.isAlive[0].path) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Properties devicebroker.isAlive.path', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // verify the required fields have been provided
    if (deviceName === undefined || deviceName === null || deviceName === '' || deviceName.length === 0) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Data', ['deviceName'], null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    try {
      // need to get the device so we can convert the deviceName to an id
      // !! if we can do a lookup by name the getDevicesFiltered may not be necessary
      const opts = {
        filter: {
          name: deviceName
        }
      };
      return this.getDevicesFiltered(opts, callOptions, (devs, ferr) => {
        // if we received an error or their is no response on the results return an error
        if (ferr) {
          return callback(null, ferr);
        }
        if (devs.list.length < 1) {
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, `Did Not Find Device ${deviceName}`, [], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        const callPromises = [];
        for (let i = 0; i < this.allProps.devicebroker.isAlive.length; i += 1) {
          // Perform component calls here.
          const callProps = this.allProps.devicebroker.isAlive[i];
          callProps.headers = {
            ...(callProps.headers || {}),
            ...(callOptions.headers || {})
          };
          callPromises.push(
            new Promise((resolve, reject) => {
              this.requestHandlerInst.iapMakeGenericCall(callOptions, 'isAlive', callProps, [devs.list[0]], null, (callRet, callErr) => {
                // return an error
                if (callErr) {
                  reject(callErr);
                } else {
                  // return the data
                  resolve(callRet);
                }
              });
            })
          );
        }

        // return an array of repsonses
        return Promise.all(callPromises).then((results) => {
          let myResult = {};
          results.forEach((result) => {
            myResult = { ...myResult, ...result };
          });

          let response = true;
          if (myResult.isAlive !== null && myResult.isAlive !== undefined && myResult.isAlive === false) {
            response = false;
          }
          return callback(response);
        }, (error) => {
          // return the error
          log.error(`Call to check device status failed: ${JSON.stringify(error)}`);
          return callback(null, error);
        });
      });
    } catch (ex) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, ex);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Gets a config for the provided Appliance
   *
   * @function getConfig
   * @param {String} deviceName - the deviceName of the appliance. (required)
   * @param {String} format - the desired format of the config. (optional)
   *
   * @param {configCallback} callback - callback function to return the result
   *                                    (appliance config) or the error
   */
  getConfig(deviceName, format, callOptions, callback) {
    const meth = 'brokerHandler-getConfig';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    // verify if enabled is set to true in properties, if it doesn't exist assume it's true
    if (this.allProps.devicebroker.enabled === false) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Device broker is not enabled in deviceBroker properties', null, null, null, null);
      log.info(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // make sure we are set up for device broker getConfig
    if (!this.allProps.devicebroker || !this.allProps.devicebroker.getConfig || this.allProps.devicebroker.getConfig.length === 0 || !this.allProps.devicebroker.getConfig[0].path) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Properties devicebroker.getConfig.path', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // verify the required fields have been provided
    if (deviceName === undefined || deviceName === null || deviceName === '' || deviceName.length === 0) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Data', ['deviceName'], null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    try {
      // need to get the device so we can convert the deviceName to an id
      // !! if we can do a lookup by name the getDevicesFiltered may not be necessary
      const opts = {
        filter: {
          name: deviceName
        }
      };
      return this.getDevicesFiltered(opts, callOptions, (devs, ferr) => {
        // if we received an error or their is no response on the results return an error
        if (ferr) {
          return callback(null, ferr);
        }
        if (devs.list.length < 1) {
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, `Did Not Find Device ${deviceName}`, [], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        const callPromises = [];
        for (let i = 0; i < this.allProps.devicebroker.getConfig.length; i += 1) {
          // Perform component calls here.
          const callProps = this.allProps.devicebroker.getConfig[i];
          callProps.headers = {
            ...(callProps.headers || {}),
            ...(callOptions.headers || {})
          };
          callPromises.push(
            new Promise((resolve, reject) => {
              this.requestHandlerInst.iapMakeGenericCall(callOptions, 'getConfig', callProps, [devs.list[0]], null, (callRet, callErr) => {
                // return an error
                if (callErr) {
                  reject(callErr);
                } else {
                  // return the data
                  resolve(callRet);
                }
              });
            })
          );
        }

        // return an array of repsonses
        return Promise.all(callPromises).then((results) => {
          let myResult = {};
          results.forEach((result) => {
            if (typeof result === 'string') {
              myResult = { ...myResult, result };
            } else if (Array.isArray(result)) {
              // myResult = result[0]; todo Commented out to resolve lint error, unsure how to fix
            } else {
              myResult = { ...myResult, ...result };
            }
          });

          // return the result
          const newResponse = {
            response: JSON.stringify(myResult, null, 2)
          };
          return callback(newResponse, null);
        }, (error) => {
          // return the error
          log.error(`Call to Get Config Failed: ${JSON.stringify(error)}`);
          return callback(null, error);
        });
      });
    } catch (ex) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, ex);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Gets the device count from the system
   *
   * @function iapGetDeviceCount
   *
   * @param {getCallback} callback - callback function to return the result
   *                                    (count) or the error
   */
  iapGetDeviceCount(callOptions, callback) {
    const meth = 'brokerHandler-iapGetDeviceCount';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    // verify if enabled is set to true in properties, if it doesn't exist assume it's true
    if (this.allProps.devicebroker.enabled === false) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Device broker is not enabled in deviceBroker properties', null, null, null, null);
      log.info(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // if caching get data from the cache
    if (this.allProps.cache.enabled) { // a bit redundant but just in case errors in cacheHandler
      let entityTypeCached = false;
      this.requestHandlerInst.checkEntityTypeCached('Device', (res, error) => {
        if (error) {
          // should never reach here
          log.error(`${origin}: cache check failed with error ${error}`);
          // continue as if cache disabled, "try again"
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, error, null, null, null);
          return callback(null, errorObj);
        }

        if (res) {
          entityTypeCached = true;
          // Retrieve devices from cache
          return this.requestHandlerInst.retrieveEntitiesCache('Device', {}, (callRet, callErr) => {
            if (callErr) {
              // MAY WANT TO FORMAT AN ERROR AND RETURN THAT (null, error)
              return callback({ count: -1 });
            }
            // return the result
            return callback({ count: callRet.length });
          });
        }
        return null; // if res === false then continue as if cache not enabled
      });
      if (entityTypeCached) {
        return null; // called callback earlier
      }
    }

    // make sure we are set up for device broker getCount
    if (!this.allProps.devicebroker || !this.allProps.devicebroker.getCount || this.allProps.devicebroker.getCount.length === 0 || !this.allProps.devicebroker.getCount[0].path) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Missing Properties devicebroker.getCount.path', null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }

    // verify the required fields have been provided
    try {
      const callPromises = [];
      for (let i = 0; i < this.allProps.devicebroker.getCount.length; i += 1) {
        // Perform component calls here.
        const callProps = this.allProps.devicebroker.getCount[i];
        callProps.headers = {
          ...(callProps.headers || {}),
          ...(callOptions.headers || {})
        };
        callPromises.push(
          new Promise((resolve, reject) => {
            this.requestHandlerInst.iapMakeGenericCall(callOptions, 'getCount', callProps, [{ fake: 'fakedata' }], null, (callRet, callErr) => {
              // return an error
              if (callErr) {
                reject(callErr);
              } else {
                // return the data
                resolve(callRet);
              }
            });
          })
        );
      }

      // return an array of repsonses
      return Promise.all(callPromises).then((results) => {
        let myResult = {};
        results.forEach((result) => {
          myResult = { ...myResult, ...result };
        });

        // return the result
        return callback({ count: Object.keys(myResult).length });
      }, (error) => {
        // return the error
        log.error(`Call to Get Device Count Failed: ${JSON.stringify(error)}`);
        return callback(null, error);
      });
    } catch (ex) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, ex);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }
}

module.exports = BrokerHandler;
