/* eslint-disable no-param-reassign */
/* @copyright Itential, LLC 2018 */

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

/* NodeJS internal utilities */
const fs = require('fs');
const path = require('path');
const jsonQuery = require('json-query');

// The schema validator
const AjvCl = require('ajv');
const { request } = require('http');

/* Fetch in the other needed components for the this Class */
const RestHandlerCl = require(path.join(__dirname, '/restHandler.js'));
const CacheHandlerCl = require(path.join(__dirname, '/cacheHandler.js'));
const BrokerHandlerCl = require(path.join(__dirname, '/brokerHandler.js'));
const GenericHandlerCl = require(path.join(__dirname, '/genericHandler.js'));
const ConnectorCl = require(path.join(__dirname, '/connectorRest.js'));
const PropUtilCl = require(path.join(__dirname, '/propertyUtil.js'));
const TransUtilCl = require(path.join(__dirname, '/translatorUtil.js'));
const DBUtilCl = require(path.join(__dirname, '/dbUtil.js'));
const AuthHandlerCl = require(path.join(__dirname, '/authenticationHandler.js'));

let id = null;
const allowFailover = 'AD.300';
const noFailover = 'AD.500';
let dbUtilInst = null;
let propUtilInst = null;
let transUtilInst = null;
const NS_PER_SEC = 1e9;
let username = null;
let healthcheckpath = null;

// INTERNAL FUNCTIONS
/**
 * @summary Validate the properties have been provided for the libraries
 *
 * @function validateProperties
 * @param {String} entityName - the name of the entity (required)
 * @param {String} actionName - the name of the action to take (required)
 *
 * @return {Object} entitySchema - the entity schema object
 */
function validateProperties(properties) {
  const origin = `${id}-requestHandler-validateProperties`;
  log.trace(origin);

  try {
    // get the path for the specific action file
    const propertyFile = path.join(__dirname, '/../schemas/propertiesSchema.json');

    // Read the action from the file system
    const propertySchema = JSON.parse(fs.readFileSync(propertyFile, 'utf-8'));

    // add any defaults to the data
    const combinedProps = propUtilInst.mergeProperties(properties, propUtilInst.setDefaults(propertySchema));

    // validate the entity against the schema
    const ajvInst = new AjvCl({ strictSchema: false, allowUnionTypes: true });
    const validate = ajvInst.compile(propertySchema);
    const result = validate(combinedProps);

    log.info(`request is ${request}`); // for lint

    // if invalid properties throw an error
    if (!result) {
      // create the generic part of an error object
      const errorObj = {
        origin,
        type: 'Schema Validation Failure',
        vars: [validate.errors[0].message]
      };

      // log and throw the error
      log.trace(`${origin}: Schema validation failure ${validate.errors[0].message}`);
      throw new Error(JSON.stringify(errorObj));
    }

    // need to decode/decrypt the static token if it is encoded/encrypted
    if (combinedProps.authentication && combinedProps.authentication.token
      && (combinedProps.authentication.token.indexOf('{code}') === 0
        || combinedProps.authentication.token.indexOf('{crypt}') === 0)) {
      combinedProps.authentication.token = propUtilInst.decryptProperty(combinedProps.authentication.token);
    }

    // need to decode/decrypt the password if it is encoded/encrypted
    if (combinedProps.authentication && combinedProps.authentication.password
      && (combinedProps.authentication.password.indexOf('{code}') === 0
        || combinedProps.authentication.password.indexOf('{crypt}') === 0)) {
      combinedProps.authentication.password = propUtilInst.decryptProperty(combinedProps.authentication.password);
    }

    // return the resulting properties --- add any necessary defaults
    return combinedProps;
  } catch (e) {
    return transUtilInst.checkAndThrow(e, origin, 'Issue validating properties');
  }
}

/**
 * @summary Walk through the entities and make sure they have an action file
 * and that file format is validated against actionSchema.json
 *
 * @function walkThroughActionFiles
 * @param {String} directory - the directory for the adapter (required)
 */
function walkThroughActionFiles(directory) {
  const origin = `${id}-requestHandler-walkThroughActionFiles`;
  log.trace(origin);
  const clean = [];

  try {
    // Read the action schema from the file system
    const actionSchemaFile = path.join(__dirname, '/../schemas/actionSchema.json');
    const actionSchema = JSON.parse(fs.readFileSync(actionSchemaFile, 'utf-8'));
    const entitydir = `${directory}/entities`;

    // if there is no entity directory - return
    if (!fs.existsSync(directory) || !fs.existsSync(entitydir)) {
      return clean;
    }

    // if there is an entity directory
    if (fs.statSync(directory).isDirectory() && fs.statSync(entitydir).isDirectory()) {
      const entities = fs.readdirSync(entitydir);

      // need to go through each entity in the entities directory
      for (let e = 0; e < entities.length; e += 1) {
        // make sure the entity is a directory - do not care about extra files
        // only entities (dir)
        if (fs.statSync(`${entitydir}/${entities[e]}`).isDirectory()) {
          // see if the action file exists in the entity
          if (fs.existsSync(`${entitydir}/${entities[e]}/action.json`)) {
            // Read the entity actions from the file system
            const actions = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/action.json`, 'utf-8'));

            // add any defaults to the data
            const defActions = propUtilInst.setDefaults(actionSchema);
            const allActions = propUtilInst.mergeProperties(actions, defActions);

            // validate the entity against the schema
            const ajvInst = new AjvCl({ strictSchema: false, allowUnionTypes: true });
            const validate = ajvInst.compile(actionSchema);
            const result = validate(allActions);

            // if invalid properties throw an error
            if (!result) {
              // Get the action and component from the first ajv error
              let action = 'checkErrorDetails';
              let component = 'checkErrorDetails';
              const temp = validate.errors[0].dataPath;
              const actStInd = temp.indexOf('[');
              const actEndInd = temp.indexOf(']');
              // if we have indexes for the action we can get specifics
              if (actStInd >= 0 && actEndInd > actStInd) {
                const actNum = temp.substring(actStInd + 1, actEndInd);
                // get the action name from the number
                if (actions.actions.length >= actNum) {
                  action = actions.actions[actNum].name;
                }
                // get the component that failed for the action
                if (temp.length > actEndInd + 1) {
                  component = temp.substring(actEndInd + 2);
                }
              }
              let msg = `${origin}: Error on validation of actions for entity `;
              msg += `${entities[e]} - ${action} - ${component}   Details: ${JSON.stringify(validate.errors)}`;
              clean.push(msg);
              log.warn(msg);
            }

            for (let a = 0; a < actions.actions.length; a += 1) {
              const act = actions.actions[a];
              let reqSchema = null;
              let respSchema = null;

              // Check that the request schema file defined for the action exist
              if (act.requestSchema) {
                if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.requestSchema}`)) {
                  let msg = `${origin}: Error on validation of actions for entity `;
                  msg += `${entities[e]}: ${act.name} - missing request schema file - ${act.requestSchema}`;
                  clean.push(msg);
                  log.warn(msg);
                } else {
                  reqSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.requestSchema}`, 'utf-8'));
                }
              } else if (act.schema && fs.existsSync(`${entitydir}/${entities[e]}/${act.schema}`)) {
                reqSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.schema}`, 'utf-8'));
              } else {
                let msg = `${origin}: Error on validation of actions for entity `;
                msg += `${entities[e]}: ${act.name} - missing request schema file - ${act.schema}`;
                clean.push(msg);
                log.warn(msg);
              }

              // Check that the response schema file defined for the action exist
              if (act.responseSchema) {
                if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.responseSchema}`)) {
                  let msg = `${origin}: Error on validation of actions for entity `;
                  msg += `${entities[e]}: ${act.name} - missing response schema file - ${act.responseSchema}`;
                  clean.push(msg);
                  log.warn(msg);
                } else {
                  respSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.responseSchema}`, 'utf-8'));
                }
              } else if (act.schema && fs.existsSync(`${entitydir}/${entities[e]}/${act.schema}`)) {
                respSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.schema}`, 'utf-8'));
              } else {
                let msg = `${origin}: Error on validation of actions for entity `;
                msg += `${entities[e]}: ${act.name} - missing response schema file - ${act.schema}`;
                clean.push(msg);
                log.warn(msg);
              }

              // check that the action is in the schemas
              if (!reqSchema || !reqSchema.properties || !reqSchema.properties.ph_request_type
                || !reqSchema.properties.ph_request_type.enum || !reqSchema.properties.ph_request_type.enum.includes(act.name)) {
                let msg = `${origin}: Error on validation of actions for entity `;
                msg += `${entities[e]}: ${act.name} - missing from ph_request_type in request schema`;
                clean.push(msg);
                log.warn(msg);
              }
              if (!respSchema || !respSchema.properties || !respSchema.properties.ph_request_type
                || !respSchema.properties.ph_request_type.enum || !respSchema.properties.ph_request_type.enum.includes(act.name)) {
                let msg = `${origin}: Error on validation of actions for entity `;
                msg += `${entities[e]}: ${act.name} - missing from ph_request_type in response schema`;
                clean.push(msg);
                log.warn(msg);
              }

              // check that the mock data files exist
              if (act.responseObjects) {
                for (let m = 0; m < act.responseObjects.length; m += 1) {
                  if (act.responseObjects[m].mockFile) {
                    if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.responseObjects[m].mockFile}`)) {
                      let msg = `${origin}: Error on validation of actions for entity `;
                      msg += `${entities[e]}: ${act} - missing mock data file - ${act.responseObjects[m].mockFile}`;
                      clean.push(msg);
                      log.warn(msg);
                    }
                  }
                }
              }
            }
          } else {
            log.warn(`${origin}: Entity ${entities[e]} missing action file.`);
            clean.push(`${origin}: Entity ${entities[e]} missing action file.`);
          }
        } else {
          log.warn(`${origin}: Entity ${entities[e]} missing entity directory.`);
          clean.push(`${origin}: Entity ${entities[e]} missing entity directory.`);
        }
      }
    }

    return clean;
  } catch (e) {
    return transUtilInst.checkAndThrow(e, origin, 'Issue validating actions');
  }
}

/**
 * @summary Method for metric db calls and tests
 *
 * @function dbCalls
 * @param {String} entity - the entity to use. (required)
 * @param {String} action - the action to use. (required)
 * @param {Object} data - anything the user provides goes here. Possible tags:
 * data = {
 *  code : <Number or String>,
 *  numRetries : <Number>,
 *  numRedirects : <Number>,
 *  isThrottling : <Boolean>,
 *  timeouts : <Number>,
 *  queueTime : <Number>,
 *  capabilityTime : <Number>,
 *  tripTime : <Number>,
 *  overallEnd : <Number>
 * }
 *
 */
function dbCalls(collectionName, entity, action, data, callback) {
  try {
    // template_entity.createEntity logs a 201 response code which we don't track so code count doesn't show up in db.
    const filter = { entity, action }; // hypothetically each doc has unique entity+action.
    const edits = {
      $inc: {
        num_called: (data.code && (data.tripTime || data.adapterTime || data.capabilityTime)) ? 1 : 0,
        numRetries: data.retries || 0,
        numRedirects: data.redirects || 0,
        throttleCount: (data.queueTime) ? 1 : 0, // separate thing to keep count of # throttles. may not be necessary.
        timeouts: data.timeouts || 0,
        tot_queue_time: parseFloat(data.queueTime) || 0,
        tot_rnd_trip: parseFloat(data.tripTime) || 0, // = tot_rnd_trip
        tot_library: parseFloat(data.capabilityTime) || 0, // = tot_library, overall things
        tot_overall: parseFloat(data.overallTime) || 0, // = tot_library, overall things
        ['results.'.concat(data.code)]: 1 // Note: this results in the JSON recording differing from DB record: JSON doesn't make a nested document, just "results.xxx".
        // adapterTime: 0 // not accessible in this file, go to adapter.js later.tripTime
      },
      $set: {
        time_units: 'ms',
        entity,
        action,
        isThrottling: data.isThrottling
      },
      metric: {
        entity,
        action
      }
    };
    // were are we writing? fs or db
    return dbUtilInst.findAndModify(collectionName, filter, null, edits, true, null, true, (err, dbres) => {
      if (err && !dbres) {
        return callback(false);
      }
      return callback(true);
    });
  } catch (e) {
    return callback(false);
  }
}

/*
 * INTERNAL FUNCTION: get data from source(s) - nested
 */
function getDataFromSources(loopField, sources, props) {
  let fieldValue = loopField;
  let foundProp = false;
  // go through the sources to find the field
  for (let s = 0; s < sources.length; s += 1) {
    // find the field value using jsonquery
    const nestedValue = jsonQuery(loopField, { data: sources[s] }).value;

    // if we found in source - set and no need to check other sources
    if (nestedValue && Object.keys(nestedValue).length !== 0) {
      fieldValue = nestedValue;
      foundProp = true;
      break;
    }
  }

  // Check for field in adapter properties
  if (!foundProp && props && props[loopField]) {
    fieldValue = props[loopField];
    foundProp = true;
  }

  if (foundProp) {
    return fieldValue;
  }
  return null;
}

/**
 * @summary  Extracts the keys to be replaced in request fields or response fields.
 *
 * @function extractKeysFromBraces
 * @param {String} value - String in which to look for {} and extract values
 *
 * @return {Array} Array containing 0 or more keys found in input String
 */
function extractKeysFromBraces(value) {
  if (Number.isInteger(value)) {
    value = value.toString();
  }
  const regex = /\{(.*?)\}/g;
  const matches = value.match(regex);
  if (matches) {
    return matches.map((key) => key.replace(/{|}/g, ''));
  }
  return [];
}

/**
 * @summary  Given a string containing a logical OR in braces {||}, return the LHS if value is found
 * in sources, otherwise return the RHS
 *
 * @function setConditionalValue
 * @param {String} valueField - String containing operands
 * @param {Array} source - Array containing sources to search for operand values
 * @param {Object} props - Object containing Adapter props as secondary source
 *
 * @return {String} Final value with data replaced in found keys.
 */
function setConditionalValue(valueField, sources, props) {
  if (!valueField.includes('{||}')) {
    throw new Error('This method is only to be used with Strings containing a logical OR in braces {||}');
  }
  const operands = valueField.split('{||}'); // Array of left side and right side
  const operandA = extractKeysFromBraces(operands[0]); // [name]
  const operandB = extractKeysFromBraces(operands[1]); // [serial]
  let finalValue = operands[0]; // {name}
  for (let i = 0; i < operandA.length; i += 1) {
    const fieldValue = getDataFromSources(operandA[i], sources, props);
    if (fieldValue === operandA[i] || fieldValue === null) {
      // did not find value - break here to try second part of conditional
      [, finalValue] = operands;
      break;
    }
    finalValue = finalValue.replace(`{${operandA[i]}}`, fieldValue);
  }

  // Check if broke out of loop due to missing value
  if (finalValue !== operands[1]) {
    return finalValue;
  }

  // Look for values for keys to the right of {||}
  for (let j = 0; j < operandB.length; j += 1) {
    const fieldValue = getDataFromSources(operandB[j], sources, props);
    if (fieldValue === operandB[j] || fieldValue === null) {
      throw new Error(`Could not find value in sources for ${operandA} or ${operandB}`);
    }
    finalValue = finalValue.replace(`{${operandB[j]}}`, fieldValue);
  }
  return finalValue;
}

function setResponseDataFromSources(loopField, sources, props) {
  if (loopField.includes('{||}')) {
    return setConditionalValue(loopField, sources, props);
  }
  let myField = loopField;
  const keys = extractKeysFromBraces(loopField);
  // Handle if val not found ?
  for (let k = 0; k < keys.length; k += 1) {
    const responseKey = keys[k];
    const fieldValue = getDataFromSources(responseKey, sources, props);
    if (fieldValue) {
      myField = myField.replace(`{${responseKey}}`, fieldValue);
    }
  }
  return myField;
}

class RequestHandler {
  /**
   * Request Handler
   * @constructor
   */
  constructor(prongId, properties, directory) {
    try {
      this.myid = prongId;
      id = prongId;
      this.props = properties;
      this.clean = [];
      this.directory = directory;
      this.suspend = false;
      this.suspendInterval = 60000;

      // need the db utilities before validation
      this.dbUtil = new DBUtilCl(this.myid, properties, directory);
      dbUtilInst = this.dbUtil;

      // need the property utilities before validation
      this.propUtil = new PropUtilCl(this.myid, directory, this.dbUtil);
      propUtilInst = this.propUtil;

      // reference to the needed classes for specific protocol handlers
      this.transUtil = new TransUtilCl(prongId, this.propUtil);
      transUtilInst = this.transUtil;

      this.authHandler = new AuthHandlerCl(this.myid, this.props, this);
      // validate the action files for the adapter
      this.clean = walkThroughActionFiles(this.directory);

      // save the adapter base directory
      this.adapterBaseDir = directory;

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

      // instantiate other runtime components
      this.connector = new ConnectorCl(this.myid, this.props, this.transUtil, this.propUtil, this.dbUtil, this.authHandler);
      this.restHandler = new RestHandlerCl(this.myid, this.props, this.connector, this.transUtil);
      this.brokerHandler = new BrokerHandlerCl(this.myid, this.props, this.directory, this);
      this.genericHandler = new GenericHandlerCl(this.myid, this.props, this);
      this.cacheHandler = new CacheHandlerCl(this.myid, this.props, this.directory, this);
    } catch (e) {
      // handle any exception
      const origin = `${this.myid}-requestHandler-constructor`;
      this.transUtil.checkAndThrow(e, origin, 'Could not start Adapter Runtime Library');
    }
  }

  /**
   * @callback Callback
   * @param {Object} result - the result of the get request
   * @param {String} error - any error that occured
   */

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

    try {
      // validate the properties that came in against library property schema
      this.props = validateProperties(properties);
      // get the list of failover codes
      this.failoverCodes = [];

      if (this.props.request && this.props.request.failover_codes
        && Array.isArray(this.props.request.failover_codes)) {
        this.failoverCodes = this.props.request.failover_codes;
      }

      this.saveMetric = this.props.save_metric || false;

      // set the username (required - default is null)
      if (typeof this.props.authentication.username === 'string') {
        username = this.props.authentication.username;
      }

      // set the healthcheck path (required - default is null)
      if (this.props.healthcheck) {
        if (typeof this.props.healthcheck.URI_Path === 'string') {
          healthcheckpath = this.props.healthcheck.URI_Path;
        }
      }

      // if this is truly a refresh and we have a connector or rest handler, refresh them
      if (this.connector) {
        this.connector.refreshProperties(properties);
      }
      if (this.restHandler) {
        this.restHandler.refreshProperties(properties);
      }
      if (this.cacheHandler) {
        this.cacheHandler.refreshProperties(properties);
      }
      if (this.brokerHandler) {
        this.brokerHandler.refreshProperties(properties);
      }
      if (this.genericHandler) {
        this.genericHandler.refreshProperties(properties);
      }
    } catch (e) {
      // handle any exception
      return this.transUtil.checkAndThrow(e, origin, 'Properties may not have been updated properly');
    }
  }

  /**
   * checkActionFiles is used to update the validation of the action files.
   *
   * @function checkActionFiles
   */
  checkActionFiles() {
    const origin = `${this.myid}-requestHandler-checkActionFiles`;
    log.trace(origin);

    try {
      // validate the action files for the adapter
      this.clean = walkThroughActionFiles(this.directory);
      return this.clean;
    } catch (e) {
      return ['Exception increase log level'];
    }
  }

  /**
   * checkProperties is used to validate the adapter properties.
   *
   * @function checkProperties
   * @param {Object} properties - an object containing all of the properties
   */
  checkProperties(properties) {
    const origin = `${this.myid}-requestHandler-checkProperties`;
    log.trace(origin);

    try {
      // validate the action files for the adapter
      this.testPropResult = validateProperties(properties);
      return this.testPropResult;
    } catch (e) {
      return { exception: 'Exception increase log level' };
    }
  }

  /**
   * exposeDB is used to update the adapter metrics with the overall adapter time
   *
   * @function exposeDB
   * @param {String} entity - the name of the entity for this request.
   *                          (required)
   * @param {String} action - the name of the action being executed. (required)
   * @param {String} overallTime - the overall time in milliseconds (required)
   */
  exposeDB(entity, action, overallTime) {
    const origin = `${this.myid}-requestHandler-exposeDB`;
    log.trace(origin);

    try {
      // only allow the adapter.js to update the overallTime
      const allowedData = {
        overallTime
      };
      dbCalls('metrics', entity, action, allowedData, (status) => {
        log.trace(`${origin}: ${status}`);
      });

      return true;
    } catch (e) {
      return false;
    }
  }

  /**
   * @summary Make the provided call(s) - could be one of many
   *
   * @function iapMakeGenericCall
   * @param {object} metadata - metadata for the call (optional).
   * @param {string} callName - the name of the call (required)
   * @param {object} callProps - the proeprties for the broker call (required)
   * @param {object} devResp - the device details to extract needed inputs (required)
   * @param {string} filterName - any filter to search on (required)
   *
   * @param {getCallback} callback - a callback function to return the result of the call
   */
  iapMakeGenericCall(metadata, callName, callProps, devResp, filterName, callback) {
    const meth = 'requestHandler-iapMakeGenericCall';
    const origin = `${this.myid}-${meth}`;
    log.trace(`${origin}: ${callName}`);

    try {
      let uriPath = '';
      let uriMethod = 'GET';
      let callQuery = {};
      let callBody = {};
      let callHeaders = {};
      let handleFail = 'fail';
      let ostypePrefix = '';
      let statusValue = 'true';
      const sources = devResp.concat([callProps.requestFields]);
      if (callProps.path) {
        uriPath = `${callProps.path}`;
        // make any necessary changes to the path
        if (devResp !== null) {
          // get the field from the provided device
          const pathKeys = extractKeysFromBraces(uriPath);
          for (let pathKey = 0; pathKey < pathKeys.length; pathKey += 1) {
            const fieldValue = getDataFromSources(pathKeys[pathKey], sources, this.props);
            if (fieldValue === null) {
              return callback(null, `Value for path key {${pathKeys[pathKey]}} not found`);
            }
            uriPath = uriPath.replace(`{${pathKeys[pathKey]}}`, fieldValue);
          }
        }
      }
      if (callProps.method) {
        uriMethod = callProps.method;
      }
      if (callProps.query) {
        callQuery = { ...callProps.query };
        // go through the query params to check for variable values
        const cpKeys = Object.keys(callQuery);
        for (let cp = 0; cp < cpKeys.length; cp += 1) {
          // get array of values to replace
          const matches = extractKeysFromBraces(callQuery[cpKeys[cp]]);
          for (let m = 0; m < matches.length; m += 1) {
            const queryKey = matches[m];
            const fieldValue = getDataFromSources(queryKey, sources, this.props);
            if (fieldValue === null) {
              return callback(null, `Value for query key {${queryKey}} not found`);
            }
            callQuery[cpKeys[cp]] = callQuery[cpKeys[cp]].replace(`{${queryKey}}`, fieldValue);
          }
        }
      }
      if (callProps.body) {
        callBody = { ...callProps.body };
        // go through the body fields to check for variable values
        const cbKeys = Object.keys(callBody);
        for (let cb = 0; cb < cbKeys.length; cb += 1) {
          const matches = extractKeysFromBraces(callBody[cbKeys[cb]]);
          for (let m = 0; m < matches.length; m += 1) {
            // make any necessary changes to the query params
            const bodyKey = matches[m];
            const fieldValue = getDataFromSources(bodyKey, sources, this.props);
            if (fieldValue === null) {
              return callback(null, `Value for body key {${bodyKey}} not found`);
            }
            callBody[cbKeys[cb]] = callBody[cbKeys[cb]].replace(`{${bodyKey}}`, fieldValue);
          }
        }
      }
      if (callProps.headers) {
        callHeaders = { ...callProps.headers };
        // go through the header fields to check for variable values
        const chKeys = Object.keys(callHeaders);
        for (let ch = 0; ch < chKeys.length; ch += 1) {
          const matches = extractKeysFromBraces(callHeaders[chKeys[ch]]);
          for (let m = 0; m < matches.length; m += 1) {
            const headerKey = matches[m];
            // make any necessary changes to the query params
            const fieldValue = getDataFromSources(headerKey, sources, this.props);
            if (fieldValue === null) {
              return callback(null, `Value for header key {${headerKey}} not found`);
            }
            callHeaders[chKeys[ch]] = callQuery[chKeys[ch]].replace(`{${headerKey}}`, fieldValue);
          }
        }
      }
      if (callProps.handleFailure) {
        handleFail = callProps.handleFailure;
      }
      if (callProps.responseFields && callProps.responseFields.ostypePrefix) {
        ostypePrefix = callProps.responseFields.ostypePrefix;
      }
      if (callProps.responseFields && callProps.responseFields.statusValue) {
        statusValue = callProps.responseFields.statusValue;
      }

      // !! using Generic makes it easier on the Adapter Builder (just need to change the path)
      // !! you can also replace with a specific call if that is easier
      if (callProps.pagination) {
        metadata.pagination = callProps.pagination;
        metadata.pagination.responseDatakey = callProps.responseDatakey || '';
      }
      return this.expandedGenericAdapterRequest(metadata, uriPath, uriMethod, null, callQuery, callBody, callHeaders, (result, error) => {
        // if we received an error or their is no response on the results return an error
        if (error) {
          if (this.props.stub && error.icode === 'AD.301') {
            return callback(null, error);
          }
          if (handleFail === 'fail') {
            return callback(null, error);
          }
          return callback({}, null);
        }
        if (!result.response) {
          if (handleFail === 'fail') {
            const errorObj = this.formatErrorObject(this.myid, meth, 'Invalid Response', [callName], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
          return callback({}, null);
        }

        // get the response piece we care about from the response
        const myResult = result;
        if (callProps.responseDatakey) {
          myResult.response = jsonQuery(callProps.responseDatakey, { data: myResult.response }).value;
        }

        // get the keys for the response fields
        let rfKeys = [];
        if (callProps.responseFields && Object.keys(callProps.responseFields).length > 0) {
          rfKeys = Object.keys(callProps.responseFields);
        }

        // if we got an array returned (e.g. getDevicesFitered)
        if (Array.isArray(myResult.response)) {
          const listDevices = [];
          for (let a = 0; a < myResult.response.length; a += 1) {
            const thisDevice = myResult.response[a];
            for (let rf = 0; rf < rfKeys.length; rf += 1) {
              if (rfKeys[rf] !== 'ostypePrefix') {
                // devResp removed due to conditional issues
                let fieldValue = setResponseDataFromSources(callProps.responseFields[rfKeys[rf]], [thisDevice, callProps.requestFields]);
                // if the field is ostype - need to add prefix
                if (rfKeys[rf] === 'ostype' && typeof fieldValue === 'string') {
                  fieldValue = ostypePrefix + fieldValue;
                }
                // if there is a status to set, set it
                if (rfKeys[rf] === 'status') {
                  // if really looking for just a good response
                  if (callProps.responseFields[rfKeys[rf]] === 'return2xx' && myResult.icode === statusValue.toString()) {
                    thisDevice.isAlive = true;
                  } else if (fieldValue.toString() === statusValue.toString()) {
                    thisDevice.isAlive = true;
                  } else {
                    thisDevice.isAlive = false;
                  }
                }
                // if we found a good value
                thisDevice[rfKeys[rf]] = fieldValue;
              }
            }

            // if there is no filter - add the device to the list
            if (!filterName || filterName.length === 0) {
              listDevices.push(thisDevice);
            } else {
              // if we have to match a filter
              let found = false;
              for (let f = 0; f < filterName.length; f += 1) {
                if (thisDevice.name.indexOf(filterName[f]) >= 0) {
                  found = true;
                  break;
                }
              }
              // matching device
              if (found) {
                listDevices.push(thisDevice);
              }
            }
          }

          // return the array of devices
          return callback(listDevices, null);
        }

        // if this is not an array - just about everything else, just handle as a single object
        let thisDevice = myResult.response;
        for (let rf = 0; rf < rfKeys.length; rf += 1) {
          // skip ostypePrefix since it is not a field
          if (rfKeys[rf] !== 'ostypePrefix') {
            let fieldValue = setResponseDataFromSources(callProps.responseFields[rfKeys[rf]], [thisDevice, devResp, callProps.requestFields]);
            // if the field is ostype - need to add prefix
            if (rfKeys[rf] === 'ostype' && typeof fieldValue === 'string') {
              fieldValue = ostypePrefix + fieldValue;
            }
            // if there is a status to set, set it
            if (rfKeys[rf] === 'status') {
              // if really looking for just a good response
              if (callProps.responseFields[rfKeys[rf]] === 'return2xx' && myResult.icode === statusValue.toString()) {
                thisDevice.isAlive = true;
              } else if (fieldValue.toString() === statusValue.toString()) {
                thisDevice.isAlive = true;
              } else {
                thisDevice.isAlive = false;
              }
            }
            // if we found a good value
            thisDevice[rfKeys[rf]] = fieldValue;
          }
        }

        // if there is a filter - check the device is in the list
        if (filterName && filterName.length > 0) {
          let found = false;
          for (let f = 0; f < filterName.length; f += 1) {
            if (thisDevice.name.indexOf(filterName[f]) >= 0) {
              found = true;
              break;
            }
          }
          // no matching device - clear the device
          if (!found) {
            thisDevice = {};
          }
        }

        return callback(thisDevice, null);
      });
    } catch (e) {
      const errorObj = this.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, e);
      log.debug(`actual error: ${JSON.stringify(e)}`);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Method that identifies the actual request to be made and then
   * makes the call through the appropriate Handler
   *
   * @function identifyRequest
   * @param {String} entity - the name of the entity for this request.
   *                          (required)
   * @param {String} action - the name of the action being executed. (required)
   * @param {Object} requestObj - an object that contains all of the possible
   *                              parts of the request (payload, uriPathVars,
   *                              uriQuery, uriOptions and addlHeaders
   *                              (optional). Can be a stringified Object.
   * @param {Boolean} translate - whether to translate the response. Defaults
   *                              to true. If no translation will just return
   *                              'success' or an error message
   * @param {Callback} callback - a callback function to return the result
   *                              Data/Status or the Error
   */
  identifyRequest(entity, action, requestObj, translate, callback) {
    const meth = 'requestHandler-identifyRequest';
    const origin = `${this.myid}-${meth}`;
    log.trace(`${origin}: ${entity}-${action}`);
    const overallTime = process.hrtime();
    try {
      // verify parameters passed are valid
      if (entity === null || entity === '') {
        const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['entity'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
      if (action === null || action === '') {
        const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['action'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      if (this.props.cache.enabled && action !== 'getGenerics' && this.cacheHandler.isTaskCached(entity, action)) {
        return this.retrieveEntitiesCache(entity, {}, (result, error) => {
          if (error) {
            return callback(null, error);
          }
          if (translate) {
            return callback(result);
          }
          return callback('success');
        });
      }

      // Get the entity schema from the file system
      return this.propUtil.getEntitySchema(entity, action, this.props.choosepath, this.dbUtil, (entitySchema, entityError) => {
        // verify protocol for call
        if (entityError) {
          const errorObj = this.transUtil.checkAndReturn(entityError, origin, 'Issue identifiying request');
          return callback(null, errorObj);
        }

        // verify protocol for call
        if (!Object.hasOwnProperty.call(entitySchema, 'protocol')) {
          const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['action protocol'], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        // Determine the Protocol so the appropriate handler can be called
        if (entitySchema.protocol.toUpperCase() === 'REST') {
          // override the data types on the request
          if (requestObj && requestObj.datatype) {
            // must have a legitimate request data type
            if (requestObj.datatype.request && (requestObj.datatype.request.toUpperCase() === 'JSON'
              || requestObj.datatype.request.toUpperCase() === 'XML' || requestObj.datatype.request.toUpperCase() === 'URLENCODE'
              || requestObj.datatype.request.toUpperCase() === 'URLQUERY' || requestObj.datatype.request.toUpperCase() === 'FORM'
              || requestObj.datatype.request.toUpperCase() === 'JSON2XML' || requestObj.datatype.request.toUpperCase() === 'PLAIN')) {
              entitySchema.requestDatatype = requestObj.datatype.request;
            }
            // must have a legitimate response data type
            if (requestObj.datatype.response && (requestObj.datatype.response.toUpperCase() === 'JSON'
              || requestObj.datatype.response.toUpperCase() === 'XML' || requestObj.datatype.response.toUpperCase() === 'URLENCODE'
              || requestObj.datatype.response.toUpperCase() === 'XML2JSON' || requestObj.datatype.response.toUpperCase() === 'PLAIN')) {
              entitySchema.responseDatatype = requestObj.datatype.response;
            }
          }

          return this.restHandler.genericRestRequest(entity, action, entitySchema, requestObj, translate, (result, error) => {
            const overallDiff = process.hrtime(overallTime);
            const overallEnd = `${Math.round(((overallDiff[0] * NS_PER_SEC) + overallDiff[1]) / 1000000)}ms`;
            if (error) {
              const newError = error;
              if (!newError.metrics) {
                newError.metrics = {};
              }

              newError.metrics.capabilityTime = overallEnd;
              // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
              if (this.saveMetric) {
                dbCalls('metrics', entity, action, newError.metrics, (saved) => {
                  if (saved) {
                    log.info(`${origin}: Metrics Saved`);
                  }
                });
              }
              return callback(null, newError);
            }

            let newRes = result;
            if (!newRes) {
              newRes = {
                metrics: {
                }
              };
            } else if (!newRes.metrics) {
              newRes.metrics = {};
            }

            newRes.metrics.capabilityTime = overallEnd;
            // overallEnd is from start of identifyRequest to right before dbCalls is called (error or good).
            // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
            if (this.saveMetric) {
              dbCalls('metrics', entity, action, newRes.metrics, (saved) => {
                if (saved) {
                  log.info(`${origin}: Metrics Saved`);
                }
              });
            }
            return callback(newRes);
          });
        }

        // Unsupported protocols
        const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [entitySchema.protocol], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      });
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue identifiying request');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Method that identifies the protocol for the healthcheck and then
   * takes the appropriate action.
   *
   * @function identifyHealthcheck
   * @param {Object} requestObj - an object that contains all of the possible
   *                              parts of the request (payload, uriPathVars,
   *                              uriQuery, uriOptions and addlHeaders
   *                              (optional). Can be a stringified Object.
   * @param {Callback} callback - a callback function to return the result of
   *                              the Healthcheck
   */
  identifyHealthcheck(requestObj, callback) {
    const meth = 'requestHandler-identifyHealthcheck';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);
    const overallTime = process.hrtime();

    try {
      let prot = this.props.healthcheck.protocol;

      // Get the entity schema from the file system
      return this.propUtil.getEntitySchema('.system', 'healthcheck', this.props.choosepath, this.dbUtil, (healthSchema, healthError) => {
        if (healthError || !healthSchema || Object.keys(healthSchema).length === 0) {
          log.debug(`${origin}: Using adapter properties for healthcheck information`);
        } else {
          log.debug(`${origin}: Using action and schema for healthcheck information`);
          if (healthcheckpath) {
            log.debug('Reading healthcheck URI from Service Instance Configuration.');
            healthSchema.entitypath = healthcheckpath;
            if (healthSchema.mockresponses && healthSchema.mockresponses[0] && healthSchema.mockresponses[0].name) {
              healthSchema.mockresponses[0].name = healthcheckpath;
            }
          }
        }

        if (healthSchema && healthSchema.protocol) {
          prot = healthSchema.protocol;
        }

        // Determine the Protocol so the appropriate handler can be called
        if (prot.toUpperCase() === 'REST') {
          return this.restHandler.healthcheckRest(healthSchema, requestObj, (result, error) => {
            const overallDiff = process.hrtime(overallTime);
            const overallEnd = `${Math.round(((overallDiff[0] * NS_PER_SEC) + overallDiff[1]) / 1000000)}ms`;
            if (error) {
              const newError = error;
              if (!newError.metrics) {
                newError.metrics = {};
              }

              newError.metrics.capabilityTime = overallEnd;
              // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
              if (this.saveMetric) {
                dbCalls('metrics', '.system', 'healthcheck', newError.metrics, (saved) => {
                  if (saved) {
                    log.info(`${origin}: Metrics Saved`);
                  }
                });
              }
              return callback(null, newError);
            }

            let newRes = result;
            if (!newRes) {
              newRes = {
                metrics: {
                }
              };
            } else if (!newRes.metrics) {
              newRes.metrics = {};
            }

            newRes.metrics.capabilityTime = overallEnd;
            // overallEnd is from start of identifyRequest to right before dbCalls is called (error or good).
            // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
            if (this.saveMetric) {
              dbCalls('metrics', '.system', 'healthcheck', newRes.metrics, (saved) => {
                if (saved) {
                  log.info(`${origin}: Metrics Saved`);
                }
              });
            }
            return callback(newRes);
          });
        }

        // Unsupported protocols
        const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [prot], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      });
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue identifiying healthcheck');
      return callback(null, errorObj);
    }
  }

  /**
   * getQueue is used to get information for all of the requests currently in the queue.
   *
   * @function getQueue
   * @param {queueCallback} callback - a callback function to return the result (Queue)
   *                                   or the error
   */
  getQueue(callback) {
    const meth = 'requestHandler-getQueue';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    try {
      // Determine the Protocol so the appropriate handler can be called
      if (this.props.healthcheck.protocol.toUpperCase() === 'REST') {
        return this.restHandler.getQueue(callback);
      }

      // Unsupported protocols
      const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [this.props.healthcheck.protocol], null, null, null);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting queue');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Takes in property text and an encoding/encryption and returns
   * the resulting encoded/encrypted string
   *
   * @function encryptProperty
   * @param {String} property - the property to encrypt
   * @param {String} technique - the technique to use to encrypt
   *
   * @param {Callback} callback - a callback function to return the result
   *                              Encrypted String or the Error
   */
  encryptProperty(property, technique, callback) {
    const meth = 'requestHandler-encryptProperty';
    const origin = `${this.myid}-${meth}`;
    log.trace(origin);

    try {
      const returnObj = {
        icode: 'AD.200'
      };

      returnObj.response = this.propUtil.encryptProperty(property, technique);
      return callback(returnObj);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue encrypting property');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Build a standard error object from the data provided
   *
   * @function formatErrorObject
   * @param {String} adaptId - the id of the adapter (required).
   * @param {String} origin - the originator of the error (required).
   * @param {String} failCode - the internal IAP error code or error type (required).
   * @param {Integer} sysCode - the error code from the other system (optional).
   * @param {Object} sysRes - the raw response from the other system (optional).
   * @param {Exception} stack - tany available stack trace from the issue (optional).
   *
   * @return {Object} the error object, null if missing pertinent information
   */
  formatErrorObject(adaptId, origin, failCode, variables, sysCode, sysRes, stack) {
    const morigin = `${this.myid}-requestHandler-formatErrorObject`;
    log.trace(morigin);

    // this is just a pass through for clients to use - rather then expose translator
    return this.transUtil.formatErrorObject(`${adaptId}-${origin}`, failCode, variables, sysCode, sysRes, stack);
  }

  /**
   * @summary Determines whether the error is one that can be failed over to
   * the next adapter to handle.
   *
   * @function setFailover
   * @param {Object} errorObj - the error object
   *
   * @return {Enumeration} failoverCode - a string containing the failover code
   *                                      'AD.300' - failover OK
   *                                      'AD.500' - no failover
   *                                      code from response if can not decide
   */
  setFailover(errorObj) {
    const origin = `${this.myid}-requestHandler-setFailover`;
    log.trace(origin);

    try {
      // if the errorMsg exists and has a code, check it
      if (errorObj && errorObj.code) {
        if (this.failoverCodes) {
          // is it a code that we allow failover?
          for (let f = 0; f < this.failoverCodes.length; f += 1) {
            if (errorObj.code === this.failoverCodes[f]) {
              return allowFailover;
            }
          }
        }
      }

      // if not resolved based on code, return what was in the error if provided
      if (errorObj && errorObj.icode) {
        return errorObj.icode;
      }

      // return the default result
      return noFailover;
    } catch (e) {
      log.error(`${origin}: Caught Exception ${e}`);
      return noFailover;
    }
  }

  /**
   * @summary Provides a way for the adapter to tell north bound integrations
   * whether the adapter supports type and specific entity
   *
   * @function verifyCapability
   * @param {String} entityType - the entity type to check for
   * @param {String} actionType - the action type to check for
   * @param {String/Array} entityId - the specific entity we are looking for only works
   *                      if caching for that entityType is enabled. does not make API call!
   *
   * @return {Array of Enumeration} - whether the entity was
   *                   'found' - entity was found
   *                   'notfound' - entity was not found
   *                   'error' - there was an error - check logs
   */
  verifyCapability(entityType, actionType, entityId, callback) {
    const origin = `${this.myid}-requestHandler-verifyCapability`;
    log.trace(origin);
    const entitiesD = `${this.adapterBaseDir}/entities`; // causes verifyCapability errors

    try {
      // verify the entities directory exists
      if (!fs.existsSync(entitiesD)) {
        log.error(`${origin}: Missing ${entitiesD} directory - Aborting!`);
        return callback(['error']);
      }

      // get the entities this adapter supports
      const entities = fs.readdirSync(entitiesD);
      // go through the entities
      for (let e = 0; e < entities.length; e += 1) {
        // did we find the entity type?
        if (entities[e] === entityType) {
          // if we are only interested in the entity
          if (!actionType && !entityId) {
            return callback(['found']);
            // return callback([true]);
          }
          // if we do not have an action, check for the specific entity
          if (!actionType) {
            return this.checkEntityCached(entityType, entityId, (data, error) => {
              if (error) {
                log.error(`${origin}: Failed check entity cached with error ${error}`);
                return callback(null, error);
              }
              return callback(data);
            });
          }

          // get the entity actions from the action file
          const actionFile = `${entitiesD}/${entities[e]}/action.json`;
          if (fs.existsSync(actionFile)) {
            const actionJson = require(actionFile); // fails verify unit test, exists but can't require
            const { actions } = actionJson;

            // go through the actions for a match
            for (let a = 0; a < actions.length; a += 1) {
              if (actions[a].name === actionType) {
                // if we are not interested in a specific entity
                if (!entityId) {
                  return callback(['found']);
                }
                return this.checkEntityCached(entityType, entityId, (data, error) => {
                  if (error) {
                    log.error(`${origin}: Failed check entity cached with error ${error}`);
                    return callback(null, error);
                  }
                  if (data) {
                    return callback(['found']);
                  }
                  return callback(['notfound']);
                });
              }
            }

            log.warn(`${origin}: Action ${actionType} not found on entity ${entityType}`);

            // return an array for the entityids since can not act on any
            const result = ['notfound'];
            if (entityId && Array.isArray(entityId)) {
              // add not found for each entity, (already added the first)
              for (let r = 1; r < entityId.length; r += 1) {
                result.push('notfound');
              }
            }
            return callback(result);
          }

          log.error(`${origin}: Action ${actionType} on entity ${entityType} missing action file`);
          return callback(['error']);
        }
      }

      log.error(`${origin}: Entity ${entityType} not found in adapter`);

      // return an array for the entityids since can not act on any
      const result = ['notfound'];
      if (entityId && Array.isArray(entityId)) {
        // add not found for each entity, (already added the first)
        for (let r = 1; r < entityId.length; r += 1) {
          result.push('notfound');
        }
      }

      return callback(result);
    } catch (e) {
      log.error(`${origin}: Caught Exception ${e}`);
      return callback(['error']);
    }
  }

  /**
   * @summary Provides a way for the adapter to tell north bound integrations
   * all of the capabilities for the current adapter
   *
   * @function getAllCapabilities
   *
   * @return {Array} - containing the entities and the actions available on each entity
   */
  getAllCapabilities() {
    const origin = `${this.myid}-requestHandler-getAllCapabilities`;
    log.trace(origin);
    const entitiesD = `${this.adapterBaseDir}/entities`;
    const capabilities = [];

    try {
      // verify the entities directory exists
      if (!fs.existsSync(entitiesD)) {
        log.error(`${origin}: Missing ${entitiesD} directory`);
        return capabilities;
      }

      // get the entities this adapter supports
      const entities = fs.readdirSync(entitiesD);

      // go through the entities
      for (let e = 0; e < entities.length; e += 1) {
        // get the entity actions from the action file
        const actionFile = `${entitiesD}/${entities[e]}/action.json`;
        const entityActions = [];

        if (fs.existsSync(actionFile)) {
          const actionJson = require(actionFile);
          const { actions } = actionJson;

          // go through the actions for a match
          for (let a = 0; a < actions.length; a += 1) {
            entityActions.push(actions[a].name);
          }
        }

        const newEntity = {
          entity: entities[e],
          actions: entityActions
        };

        capabilities.push(newEntity);
      }

      return capabilities;
    } catch (e) {
      log.error(`${origin}: Caught Exception ${e}`);
      return capabilities;
    }
  }

  /**
   * @summary get a token with the provide parameters
   *
   * @function makeTokenRequest
   * @param {Object} reqBody - Any data to add to the body of the token request
   * @param {Object} callProperties - Properties that override the default properties
   *
   * @return {Object} - containing the token(s)
   */
  makeTokenRequest(reqBody, callProperties, callback) {
    const origin = `${this.myid}-requestHandler-makeTokenRequest`;
    log.trace(origin);

    try {
      // set up the right credentials - passed in overrides default
      let useUser = username;
      if (callProperties && callProperties.authentication && callProperties.authentication.username) {
        useUser = callProperties.authentication.username;
      }

      // validate the action files for the adapter
      return this.connector.makeTokenRequest('/none/token/path', useUser, reqBody, null, callProperties, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting token');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary provide inventory information abbout the adapter
   *
   * @function getAdapterInventory
   *
   * @return {Object} - containing the adapter inventory information
   */
  getAdapterInventory(callback) {
    const origin = `${this.myid}-requestHandler-getAdapterInventory`;
    log.trace(origin);

    try {
      const adapterInv = {
        issues: {}
      };

      // are the utils within the adapter
      if (__dirname.indexOf(this.directory) === 0) {
        adapterInv.issues.embedUtils = true;
      } else {
        adapterInv.issues.embedUtils = false;
      }

      // get the adapter package.json
      if (fs.existsSync(path.join(this.directory, 'package.json'))) {
        const adaptPackage = require(path.join(this.directory, 'package.json'));

        // Namespace - package & directory
        const [tempNS, tempName] = adaptPackage.name.split('/');
        adapterInv.namespace = tempNS;
        if (this.directory.indexOf(adapterInv.namespace) > 0) {
          adapterInv.issues.inNamespace = true;
        } else {
          adapterInv.issues.inNamespace = false;
        }

        // Name - package & directory
        adapterInv.name = tempName;
        if (this.directory.indexOf(adapterInv.name) > 0) {
          adapterInv.issues.inName = true;
        } else {
          adapterInv.issues.inName = false;
        }

        // Version - package
        adapterInv.version = adaptPackage.version;
      } else {
        adapterInv.namespace = 'TBD';
        adapterInv.issues.inNamespace = false;
        adapterInv.name = 'TBD';
        adapterInv.issues.inName = false;
        adapterInv.version = 'TBD';
      }

      // get the adapter pronghorn.json
      adapterInv.brokerDefined = false;
      adapterInv.cacheDefined = false;
      if (fs.existsSync(path.join(this.directory, 'pronghorn.json'))) {
        const adaptPronghorn = require(path.join(this.directory, 'pronghorn.json'));

        // Active WF Tasks - pronghorn.json
        let wfCount = 0;
        for (let i = 0; i < adaptPronghorn.methods.length; i += 1) {
          if (adaptPronghorn.methods[i].task === true) {
            wfCount += 1;
          }
          if (adaptPronghorn.methods[i].name === 'getDevicesFiltered') {
            adapterInv.brokerDefined = true;
          }
          if (adaptPronghorn.methods[i].name === 'iapUpdateAdapterCache') {
            adapterInv.cacheDefined = true;
          }
        }
        adapterInv.activeWFTasks = wfCount;
      } else {
        adapterInv.activeWFTasks = -1;
      }

      // get the utils package.json
      if (fs.existsSync('../package.json')) {
        const utilsPackage = require('../package.json');

        // Version of adapter-utils - utils package
        adapterInv.utilVersion = utilsPackage.version;
      } else {
        adapterInv.utilVersion = 'TBD';
      }

      // return the results
      return callback(adapterInv);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting Inventory information');
      return callback(null, errorObj);
    }
  }

  /* ********************************************** */
  /*                                                */
  /*           EXPOSES CACHE HANDLER                */
  /*                                                */
  /* ********************************************** */
  /**
   * @summary Check the current cache to see if we know about a specific entity
   *
   * @function checkEntityCached
   * @param {String} entityType - the entity type to check for
   * @param {String/Array} entityNames - the specific entity we are looking for
   *
   * @return {Array of Boolean} - for each entity name, whether found
   */
  checkEntityCached(entityType, entityNames, callback) {
    const origin = `${this.myid}-requestHandler-checkEntityCached`;
    log.trace(origin);

    try {
      return this.cacheHandler.isEntityCached(entityType, entityNames, callback);
    } catch (e) {
      // handle any exception
      log.error('Check Entity Cached failed with error.');
      return callback(false); // pretend no error and that it is not cached
    }
  }

  /**
   * @function checkEntityTypeCached
   * @summary checks whether an entity type is currently cached
   * @param {String} entityType - entity type to check
   * @return {Boolean} - whether or not the entity type is in cache
   */
  checkEntityTypeCached(entityType, callback) {
    const origin = `${this.myid}-requestHandler-checkEntityTypeCached`;
    log.trace(origin);

    try {
      return this.cacheHandler.isEntityTypeToBeCached(entityType, callback);
    } catch (e) {
      // handle any exception
      log.error('Check Entity Type Cached failed with error.');
      return callback(false); // pretend no error and that it is not cached
    }
  }

  /**
   * @summary Adds the provided entity list to the cache
   *
   * @function addEntityCache
   * @param {String/Array of Strings} entityType - the entity type(s) to populate
   * @param {Integer} cacheLimit - limit of items to store in cache
   * @param {Double} newFrequency - how often to update (in hours)
   * @param {Callback} callback - whether the cache was updated or not
   * @returns return of the callback
   */
  addEntityCache(entityType, entities, callback) {
    const origin = `${this.myid}-requestHandler-addEntityCache`;
    return callback(null, `${origin} ${entityType}:${entities} call has been deprecated with new cache handling!`);
  }

  /**
   * @summary Populate the cache for the given entities
   *
   * @function populateEntityCache
   * @param {String/Array of Strings} entityType - the entity type(s) to populate
   * @param {Callback} callback - whether the cache was updated or not for each entity type
   * @returns return of the callback
   */
  populateEntityCache(entityTypes, callback) {
    const origin = `${this.myid}-requestHandler-populateEntityCache`;
    log.trace(origin);

    return this.cacheHandler.populateCache(entityTypes).then((result) => callback(result, null)).catch((error) => callback(null, error));
  }

  /**
   * @summary Retrieves data from cache for specified entity type
   *
   * @function retrieveEntitiesCache
   * @param {String} entityType - entity of which to retrieve
   * @param {Object} options - settings of which data to return and how to return it
   * @param {Callback} callback - the data if it was retrieved
   */
  retrieveEntitiesCache(entityType, options, callback) {
    const origin = `${this.myid}-requestHandler-retrieveEntitiesCache`;
    log.trace(origin);

    if (!options) {
      return callback(null, 'Options cannot be null!');
    }
    try {
      return this.cacheHandler.retrieveCacheEntries(entityType, options, callback);
    } catch (e) {
      // handle any exception
      return callback(null, 'Retrieve Cache Failed');
    }
  }

  /* ********************************************** */
  /*                                                */
  /*          EXPOSES BROKER HANDLER                */
  /*                                                */
  /* ********************************************** */
  /**
   * @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, callback) {
    const origin = `${this.myid}-requestHandler-hasEntities`;
    log.trace(origin);

    try {
      return this.brokerHandler.hasEntities(entityType, entityList, {}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker hasEntities Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Determines if this adapter supports any in a list of entities with custom auth
   *
   * @function hasEntitiesAuth
   * @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
   */
  hasEntitiesAuth(entityType, entityList, callOptions, callback) {
    const origin = `${this.myid}-requestHandler-hasEntitiesAuth`;
    log.trace(origin);

    try {
      return this.brokerHandler.hasEntities(entityType, entityList, callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker hasEntities Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * @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, callback) {
    const origin = `${this.myid}-requestHandler-getDevice`;
    log.trace(origin);

    try {
      return this.brokerHandler.getDevice(deviceName, {}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getDevice Failed');
      return callback(null, errorObj);
    }
  }

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

    try {
      return this.brokerHandler.getDevice(deviceName, callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getDevice Failed');
      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, callback) {
    const origin = `${this.myid}-requestHandler-getDevicesFiltered`;
    log.trace(origin);

    try {
      return this.brokerHandler.getDevicesFiltered(options, {}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getDevicesFiltered Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Get Appliances that match the filter with custom auth
   *
   * @function getDevicesFilteredAuth
   * @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
   */
  getDevicesFilteredAuth(options, callOptions, callback) {
    const origin = `${this.myid}-requestHandler-getDevicesFiltered`;
    log.trace(origin);

    try {
      return this.brokerHandler.getDevicesFiltered(options, callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getDevicesFiltered Failed');
      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, callback) {
    const origin = `${this.myid}-requestHandler-isAlive`;
    log.trace(origin);

    try {
      return this.brokerHandler.isAlive(deviceName, {}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker isAlive Failed');
      return callback(null, errorObj);
    }
  }

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

    try {
      return this.brokerHandler.isAlive(deviceName, callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker isAlive Failed');
      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, callback) {
    const origin = `${this.myid}-requestHandler-getConfig`;
    log.trace(origin);

    try {
      return this.brokerHandler.getConfig(deviceName, format, {}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getConfig Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Gets a config for the provided Appliance with custom auth
   *
   * @function getConfigAuth
   * @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
   */
  getConfigAuth(deviceName, format, callOptions, callback) {
    const origin = `${this.myid}-requestHandler-getConfigAuth`;
    log.trace(origin);

    try {
      return this.brokerHandler.getConfig(deviceName, format, callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker getConfig Failed');
      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(callback) {
    const origin = `${this.myid}-requestHandler-iapGetDeviceCount`;
    log.trace(origin);

    try {
      return this.brokerHandler.iapGetDeviceCount({}, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker iapGetDeviceCount Failed');
      return callback(null, errorObj);
    }
  }

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

    try {
      return this.brokerHandler.iapGetDeviceCount(callOptions, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Broker iapGetDeviceCount Failed');
      return callback(null, errorObj);
    }
  }

  /* ********************************************** */
  /*                                                */
  /*          EXPOSES GENERIC HANDLER               */
  /*                                                */
  /* ********************************************** */
  /**
   * Makes the requested generic call
   *
   * @function expandedGenericAdapterRequest
   * @param {Object} metadata - metadata for the call (optional).
   *                 Can be a stringified Object.
   * @param {String} uriPath - the path of the api call - do not include the host, port, base path or version (optional)
   * @param {String} restMethod - the rest method (GET, POST, PUT, PATCH, DELETE) (optional)
   * @param {Object} pathVars - the parameters to be put within the url path (optional).
   *                 Can be a stringified Object.
   * @param {Object} queryData - the parameters to be put on the url (optional).
   *                 Can be a stringified Object.
   * @param {Object} requestBody - the body to add to the request (optional).
   *                 Can be a stringified Object.
   * @param {Object} addlHeaders - additional headers to be put on the call (optional).
   *                 Can be a stringified Object.
   * @param {getCallback} callback - a callback function to return the result (Generics)
   *                 or the error
   */
  expandedGenericAdapterRequest(metadata, uriPath, restMethod, pathVars, queryData, requestBody, addlHeaders, callback) {
    const origin = `${this.myid}-requestHandler-expandedGenericAdapterRequest`;
    log.trace(origin);

    try {
      return this.genericHandler.expandedGenericAdapterRequest(metadata, uriPath, restMethod, pathVars, queryData, requestBody, addlHeaders, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Expanded Generic Adapter Request Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * Makes the requested generic call
   *
   * @function genericAdapterRequest
   * @param {String} uriPath - the path of the api call - do not include the host, port, base path or version (required)
   * @param {String} restMethod - the rest method (GET, POST, PUT, PATCH, DELETE) (required)
   * @param {Object} queryData - the parameters to be put on the url (optional).
   *                 Can be a stringified Object.
   * @param {Object} requestBody - the body to add to the request (optional).
   *                 Can be a stringified Object.
   * @param {Object} addlHeaders - additional headers to be put on the call (optional).
   *                 Can be a stringified Object.
   * @param {getCallback} callback - a callback function to return the result (Generics)
   *                 or the error
   */
  genericAdapterRequest(uriPath, restMethod, queryData, requestBody, addlHeaders, callback) {
    const origin = `${this.myid}-requestHandler-genericAdapterRequest`;
    log.trace(origin);

    try {
      return this.genericHandler.genericAdapterRequest(uriPath, restMethod, queryData, requestBody, addlHeaders, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Generic Adapter Request Failed');
      return callback(null, errorObj);
    }
  }

  /**
   * Makes the requested generic call with no base path or version
   *
   * @function genericAdapterRequestNoBasePath
   * @param {String} uriPath - the path of the api call - do not include the host, port, base path or version (required)
   * @param {String} restMethod - the rest method (GET, POST, PUT, PATCH, DELETE) (required)
   * @param {Object} queryData - the parameters to be put on the url (optional).
   *                 Can be a stringified Object.
   * @param {Object} requestBody - the body to add to the request (optional).
   *                 Can be a stringified Object.
   * @param {Object} addlHeaders - additional headers to be put on the call (optional).
   *                 Can be a stringified Object.
   * @param {getCallback} callback - a callback function to return the result (Generics)
   *                 or the error
   */
  genericAdapterRequestNoBasePath(uriPath, restMethod, queryData, requestBody, addlHeaders, callback) {
    const origin = `${this.myid}-requestHandler-genericAdapterRequestNoBasePath`;
    log.trace(origin);

    try {
      return this.genericHandler.genericAdapterRequestNoBasePath(uriPath, restMethod, queryData, requestBody, addlHeaders, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Generic Adapter Request No Base Path Failed');
      return callback(null, errorObj);
    }
  }

  /* ********************************************** */
  /*                                                */
  /*          EXPOSES AUTH HANDLER               */
  /*                                                */
  /* ********************************************** */

  /**
   * @summary Gets the hcma authorization for the call
   *
   * @function getAWSAuthorization
   * @param {string} method - the method for the action we are setting authorization for
   * @param {object} requestObj - an object that contains all of the possible parts of the request (payload, uriPathVars,
   *                              uriQuery, uriOptions and addlHeaders (optional). Can be a stringified Object.
   * @param {string} uriPath - the path for the call. (required)
   * @param {string} service - the AWS service we are talking to
   * @param {object} [stsParams] - STS Parameters to use for authentication.
   * @param {string} [roleName] - RoleName to authenticate against
   *
   *  @return {Object} the headers to add to the request
   */
  getAWSAuthorization(method, requestObj, uriPath, service, STSParams, roleName, callback) {
    const origin = `${this.myid}-requestHandler-getAWSAuthentication`;
    log.trace(origin);

    try {
      return this.authHandler.getAWSAuthorization(method, requestObj, uriPath, service, STSParams, roleName, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'AWS Authentication Request Failed');
      return callback(null, errorObj);
    }
  }
}

module.exports = RequestHandler;
