/* @copyright Itential, LLC 2018 */

// Set globals
/* global adapters brokers g_redis log */
/* eslint class-methods-use-this:warn */
/* eslint consistent-return:warn */
/* eslint import/no-dynamic-require:warn */
/* eslint no-underscore-dangle: [2, { "allow": ["_id"] }] */
/* eslint no-unused-vars:warn */
/* eslint no-use-before-define:warn */
/* eslint prefer-destructuring:warn */
/* eslint prefer-object-spread:warn */
/* eslint no-param-reassign:warn */

/* NodeJS internal API utilities */
const uuid = require('uuid');
const os = require('os');
const fs = require('fs');
const jwt = require('jsonwebtoken');

/* Fetch in the other needed components */
const path = require('path');
const https = require('https');
const http = require('http');
const cookieHandler = require('cookie');
const jsonQuery = require('json-query');
const xml2js = require('xml2js');
const querystring = require('querystring');
const HttpsProxyAgent = require('https-proxy-agent');
const SocksProxyAgent = require('socks-proxy-agent');
const AsyncLockCl = require('async-lock');
const FormData = require('form-data');

const ThrottleCl = require(path.join(__dirname, '/throttle.js'));

const allowFailover = 'AD.300';
const noFailover = 'AD.500';
let transUtilInst = null;
let propUtilInst = null;
let dbUtilInst = null;

// Global Variables - From Properties
let props = {};
let host = null;
let port = null;
let basepath = null;
let version = null;
let authMethod = null;
let authField = null;
let authFormat = null;
let authLogging = false;
let username = null;
let password = null;
let staticToken = null;
let tokenUserField = 'username';
let tokenPwdField = 'password';
let tokenResField = 'token';
let tokenTimeout = -1;
let tokenError = 401;
let tokenPath = null;
let tokenCache = 'local';
const tokenList = [];
const tokenlock = 0;
let stub = false;
let protocol = 'http';
let healthcheckpath = null;
let throttleEnabled = false;
let numberPhs = 1;
let ecdhAuto = false;
let sslEnabled = false;
let sslAcceptInvalid = false;
let sslCAFile = null;
let sslKeyFile = null;
let sslCertFile = null;
let sslCiphers = null;
let secureProtocol = null;
let proxyEnabled = false;
let proxyHost = null;
let proxyPort = null;
let proxyProtocol = 'http';
let proxyUser = null;
let proxyPassword = null;
let numRetries = 3;
let numRedirects = 0;
let limitRetryError = [0];
let attemptTimeout = 5000;
let healthcheckOnTimeout = true;
let globalRequest = null;
let archiving = false;
let returnRequest = false;
let healthy = false;
const healthlock = 0;
let healthcheck = false;
const healthchecklock = 0;

// Other global variables
let id = null;
let phInstance = null;
let requestId = 0;
let archiveColl = '_results';
const NS_PER_SEC = 1e9;
let throttleEng = null;
let tlock = null;
let hlock = null;
let hclock = null;

let crest = null;
let cacheHHead = null;
let cacheHSchema = null;
let cacheHPay = null;

/* CONNECTOR ENGINE INTERNAL FUNCTIONS         */
/** Wait for adapter-mongo to be available.
 * @summary adapter may load before adapter-mongo but it requires UPDATE: test if dbUtil object can connect.
 * mongo to be available before. This function loops until mongo is available.
 */
function waitForMongo() {
  const origin = `${id}-connectorRest-waitForMongo`;
  log.trace(origin);

  return new Promise((resolve, reject) => {
    const INTERVAL = 5000;
    let count = 0;

    // Run this interval until we can connect to the Mongo Adapter
    const intervalObject = setInterval(() => {
      count += INTERVAL / 1000;

      // try to see if mongo is available
      return dbUtilInst.connect((alive, db) => {
        if (!alive) {
          log.warn(`${origin}: No database connections available`);
          if (count === 100 * (INTERVAL / 1000)) {
            log.warn(`${origin}: attempted mongo enough times - will be saving to local JSONs`);
            resolve();
          }
        } else {
          log.info(`${origin}: dbUtil reports a status = success will now be able to connect.`);
          db.close();
          resolve();
          clearInterval(intervalObject);
        }
        log.info(`${origin}: Waiting for dbUtil to be available. Seconds passed: ${count}.`);
      });
    }, INTERVAL);
  });
}

/** Wait for system to be available.
 * @summary assumes System has gone down and we are not able to connect
 *    to it and receive any data
 */
function waitForSystem(callProperties) {
  const origin = `${id}-connectorRest-waitForSystem`;
  log.trace(origin);

  return new Promise((resolve, reject) => {
    // if not running healthchecks on timeout just return
    if (callProperties && callProperties.host) {
      log.debug(`${origin}: skipping healthcheck after abort based on changed host`);
      resolve();
    } else if (callProperties && callProperties.request && Object.hasOwnProperty.call(callProperties.request, 'healthcheck_on_timeout')) {
      if (!callProperties.request.healthcheck_on_timeout) {
        log.debug(`${origin}: skipping healthcheck after abort based on call propertues`);
        resolve();
      }
    } else if (!healthcheckOnTimeout) {
      log.debug(`${origin}: skipping healthcheck after abort based on configuration`);
      resolve();
    }

    let useTimeout = attemptTimeout;
    if (callProperties && callProperties.request && callProperties.request.attempt_timeout) {
      useTimeout = callProperties.request.attempt_timeout;
    }

    // Find out if currently healthy
    hlock.acquire(healthlock, (doneH) => {
      doneH(healthy);
    }, (retH) => {
      // healthy
      if (retH) {
        log.debug(`${origin}: skipping healthcheck since another command has executed successfully`);
        resolve();
      } else {
        // unhealthy
        let count = 0;

        // Run this interval until we can connect to the System
        const intervalObject = setInterval(() => {
          count += useTimeout / 1000;

          // Lock the healthcheck so only one healthcheck at a time
          hclock.acquire(healthchecklock, (doneHC) => {
            // if not already running
            if (!healthcheck) {
              healthcheck = true;
              doneHC(true);
            } else {
              doneHC(false);
            }
          }, (retHC) => {
            // need to run healthcheck
            if (retHC) {
              crest.healthCheck(cacheHSchema, cacheHPay, cacheHHead, callProperties, (hc, error) => {
                if (hc) {
                  if (hc === 'fail') {
                    log.error(`${origin}: healthcheck reports a status = fail with error ${JSON.stringify(error)}`);

                    if (count === 100 * (useTimeout / 1000)) {
                      log.error(`${origin}: attempted healthcheck enough times - failing`);

                      // turn healthcheck false - since no longer active
                      hclock.acquire(healthchecklock, (doneHC2) => {
                        healthcheck = false;
                        doneHC2(true);
                      }, (retHC2) => {
                        reject();
                      });
                    }
                  } else if (hc === 'success') {
                    log.info(`${origin}: healthcheck reports a status = success. adapter will now restart processing.`);

                    // turn healthcheck false - since no longer active
                    hclock.acquire(healthchecklock, (doneHC3) => {
                      healthcheck = false;
                      doneHC3(true);
                    }, (retHC3) => {
                      resolve();
                      clearInterval(intervalObject);
                    });
                  }
                }

                log.info(`${origin}: Waiting for system to be available. Seconds passed: ${count}.`);
              });
            }
          });
        }, useTimeout);
      }
    });
  });
}

/*
 * INTERNAL FUNCTION: Get the best match for the moc k data response
 */
function matchMock(uriPath, method, type, mockresponses, respDatatype) {
  // Go through the mock data keys to find the proper data to return
  for (let p = 0; p < mockresponses.length; p += 1) {
    // is this the mock data for this call
    if (Object.hasOwnProperty.call(mockresponses[p], 'name')
        && uriPath === mockresponses[p].name) {
      if (Object.hasOwnProperty.call(mockresponses[p], 'method')
          && method.toUpperCase() === mockresponses[p].method.toUpperCase()) {
        if (Object.hasOwnProperty.call(mockresponses[p], 'type')
            && type.toUpperCase() === mockresponses[p].type.toUpperCase()) {
          // This is the mock data we really want as it best matches the request
          const specificResp = {
            status: 'success',
            code: 200,
            response: {}
          };

          if (Object.hasOwnProperty.call(mockresponses[p], 'response')) {
            // if a status is defined in the response, use it
            if (Object.hasOwnProperty.call(mockresponses[p].response, 'response')
                && Object.hasOwnProperty.call(mockresponses[p].response, 'status')) {
              specificResp.code = mockresponses[p].response.status;

              if (respDatatype && respDatatype.toUpperCase() === 'XML2JSON') {
                specificResp.response = mockresponses[p].response.response;
              } else {
                specificResp.response = JSON.stringify(mockresponses[p].response.response);
              }
            } else if (respDatatype && respDatatype.toUpperCase() === 'XML2JSON') {
              // if no response field, assume that the entire data is the response
              specificResp.response = mockresponses[p].response;
            } else {
              specificResp.response = JSON.stringify(mockresponses[p].response);
            }
          }

          return specificResp;
        }
      }
    }
  }

  return null;
}

/*
 * INTERNAL FUNCTION: recursively inspect body data if heirarchical
 */
function checkBodyData(uriPath, method, reqBdObj, mockresponses, respDatatype) {
  let specificResp = null;

  if (reqBdObj) {
    const reqBKeys = Object.keys(reqBdObj);

    // go through each key in the passed in object
    for (let k = 0; k < reqBKeys.length; k += 1) {
      const bVal = reqBdObj[reqBKeys[k]];

      if (bVal !== undefined && bVal !== null && bVal !== '') {
        // if the field is an object and not an array - recursively call with the new field value
        if (typeof bVal === 'object' && !Array.isArray(bVal)) {
          specificResp = checkBodyData(uriPath, method, bVal, mockresponses, respDatatype);
        } else if (Array.isArray(bVal) && bVal.length > 0 && (typeof bVal[0] === 'object')) {
          // if the field is an array containing objects - recursively call with each object in the array
          for (let a = 0; a < bVal.length; a += 1) {
            specificResp = checkBodyData(uriPath, method, bVal[a], mockresponses, respDatatype);

            // if the data match is found break the for loop - will return below
            if (specificResp !== null) {
              break;
            }
          }
        } else if (Array.isArray(bVal)) {
          // if an array of data, need to check each data in the array
          for (let a = 0; a < bVal.length; a += 1) {
            // should match fieldName-fieldValue
            const compStr = `${reqBKeys[k]}-${bVal[a]}`;
            specificResp = matchMock(uriPath, method, compStr, mockresponses, respDatatype);

            // if the data match is found break the for loop - will return below
            if (specificResp !== null) {
              break;
            }
          }
        } else {
          // should match fieldName-fieldValue
          const compStr = `${reqBKeys[k]}-${bVal}`;
          specificResp = matchMock(uriPath, method, compStr, mockresponses, respDatatype);
        }

        if (specificResp !== null) {
          break;
        }
      }
    }
  }
  return specificResp;
}

/*
 * INTERNAL FUNCTION: return stub results based on the path and method
 */
function returnStub(request, entitySchema, callProperties) {
  const origin = `${id}-connectorRest-returnStub`;
  log.trace(origin);
  const uriPath = request.origPath;
  const method = request.header.method.toUpperCase();
  const reqBody = request.body;
  const reqPath = request.header.path;

  const callResp = {
    status: 'success',
    code: 200,
    response: '{}'
  };

  // if there is no entity schema this should be a healthcheck
  if (!entitySchema) {
    log.error(`${origin}: healthcheck? no entity schema - no mock data to return`);
    return callResp;
  }

  log.trace(`${origin}: ${JSON.stringify(entitySchema)}`);

  // if there are no mock responses defined in the entity schema, stubbing will not work
  if (!Object.hasOwnProperty.call(entitySchema, 'mockresponses')) {
    callResp.status = 'failure';
    callResp.code = 400;
    callResp.message = 'no mock data defined in entity scheme - no mock data to return';
    delete callResp.response;

    log.error(`${origin}: ${callResp.message}`);
    return callResp;
  }

  const mockresponses = entitySchema.mockresponses;
  let specificResp = null;

  // if there is a request body, see if there is something that matches a specific input
  if (reqBody && (!entitySchema || !entitySchema.requestDatatype
      || entitySchema.requestDatatype.toUpperCase() === 'JSON' || entitySchema.requestDatatype.toUpperCase() === 'URLENCODE')) {
    let reqBdObj = null;
    if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'URLENCODE') {
      reqBdObj = querystring.parse(reqBody.trim());
    } else {
      reqBdObj = JSON.parse(reqBody.trim());
    }

    specificResp = checkBodyData(uriPath, method, reqBdObj, mockresponses, entitySchema.responseDatatype);

    if (specificResp !== null) {
      return specificResp;
    }
  }

  // if there are path variables, see if there is something that matches a specific variable
  if (uriPath.indexOf('{pathv') >= 0) {
    const uriTemp = uriPath.split('?');
    const actTemp = reqPath.split('?');
    uriTemp[0] = uriTemp[0].replace(/{pathv/g, '/{pathv');
    uriTemp[0] = uriTemp[0].replace(/{version/g, '/{version');
    uriTemp[0] = uriTemp[0].replace(/{base/g, '/{base');
    uriTemp[0] = uriTemp[0].replace(/\/\//g, '/');

    // remove basepath from both paths
    // get rid of base path from the uriPath
    uriTemp[0] = uriTemp[0].replace(/\/{base_path}/g, '');
    // if a base path was added to the request, remove it
    if (callProperties && callProperties.base_path && callProperties.base_path !== '/') {
      actTemp[0] = actTemp[0].replace(callProperties.base_path, '');
    } else if (basepath && basepath !== '/') {
      actTemp[0] = actTemp[0].replace(basepath, '');
    }

    // remove version from both paths
    // get rid of version from the uriPath
    uriTemp[0] = uriTemp[0].replace(/\/{version}/g, '');
    // if a version was added to the request, remove it
    if (callProperties && callProperties.version) {
      actTemp[0] = actTemp[0].replace(`/${callProperties.version}`, '');
    } else if (version && version !== '/') {
      actTemp[0] = actTemp[0].replace(`/${version}`, '');
    }

    const uriArray = uriTemp[0].split('/');
    const actArray = actTemp[0].split('/');

    // the number of items in both should be the same
    if (uriArray.length === actArray.length) {
      for (let i = 0; i < uriArray.length; i += 1) {
        if (uriArray[i].indexOf('{pathv') >= 0) {
          specificResp = matchMock(uriPath, method, actArray[i], mockresponses, entitySchema.responseDatatype);

          if (specificResp !== null) {
            return specificResp;
          }
        }
      }
    }
  }

  // if there are queiries or options, see if there is something that matches a specific input
  if (reqPath.indexOf('?') >= 0) {
    const queries = reqPath.substring(reqPath.indexOf('?') + 1);
    const queryArr = queries.split('&');

    // go through each query parameter
    for (let q = 0; q < queryArr.length; q += 1) {
      let qval = queryArr[q];
      if (qval !== undefined && qval !== null && qval !== '') {
        // stringifies it - in case it was not a string
        qval = `${qval}`;
        specificResp = matchMock(uriPath, method, qval, mockresponses, entitySchema.responseDatatype);

        if (specificResp !== null) {
          return specificResp;
        }
      }
    }
  }

  // if there is a request body, see if there is a specific response for body
  if (reqBody) {
    specificResp = matchMock(uriPath, method, 'WITHBODY', mockresponses, entitySchema.responseDatatype);

    if (specificResp !== null) {
      return specificResp;
    }
  }

  // if there are path variables, see if there is a specific response for path vars
  if (uriPath.indexOf('{pathv') >= 0) {
    const uriTemp = uriPath.split('?');
    const actTemp = reqPath.split('?');
    uriTemp[0] = uriTemp[0].replace(/{pathv/g, '/{pathv');
    uriTemp[0] = uriTemp[0].replace(/{version/g, '/{version');
    uriTemp[0] = uriTemp[0].replace(/{base_path}/g, '/{base_path}');
    uriTemp[0] = uriTemp[0].replace(/\/\//g, '/');

    // remove basepath from both paths
    // get rid of base path from the uriPath
    uriTemp[0] = uriTemp[0].replace(/\/{base_path}/g, '');
    // if a base path was added to the request, remove it
    if (callProperties && callProperties.base_path && callProperties.base_path !== '/') {
      actTemp[0] = actTemp[0].replace(callProperties.base_path, '');
    } else if (basepath && basepath !== '/') {
      actTemp[0] = actTemp[0].replace(basepath, '');
    }

    // remove version from both paths
    // get rid of version from the uriPath
    uriTemp[0] = uriTemp[0].replace(/\/{version}/g, '');
    // if a version was added to the request, remove it
    if (callProperties && callProperties.version) {
      actTemp[0] = actTemp[0].replace(`/${callProperties.version}`, '');
    } else if (version && version !== '/') {
      actTemp[0] = actTemp[0].replace(`/${version}`, '');
    }

    const uriArray = uriTemp[0].split('/');
    const actArray = actTemp[0].split('/');

    // the number of items in both should be the same
    if (uriArray.length === actArray.length) {
      let cnt = 1;
      for (let i = 0; i < uriArray.length; i += 1) {
        if (uriArray[i].indexOf('{pathv') >= 0) {
          specificResp = matchMock(uriPath, method, `WITHPATHV${cnt}`, mockresponses, entitySchema.responseDatatype);

          if (specificResp !== null) {
            return specificResp;
          }
          cnt += 1;
        }
      }
    }
  }

  // if there are queiries or options, see if there is a specific response for query or options
  if (uriPath.indexOf('?') >= 0) {
    specificResp = matchMock(uriPath, method, 'WITHQUERY', mockresponses, entitySchema.responseDatatype);

    if (specificResp !== null) {
      return specificResp;
    }

    specificResp = matchMock(uriPath, method, 'WITHOPTIONS', mockresponses, entitySchema.responseDatatype);

    if (specificResp !== null) {
      return specificResp;
    }
  }

  specificResp = matchMock(uriPath, method, 'DEFAULT', mockresponses, entitySchema.responseDatatype);

  if (specificResp !== null) {
    return specificResp;
  }

  callResp.status = 'failure';
  callResp.code = 400;
  callResp.message = `no mock data for ${uriPath} - no mock data to return`;
  delete callResp.response;

  log.error(`${origin}: ${callResp.message}`);
  return callResp;
}

/*
 * INTERNAL FUNCTION: makeRequest makes the actual call to System
 */
function makeRequest(request, entitySchema, callProperties, startTrip, attempt, callback) {
  const origin = `${id}-connectorRest-makeRequest`;
  log.trace(origin);
  let myTimeout = attemptTimeout;
  const roundTTime = startTrip || process.hrtime();
  const stTime = new Date().getTime();

  // if there is a timeout from the action (schema) use it instead
  if (entitySchema && entitySchema.timeout && entitySchema.timeout > 0) {
    myTimeout = entitySchema.timeout;
  }
  if (callProperties && callProperties.request && callProperties.request.attempt_timeout) {
    myTimeout = callProperties.request.attempt_timeout;
  }

  // if we need ecdhCurve set to auto
  if (callProperties && callProperties.ssl && Object.hasOwnProperty.call(callProperties.ssl, 'ecdhCurve')) {
    if (callProperties.ssl.ecdhCurve) {
      request.header.ecdhCurve = 'auto';
    }
  } else if (ecdhAuto) {
    request.header.ecdhCurve = 'auto';
  }

  // STUB CODE - this is good for an initial test - set stub in Pronghorn property file to true
  if (callProperties && Object.hasOwnProperty.call(callProperties, 'stub')) {
    if (callProperties.stub) {
      return callback(returnStub(request, entitySchema, callProperties));
    }
  } else if (stub) {
    return callback(returnStub(request, entitySchema, callProperties));
  }
  let useProt = http;
  let protStr = 'http';

  try {
    // determine proper protocol for primary request
    if (callProperties && callProperties.protocol) {
      if (callProperties.protocol === 'https') {
        log.debug(`${origin}: Switching to https protocol`);
        useProt = https;
        protStr = 'https';
      }
    } else if (entitySchema.sso && entitySchema.sso.protocol) {
      // this is for single signon - should we limit to token???
      if (entitySchema.sso.protocol === 'https') {
        log.debug(`${origin}: Switching to https protocol`);
        useProt = https;
        protStr = 'https';
      }
    } else if (protocol === 'https') {
      log.debug(`${origin}: Switching to https protocol`);
      useProt = https;
      protStr = 'https';
    }

    // add the proxy information to the request
    if (callProperties && callProperties.proxy && Object.hasOwnProperty.call(callProperties.proxy, 'enabled')) {
      if (callProperties.proxy.enabled) {
        let proxy = `${callProperties.proxy.protocol}://${callProperties.proxy.host}:${callProperties.proxy.port}`;
        if (callProperties.proxy.username && callProperties.proxy.password) {
          proxy = `${callProperties.proxy.protocol}://${callProperties.proxy.username}:${callProperties.proxy.password}@${callProperties.proxy.host}:${callProperties.proxy.port}`;
        }
        request.header.agent = new HttpsProxyAgent(proxy);

        if (callProperties.proxy.protocol.indexOf('socks') >= 0) {
          request.header.agent = new SocksProxyAgent(proxy);
        }
      }
    } else if (proxyEnabled) {
      let proxy = `${proxyProtocol}://${proxyHost}:${proxyPort}`;
      if (proxyUser && proxyPassword) {
        proxy = `${callProperties.proxy.protocol}://${proxyUser}:${proxyPassword}@${callProperties.proxy.host}:${callProperties.proxy.port}`;
      }
      request.header.agent = new HttpsProxyAgent(proxy);

      if (proxyProtocol.indexOf('socks') >= 0) {
        request.header.agent = new SocksProxyAgent(proxy);
      }
    }

    // if the request uses form-data type, need to add it to the headers
    const formData = new FormData();
    if (entitySchema.requestDatatype.toUpperCase() === 'FORM') {
      // need to convert request.body back to JSON
      let mybody = request.body;
      if (typeof mybody === 'string') {
        mybody = JSON.parse(request.body);
      }
      // add the data for each field into the form
      const mykeys = Object.keys(mybody);
      for (let k = 0; k < mykeys.length; k += 1) {
        log.debug(`APPENDING: ${mykeys[k]}, WITH VALUE ${mybody[mykeys[k]]}`);
        formData.append(mykeys[k], mybody[mykeys[k]]);
      }
      // get the new headers
      request.header.headers = Object.assign(request.header.headers, formData.getHeaders());
      // seems to not use Content-Type so fix that
      if (request.header.headers['content-type']) {
        request.header.headers['Content-Type'] = request.header.headers['content-type'];
      }
      delete request.header.headers['Content-length'];
      log.debug(`${origin}: Changing Headers for formData: ${JSON.stringify(request.header.headers)}`);
    }

    // these logs are very useful when debugging - however there is the potential for credentials to be exposed.
    if (authLogging) {
      log.debug(`FULL REQUEST: ${JSON.stringify(request.header)}`);
      log.debug(`FULL BODY: ${request.body}`);
    }

    // make the call to System
    const httpRequest = useProt.request(request.header, (res) => {
      let respStr = '';
      res.setEncoding('utf8');

      // process data from response
      res.on('data', (replyData) => {
        respStr += replyData;
      });

      // process end of response
      res.on('end', () => {
        const tripDiff = process.hrtime(roundTTime);
        const tripEnd = `${Math.round(((tripDiff[0] * NS_PER_SEC) + tripDiff[1]) / 1000000)}ms`;
        const callResp = {
          status: 'success',
          code: res.statusCode,
          headers: res.headers,
          response: respStr,
          request: {
            protocol: protStr,
            host: request.header.hostname,
            port: request.header.port,
            uri: request.header.path,
            method: request.header.method,
            payload: request.body
          },
          redirects: attempt,
          tripTime: tripEnd
        };

        // if there was a set cookie returned, need to set cookie on the new request
        if (request.header.headers.Cookie) {
          callResp.requestCookie = request.header.headers.Cookie;
        }

        // handling redirects
        let useRedirect = numRedirects;
        if (callProperties && callProperties.request && typeof callProperties.request.number_redirects === 'number') {
          useRedirect = Number(callProperties.request.number_redirects);
        }

        if (attempt < useRedirect && res.statusCode >= 300 && res.statusCode <= 308 && res.headers.location) {
          // retries = 0 go here, retries = 1 it doesn't
          const newProp = Object.assign({}, callProperties);
          const newRequest = Object.assign({}, request);
          const nextAtt = attempt + 1;

          // if there is a protocol on the new location use it
          if (res.headers.location.indexOf('://') >= 0) {
            const protSplit = res.headers.location.split('://');
            newProp.protocol = protSplit[0];

            if (protSplit.length > 1) {
              const pathSplit = protSplit[1].split('/');

              // grab the path
              if (pathSplit.length > 1) {
                newRequest.header.path = `/${protSplit[1].substring(protSplit[1].indexOf('/'))}`;
              } else {
                newRequest.header.path = '';
              }

              const portSplit = pathSplit[0].split(':');

              // grab the hostname
              newRequest.header.hostname = `${portSplit[0]}`;

              // grab the port
              if (portSplit.length > 1) {
                newRequest.header.port = portSplit[1];
              } else if (newProp.protocol === 'https') {
                newRequest.header.port = 443;
              } else {
                newRequest.header.port = 80;
              }
            }
          } else {
            // must just have the path - no protocol, host or port
            newRequest.header.path = res.headers.location;
          }

          // if there was a set cookie returned, need to set cookie on the new request
          if (res.headers['Set-Cookie']) {
            newRequest.header.headers.Cookie = res.headers['set-cookie'];
          }

          // make the modified request
          log.debug(`${origin}: REDIRECTING TO - ${res.headers.location}`);
          return makeRequest(newRequest, entitySchema, newProp, roundTTime, nextAtt, callback);
        }

        // callResp.timeouts = myTimeout;
        // turn healthy true - since valid return
        hlock.acquire(healthlock, (doneH2) => {
          healthy = true;
          doneH2(true);
        }, (retH2) => {
          log.debug(`${origin}: CALL RETURN ${JSON.stringify(callResp)}`);
          useProt = undefined;
          callResp.reqHdr = request.header.headers;
          return callback(callResp);
        });
      });
    });

    // handle post error
    httpRequest.on('error', (cerror) => {
      const tripDiff = process.hrtime(roundTTime);
      const tripEnd = `${Math.round(((tripDiff[0] * NS_PER_SEC) + tripDiff[1]) / 1000000)}ms`;
      const callResp = {
        status: 'failure',
        code: -1,
        message: cerror,
        request: {
          protocol: protStr,
          host: request.header.hostname,
          port: request.header.port,
          uri: request.header.path,
          method: request.header.method
        },
        redirects: attempt,
        tripTime: tripEnd
      };
      let didTimeout = 0;

      // if the connection was reset - need to determine if client or server cut it
      if (cerror.code === 'ECONNRESET') {
        const endTime = new Date().getTime();

        // see if this is a client timeout
        if (endTime - stTime >= myTimeout) {
          callResp.code = -2;
          didTimeout += 1;
        }
      }

      // turn healthy false - since error return
      hlock.acquire(healthlock, (doneH2) => {
        healthy = false;
        doneH2(true);
      }, (retH2) => {
        log.debug(`${origin}: ERROR RETURN ${JSON.stringify(callResp)}`);
        useProt = undefined;
        callResp.timeouts = didTimeout;
        callResp.reqHdr = request.header.headers;
        return callback(callResp);
      });
    });

    httpRequest.setTimeout(myTimeout, () => {
      httpRequest.abort();
    }, myTimeout);

    // write to the http request data to the body if not a get (gets do not have data in the body)
    if ((request.header.method !== 'GET' || entitySchema.sendGetBody)
        && ((typeof request.body === 'object' && Object.keys(request.body).length > 0)
        || (typeof request.body === 'string' && request.body !== '{}') || entitySchema.sendEmpty)) {
      if (entitySchema.requestDatatype.toUpperCase() === 'FORM') {
        // pass the formData to the request
        log.debug(`${origin}: Sending request body through formData`);
        formData.pipe(httpRequest);
      } else {
        // pass the body to the request
        log.debug(`${origin}: Sending request body normally`);
        httpRequest.write(request.body);
      }
    }

    // end the http request
    httpRequest.end();
  } catch (e) {
    // handle any exception
    const tripDiff = process.hrtime(roundTTime);
    const tripEnd = `${Math.round(((tripDiff[0] * NS_PER_SEC) + tripDiff[1]) / 1000000)}ms`;
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue during make request');
    errorObj.metrics = {
      code: 'translate_error',
      redirects: attempt,
      tripTime: tripEnd
    };
    errorObj.request = {
      protocol: protStr,
      host: request.header.hostname,
      port: request.header.port,
      uri: request.header.path,
      method: request.header.method,
      payload: request.body
    };
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: finds the token in the object that was returned
 */
function findPrimaryTokenInResult(result) {
  const origin = `${id}-connectorRest-findPrimaryTokenInResult`;
  log.trace(origin);
  let token = null;

  if (!result) {
    return token;
  }

  // if the result is a string, need to assume that is the token
  if (typeof result === 'string') {
    return result;
  }

  // if the token is in this object
  if (result.token) {
    return result.token;
  }

  // need to check objects within this object
  const keys = Object.keys(result);

  for (let k = 0; k < keys.length; k += 1) {
    if (typeof result[keys[k]] === 'object') {
      const found = findPrimaryTokenInResult(result[keys[k]]);

      // if we found the token
      if (found !== null) {
        token = found;
        break;
      }
    }
  }

  // return the found token
  return token;
}

/*
 * INTERNAL FUNCTION: finds the second token in the object that was returned
 */
function findSecondaryTokenInResult(result) {
  const origin = `${id}-connectorRest-findSecondaryTokenInResult`;
  log.trace(origin);
  let tokenp2 = null;

  if (!result) {
    return tokenp2;
  }

  // if the result is a string, need to assume that is the second token
  if (typeof result === 'string') {
    return result;
  }

  // if the second token is in this object
  if (result.tokenp2) {
    return result.tokenp2;
  }

  // need to check objects within this object
  const keys = Object.keys(result);

  for (let k = 0; k < keys.length; k += 1) {
    if (typeof result[keys[k]] === 'object') {
      const found = findSecondaryTokenInResult(result[keys[k]]);

      // if we found the second token
      if (found !== null) {
        tokenp2 = found;
        break;
      }
    }
  }

  // return the found second token
  return tokenp2;
}

/*
 * INTERNAL FUNCTION: finds the expiration in the object that was returned
 */
function findExpireInResult(result) {
  const origin = `${id}-connectorRest-findExpireInResult`;
  log.trace(origin);
  let expire = null;

  if (!result) {
    return expire;
  }

  // if the expires is in this object
  if (result.expires) {
    return new Date(result.expires).getTime();
  }

  // need to check objects within this object
  const keys = Object.keys(result);

  for (let k = 0; k < keys; k += 1) {
    if (typeof result[k] === 'object') {
      const found = findExpireInResult(result[k]);

      // if we found the expiration
      if (found !== null) {
        expire = new Date(result).getTime();
        break;
      }
    }
  }

  // return the found expiration
  return expire;
}

/*
 * INTERNAL FUNCTION: makes the request and processes the response
 * for the request to get the token
 */
function getToken(reqPath, options, tokenSchema, bodyString, callProperties, callback) {
  const origin = `${id}-connectorRest-getToken`;
  log.trace(origin);

  try {
    // no need for orig path since we handled the stub case
    const request = {
      header: options,
      body: bodyString,
      origPath: tokenSchema.entitypath
    };

    // set stub if that is the mode we are in
    let useStub = false;
    if (callProperties && Object.hasOwnProperty.call(callProperties, 'stub')) {
      useStub = callProperties.stub;
    } else if (stub) {
      useStub = stub;
    }

    // if there is a mock result, return that
    if (useStub && tokenSchema) {
      // get the data from the mock data file
      const tokenResp = returnStub(request, tokenSchema, callProperties);

      // if the request failed, return the error
      if (tokenResp.code < 200 || tokenResp.code > 299) {
        const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Authenticate', ['Token', tokenResp.code], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      log.debug(`${origin}: tokenResp.response`);

      // return the token from the token schema
      const translated = transUtilInst.mapFromOutboundEntity(tokenResp.response, tokenSchema.responseSchema);

      // if what we got back is an array, just return the first element
      // should only have one token!!!
      if (translated && Array.isArray(translated)) {
        return callback(translated[0]);
      }

      // return the token that we find in the translated object
      return callback(translated);
    }

    if (useStub || reqPath === tokenPath || (tokenSchema && reqPath === tokenSchema.entitypath)) {
      // do not make the call to return a token if the request is actually
      // to get a token. Getting a token to get a token -- that should go
      // direct to make request
      return callback({ token: 'faketoken' });
    }

    log.debug(`${origin}: OPTIONS: ${JSON.stringify(options)}`);

    // request the token
    return makeRequest(request, tokenSchema, callProperties, null, 0, (result, merror) => {
      if (merror) {
        return callback(null, merror);
      }

      // if the request failed, return the error
      if (result.code < 200 || result.code > 299) {
        const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Authenticate', ['Token', result.code], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      if (!tokenSchema) {
        // if no token schema, can not determine what to return
        const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Authenticate', ['Token', result.code], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      // parse the token out of the result
      const currResult = {
        token: null,
        tokenp2: null
      };

      // process primary token from header
      if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.token
          && tokenSchema.responseSchema.properties.token.placement && tokenSchema.responseSchema.properties.token.placement.toUpperCase() === 'HEADER') {
        if (!tokenSchema.responseSchema.properties.token.external_name) {
          const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Get Primary Token', ['Primary Token', result.code], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        const exName = tokenSchema.responseSchema.properties.token.external_name.toLowerCase();
        const headKeys = Object.keys(result.headers);
        let fullToken = null;
        let setCookie = null;

        // go through and find the token
        for (let h = 0; h < headKeys.length; h += 1) {
          if (headKeys[h].toLowerCase() === exName) {
            fullToken = result.headers[headKeys[h]];
            currResult.token = result.headers[headKeys[h]];
          }
          if (headKeys[h].toLowerCase() === 'set-cookie') {
            setCookie = result.headers[headKeys[h]];
          }
        }

        // if the exName field is an array
        if (exName === 'set-cookie' && fullToken && Array.isArray(fullToken)) {
          currResult.token = fullToken[0];
          for (let ex = 1; ex < fullToken.length; ex += 1) {
            currResult.token += `; ${fullToken[ex]}`;
          }
        }

        // if the token has been returned in the cookie
        if (exName.substring(0, 11) === 'set-cookie.') {
          const fname = exName.substring(11).toLowerCase();
          let thisCook = null;

          // if the cookie is an array - usual case
          if (setCookie && Array.isArray(setCookie)) {
            // go through the array looking for the defined token field
            for (let sc = 0; sc < setCookie.length; sc += 1) {
              // parses the cookie into an object
              thisCook = cookieHandler.parse(setCookie[sc]);
              const cookKeys = Object.keys(thisCook);
              let set = false;

              // go through the cookie to find the token
              for (let h = 0; h < cookKeys.length; h += 1) {
                if (cookKeys[h].toLowerCase() === fname) {
                  currResult.token = thisCook[cookKeys[h]];
                  set = true;
                  break;
                }
              }
              if (set) {
                break;
              }
            }
          } else if (setCookie) {
            // if the cookie is just one string, parse into an object
            thisCook = cookieHandler.parse(setCookie);
            const cookKeys = Object.keys(thisCook);

            // go through the cookie to find the token
            for (let h = 0; h < cookKeys.length; h += 1) {
              if (cookKeys[h].toLowerCase() === fname) {
                currResult.token = thisCook[cookKeys[h]];
                break;
              }
            }
          }
        }
      }

      // process second token from header
      if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.tokenp2
        && tokenSchema.responseSchema.properties.tokenp2.placement && tokenSchema.responseSchema.properties.tokenp2.placement.toUpperCase() === 'HEADER') {
        if (!tokenSchema.responseSchema.properties.tokenp2.external_name) {
          const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Get Secondary Token', ['Secondary Token', result.code], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        const exName = tokenSchema.responseSchema.properties.tokenp2.external_name.toLowerCase();
        const headKeys = Object.keys(result.headers);
        let fullToken = null;
        let setCookie = null;

        // go through and find the token
        for (let h = 0; h < headKeys.length; h += 1) {
          if (headKeys[h].toLowerCase() === exName) {
            fullToken = result.headers[headKeys[h]];
            currResult.tokenp2 = result.headers[headKeys[h]];
          }
          if (headKeys[h].toLowerCase() === 'set-cookie') {
            setCookie = result.headers[headKeys[h]];
          }
        }

        // if the exName field is an array
        if (exName === 'set-cookie' && fullToken && Array.isArray(fullToken)) {
          currResult.tokenp2 = fullToken[0];
          for (let ex = 1; ex < fullToken.length; ex += 1) {
            currResult.tokenp2 += `; ${fullToken[ex]}`;
          }
        }

        // if the token has been returned in the cookie
        if (exName.substring(0, 11) === 'set-cookie.') {
          const fname = exName.substring(11).toLowerCase();
          let thisCook = null;

          // if the cookie is an array - usual case
          if (setCookie && Array.isArray(setCookie)) {
            // go through the array looking for the defined token field
            for (let sc = 0; sc < setCookie.length; sc += 1) {
              // parses the cookie into an object
              thisCook = cookieHandler.parse(setCookie[sc]);
              const cookKeys = Object.keys(thisCook);
              let set = false;

              // go through the cookie to find the token
              for (let h = 0; h < cookKeys.length; h += 1) {
                if (cookKeys[h].toLowerCase() === fname) {
                  currResult.tokenp2 = thisCook[cookKeys[h]];
                  set = true;
                  break;
                }
              }
              if (set) {
                break;
              }
            }
          } else if (setCookie) {
            // if the cookie is just one string, parse into an object
            thisCook = cookieHandler.parse(setCookie);
            const cookKeys = Object.keys(thisCook);

            // go through the cookie to find the token
            for (let h = 0; h < cookKeys.length; h += 1) {
              if (cookKeys[h].toLowerCase() === fname) {
                currResult.tokenp2 = thisCook[cookKeys[h]];
                break;
              }
            }
          }
        }
      }

      // process the body
      // if response is just a string
      if (Object.hasOwnProperty.call(tokenSchema, 'responseDatatype') && tokenSchema.responseDatatype.toUpperCase() === 'PLAIN') {
        if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.token
          && tokenSchema.responseSchema.properties.token.placement && tokenSchema.responseSchema.properties.token.placement.toUpperCase() === 'BODY') {
          currResult.token = result.response;
        }
        if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.tokenp2
          && tokenSchema.responseSchema.properties.tokenp2.placement && tokenSchema.responseSchema.properties.tokenp2.placement.toUpperCase() === 'BODY') {
          currResult.tokenp2 = result.response;
        }
      }
      if (Object.hasOwnProperty.call(tokenSchema, 'responseDatatype') && tokenSchema.responseDatatype.toUpperCase() === 'XML') {
        if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.token
          && tokenSchema.responseSchema.properties.token.placement && tokenSchema.responseSchema.properties.token.placement.toUpperCase() === 'BODY') {
          currResult.token = result.response;
        }
        if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.tokenp2
          && tokenSchema.responseSchema.properties.tokenp2.placement && tokenSchema.responseSchema.properties.tokenp2.placement.toUpperCase() === 'BODY') {
          currResult.tokenp2 = result.response;
        }
      }

      // if response can be put into a JSON object
      let tempResult = null;
      if (result.response) {
        if (Object.hasOwnProperty.call(tokenSchema, 'responseDatatype') && tokenSchema.responseDatatype.toUpperCase() === 'XML2JSON') {
          try {
            const parser = new xml2js.Parser({ explicitArray: false, attrkey: '_attr' });
            parser.parseString(result.response, (error, presult) => {
              if (error) {
                log.warn(`${origin}: Unable to parse xml to json ${error}`);
                return callback(parser.toJson(presult));
              }
              tempResult = presult;
            });
          } catch (ex) {
            log.warn(`${origin}: Unable to get json from xml ${ex}`);
            if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.token
              && tokenSchema.responseSchema.properties.token.placement && tokenSchema.responseSchema.properties.token.placement.toUpperCase() === 'BODY') {
              currResult.token = result.response;
            }
            if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.tokenp2
              && tokenSchema.responseSchema.properties.tokenp2.placement && tokenSchema.responseSchema.properties.tokenp2.placement.toUpperCase() === 'BODY') {
              currResult.tokenp2 = result.response;
            }
          }
        }
        if (Object.hasOwnProperty.call(tokenSchema, 'responseDatatype') && tokenSchema.responseDatatype.toUpperCase() === 'URLENCODE') {
          tempResult = querystring.parse(result.response.trim());
        } else {
          try {
            tempResult = JSON.parse(result.response.trim());
          } catch (exc) {
            log.warn(exc);
          }
        }
      }

      // at this point if there is nothing in tempResult nothing further to do
      if (!tempResult) {
        return callback(currResult);
      }

      // need to see if there is a response key
      if (tokenSchema.responseObjects) {
        let tKey = null;

        // it should be an array - then take the first element of array
        if (Array.isArray(tokenSchema.responseObjects)) {
          if (tokenSchema.responseObjects[0].key) {
            tKey = tokenSchema.responseObjects[0].key;
          }
        } else if (tokenSchema.responseObjects.key) {
          tKey = tokenSchema.responseObjects.key;
        }

        // if we found a key, use it
        if (tKey) {
          tempResult = jsonQuery(tKey, { data: tempResult }).value;
        }
      }

      // the token should not be an array so if it is, return the 0 item.
      if (Array.isArray(tempResult)) {
        tempResult = tempResult[0];
      }

      // return the token from the token schema
      let translated = transUtilInst.mapFromOutboundEntity(tempResult, tokenSchema.responseSchema);

      // if what we got back is an array, just return the first element
      // should only have one token!!!
      if (translated && Array.isArray(translated)) {
        translated = translated[0];
      }

      if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.token
        && tokenSchema.responseSchema.properties.token.placement && tokenSchema.responseSchema.properties.token.placement.toUpperCase() === 'BODY') {
        currResult.token = translated.token;
      }
      if (tokenSchema.responseSchema && tokenSchema.responseSchema.properties && tokenSchema.responseSchema.properties.tokenp2
        && tokenSchema.responseSchema.properties.tokenp2.placement && tokenSchema.responseSchema.properties.tokenp2.placement.toUpperCase() === 'BODY') {
        currResult.tokenp2 = translated.tokenp2;
      }

      // return the token that we find in the translated object
      return callback(currResult);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue retrieving a token');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: prepares a request to get a token from the system
 */
function buildTokenRequest(reqPath, reqBody, callProperties, callback) {
  const origin = `${id}-connectorRest-buildTokenRequest`;
  log.trace(origin);

  try {
    let tokenSchema = null;

    try {
      // Get the token schema from the file system
      tokenSchema = propUtilInst.getEntitySchema('.system', 'getToken');
      log.debug(`${origin}: Using action and schema for token information`);
    } catch (ex) {
      // Token schema is not required (could be defined in
      // properties so ignore the exception!!!
      log.debug(`${origin}: Using adapter properties for token information`);
    }

    // prepare the additional headers we received
    let thisAHdata = null;

    if (tokenSchema) {
      thisAHdata = transUtilInst.mergeObjects(thisAHdata, tokenSchema.headers);
    }
    if (globalRequest) {
      thisAHdata = transUtilInst.mergeObjects(thisAHdata, globalRequest.addlHeaders);
    }

    // set up the right credentials - passed in overrides default
    let useUser = username;
    let usePass = password;

    if (callProperties && callProperties.authentication && callProperties.authentication.username) {
      useUser = callProperties.authentication.username;
    }
    if (callProperties && callProperties.authentication && callProperties.authentication.password) {
      usePass = callProperties.authentication.password;
    }

    // if no header data passed in create empty - will add data below
    if (!thisAHdata) {
      thisAHdata = {};
    } else {
      const hKeys = Object.keys(thisAHdata);

      for (let h = 0; h < hKeys.length; h += 1) {
        let tempStr = thisAHdata[hKeys[h]];

        // replace username variable
        if (tempStr.indexOf('{username}') >= 0) {
          tempStr = tempStr.replace('{username}', useUser);
        }
        // replace password variable
        if (tempStr.indexOf('{password}') >= 0) {
          tempStr = tempStr.replace('{password}', usePass);
        }
        thisAHdata[hKeys[h]] = tempStr;

        // handle any base64 encoding required on the authStr
        if (thisAHdata[hKeys[h]].indexOf('{b64}') >= 0) {
          // get the range to be encoded
          const sIndex = thisAHdata[hKeys[h]].indexOf('{b64}');
          const eIndex = thisAHdata[hKeys[h]].indexOf('{/b64}');

          // if start but no end - return an error
          if (sIndex >= 0 && eIndex < sIndex + 5) {
            const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Encode', [thisAHdata[hKeys[h]]], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }

          // get the string to be encoded
          const bufString = thisAHdata[hKeys[h]].substring(sIndex + 5, eIndex);

          // encode the string
          const encString = Buffer.from(bufString).toString('base64');
          let tempAuthStr = '';

          // build the new auth field with the encoded string
          if (sIndex > 0) {
            // add the start of the string that did not need encoding
            tempAuthStr = thisAHdata[hKeys[h]].substring(0, sIndex);
          }
          // add the encoded string
          tempAuthStr += encString;
          if (eIndex + 5 < thisAHdata[hKeys[h]].length) {
            // add the end of the string that did not need encoding
            tempAuthStr += thisAHdata[hKeys[h]].substring(eIndex + 6);
          }

          // put the temp string into the auth string we will add to the request
          thisAHdata[hKeys[h]] = tempAuthStr;
        }
      }
    }

    // set the Content Type headers based on the type of request data for the call
    if (thisAHdata['Content-Type'] === undefined || thisAHdata['Content-Type'] === null) {
      if (tokenSchema && tokenSchema.requestDatatype && tokenSchema.requestDatatype.toUpperCase() === 'PLAIN') {
        // add the Plain headers if they were not set already
        thisAHdata['Content-Type'] = 'text/plain';
      } else if (tokenSchema && tokenSchema.requestDatatype && tokenSchema.requestDatatype.toUpperCase() === 'XML') {
        // add the XML headers if they were not set already
        thisAHdata['Content-Type'] = 'application/xml';
      } else if (tokenSchema && tokenSchema.requestDatatype && tokenSchema.requestDatatype.toUpperCase() === 'URLENCODE') {
        // add the URLENCODE headers if they were not set already
        thisAHdata['Content-Type'] = 'application/x-www-form-urlencoded';
      } else {
        // add the JSON headers if they were not set already
        thisAHdata['Content-Type'] = 'application/json';
      }
    }
    // set the Accept headers based on the type of response data for the call
    if (thisAHdata.Accept === undefined || thisAHdata.Accept === null) {
      if (tokenSchema && tokenSchema.responseDatatype && tokenSchema.responseDatatype.toUpperCase() === 'PLAIN') {
        // add the Plain headers if they were not set already
        thisAHdata.Accept = 'text/plain';
      } else if (tokenSchema && tokenSchema.responseDatatype && tokenSchema.responseDatatype.toUpperCase() === 'XML') {
        // add the XML headers if they were not set already
        thisAHdata.Accept = 'application/xml';
      } else if (tokenSchema && tokenSchema.responseDatatype && tokenSchema.responseDatatype.toUpperCase() === 'URLENCODE') {
        // add the URLENCODE headers if they were not set already
        thisAHdata.Accept = 'application/x-www-form-urlencoded';
      } else {
        // add the JSON headers if they were not set already
        thisAHdata.Accept = 'application/json';
      }
    }

    if (thisAHdata.Accept === '') {
      delete thisAHdata.Accept;
    }
    if (thisAHdata['Content-Type'] === '') {
      delete thisAHdata['Content-Type'];
    }

    // set up the options for the call to get incidents - default is all
    const options = {
      hostname: host,
      port,
      path: tokenPath,
      method: 'POST',
      headers: thisAHdata
    };

    // passed in properties override defaults
    if (callProperties && callProperties.host) {
      options.hostname = callProperties.host;
    }
    if (callProperties && callProperties.port) {
      options.port = callProperties.port;
    }

    // specific token properties override everything (Single Sign On System)
    if (tokenSchema.sso && tokenSchema.sso.host) {
      options.hostname = tokenSchema.sso.host;
    }
    if (tokenSchema.sso && tokenSchema.sso.port) {
      options.port = tokenSchema.sso.port;
    }

    // If there is a token schema, need to take the data from there
    if (tokenSchema) {
      options.path = tokenSchema.entitypath;
      options.method = tokenSchema.method;
    }

    // if the path has a base path parameter in it, need to replace it
    let bpathStr = '{base_path}';
    if (options.path.indexOf(bpathStr) >= 0) {
      // be able to support this if the base path has a slash before it or not
      if (options.path.indexOf('/{base_path}') >= 0) {
        bpathStr = '/{base_path}';
      }

      // replace with base path if we have one, otherwise remove base path
      if (callProperties && callProperties.base_path) {
        // if no leading /, insert one
        if (callProperties.base_path.indexOf('/') !== 0) {
          options.path = options.path.replace(bpathStr, `/${callProperties.base_path}`);
        } else {
          options.path = options.path.replace(bpathStr, callProperties.base_path);
        }
      } else if (basepath) {
        // if no leading /, insert one
        if (basepath.indexOf('/') !== 0) {
          options.path = options.path.replace(bpathStr, `/${basepath}`);
        } else {
          options.path = options.path.replace(bpathStr, basepath);
        }
      } else {
        options.path = options.path.replace(bpathStr, '');
      }
    }

    // if the path has a version parameter in it, need to replace it
    let versStr = '{version}';
    if (options.path.indexOf(versStr) >= 0) {
      // be able to support this if the version has a slash before it or not
      if (options.path.indexOf('/{version}') >= 0) {
        versStr = '/{version}';
      }

      // replace with version if we have one, otherwise remove version
      if (callProperties && callProperties.version) {
        options.path = options.path.replace(versStr, `/${encodeURIComponent(callProperties.version)}`);
      } else if (version) {
        options.path = options.path.replace(versStr, `/${encodeURIComponent(version)}`);
      } else {
        options.path = options.path.replace(versStr, '');
      }
    }

    // if there are URI path variables that have been provided, need to add
    // them to the path
    if (reqBody && reqBody.uriPathVars && reqBody.uriPathVars.length > 0) {
      for (let p = 0; p < reqBody.uriPathVars.length; p += 1) {
        const vnum = p + 1;
        const holder = `pathv${vnum.toString()}`;
        const hindex = options.path.indexOf(holder);

        // if path variable is in the url, replace it!!!
        if (hindex >= 0 && reqBody.uriPathVars[p] !== null && reqBody.uriPathVars[p] !== '') {
          // with the provided id
          let idString = '';

          // check if the current URI path ends with a slash (may require
          // slash at end)
          if (options.path[hindex - 2] === '/' || options.path[hindex - 2] === ':') {
            // ends with a slash need to add slash to end
            idString = encodeURIComponent(reqBody.uriPathVars[p]);
          } else {
            // otherwise add / to start
            idString = '/';
            idString += encodeURIComponent(reqBody.uriPathVars[p]);
          }

          // replace the id in url with the id string
          options.path = options.path.replace(`{${holder}}`, idString);
        }
      }
    }

    // need to remove all of the remaining path holders from the URI
    while (options.path.indexOf('{pathv') >= 0) {
      let sIndex = options.path.indexOf('{pathv');
      const eIndex = options.path.indexOf('}', sIndex);

      // if there is a / before the {pathv} need to remove it
      if (options.path[sIndex - 1] === '/' || options.path[sIndex - 1] === ':') {
        sIndex -= 1;
      }

      if (sIndex > 0) {
        // add the start of the path
        let tempStr = options.path.substring(0, sIndex);

        if (eIndex < options.path.length) {
          // add the end of the path
          tempStr += options.path.substring(eIndex + 1);
        }

        options.path = tempStr;
      } else if (eIndex > 0 && eIndex < options.path.length) {
        // add the end of the path
        options.path = options.path.substring(eIndex + 1);
      } else {
        // should not get here - there is some issue in the uripath - missing
        // an end or the path is just {pathv#}
        // add the specific pieces of the error object
        const errorObj = this.transUtil.formatErrorObject(origin, 'Invalid Action File', ['missing entity path', '.system/getToken'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
    }

    // remove the path vars from the reqBody
    const actReqBody = Object.assign({}, reqBody);
    if (actReqBody && actReqBody.uriPathVars) {
      delete actReqBody.uriPathVars;
    }

    // if ssl enabled add the options for ssl
    if (callProperties && callProperties.ssl && Object.hasOwnProperty.call(callProperties.ssl, 'enabled')) {
      if (callProperties.ssl.enabled) {
        if (callProperties.ssl.accept_invalid_cert) {
          // if we are accepting invalid certificates (ok for lab not so much production)
          options.rejectUnauthorized = false;
        } else {
          // if we are not accepting invalid certs, need the ca file in the options
          try {
            options.rejectUnauthorized = true;
            options.ca = [fs.readFileSync(callProperties.ssl.ca_file)];
          } catch (e) {
            const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [callProperties.ssl.ca_file], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
        }

        if (callProperties.ssl.ciphers) {
          options.ciphers = callProperties.ssl.ciphers;
        }
        if (callProperties.ssl.secure_protocol) {
          options.secureProtocol = callProperties.ssl.secure_protocol;
        }

        log.info(`${origin}: Connector SSL connections enabled`);
      }
    } else if (sslEnabled) {
      if (sslAcceptInvalid) {
        // if we are accepting invalid certificates (ok for lab not so much production)
        options.rejectUnauthorized = false;
      } else {
        // if we are not accepting invalid certs, need the ca file in the options
        try {
          options.rejectUnauthorized = true;
          options.ca = [fs.readFileSync(sslCAFile)];
        } catch (e) {
          const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCAFile], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }
        // if there is a cert file, try to read in a cert file in the options
        if (sslCertFile) {
          try {
            options.cert = [fs.readFileSync(sslCertFile)];
          } catch (e) {
            const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCertFile], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
        }
        // if there is a key file, try to read in a key file in the options
        if (sslKeyFile) {
          try {
            options.key = [fs.readFileSync(sslKeyFile)];
          } catch (e) {
            const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslKeyFile], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
        }
      }

      if (sslCiphers) {
        options.ciphers = sslCiphers;
      }
      if (secureProtocol) {
        options.secureProtocol = secureProtocol;
      }

      log.info(`${origin}: Connector SSL connections enabled`);
    }

    const reqData = {};
    let bodyString = null;

    // if we have been passed a schema and the Authorization is in a header
    if (tokenSchema && tokenSchema.headers && tokenSchema.headers.Authorization) {
      // request the token
      options.headers['Content-length'] = 0;
      return getToken(reqPath, options, tokenSchema, '', callProperties, callback);
    }
    if (tokenSchema) {
      // if this is a get, need to put the username on the url
      if (options.path.indexOf('/{username}') >= 0) {
        options.path = options.path.replace('/{username}', useUser);
      } else {
        options.path = options.path.replace('{username}', useUser);
      }

      // if this is a get, need to put the password on the url
      if (options.path.indexOf('/{password}') >= 0) {
        options.path = options.path.replace('/{password}', usePass);
      } else {
        options.path = options.path.replace('{password}', usePass);
      }

      // if this is not a get, need to add the info to the request
      if (options.method !== 'GET') {
        let creds = {
          username: useUser,
          password: usePass
        };

        // if there is body data to add to the token request body
        creds = transUtilInst.mergeObjects(actReqBody, creds);

        if (globalRequest) {
          creds = transUtilInst.mergeObjects(creds, globalRequest.authData);
        }

        // if there is body data to add to the token request body
        if (actReqBody) {
          const bodyKey = Object.keys(actReqBody);

          for (let k = 0; k < bodyKey.length; k += 1) {
            creds[bodyKey[k]] = actReqBody[bodyKey[k]];
          }
        }

        // map the data we received to an Entity - will get back the defaults
        const tokenEntity = transUtilInst.mapToOutboundEntity(creds, tokenSchema.requestSchema);
        bodyString = tokenEntity;

        // if it is JSON or URLENCODE need to put body into right format
        if (!tokenSchema.requestDatatype || tokenSchema.requestDatatype.toUpperCase() === 'JSON') {
          bodyString = JSON.stringify(tokenEntity);
        } else if (tokenSchema.requestDatatype && tokenSchema.requestDatatype.toUpperCase() === 'URLENCODE') {
          bodyString = querystring.stringify(tokenEntity);
        }

        // if there is a body, set the content length of the body and add it to
        // the header
        if (Object.keys(tokenEntity).length > 0) {
          options.headers['Content-length'] = Buffer.byteLength(bodyString);
        }

        // request the token
        return getToken(reqPath, options, tokenSchema, bodyString, callProperties, callback);
      }
    } else {
      // set the user and password for the token request
      reqData[tokenUserField] = useUser;
      reqData[tokenPwdField] = usePass;
      bodyString = reqData;

      // since not a get call, convert reqData to a string and add length
      bodyString = JSON.stringify(reqData);
      options.headers['Content-length'] = Buffer.byteLength(bodyString);
    }

    // request the token
    return getToken(reqPath, options, tokenSchema, bodyString, callProperties, callback);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue requesting token');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: addTokenItem is used to add a token item to the
 *    memory based on the user provided
 */
function addTokenItem(user, reqBody, token, timeout, callback) {
  const origin = `${id}-connectorRest-addTokenItem`;
  log.trace(origin);

  try {
    // there is already a lock from the get, so no need to lock the tokenList
    const now = new Date();

    // create the token item
    let tkey = `${id}__%%__${user}`;
    const tokenItem = {
      adapter: id,
      username: user,
      token,
      start: now.getTime(),
      expire: timeout
    };

    if (reqBody) {
      tkey += `__%%__${JSON.stringify(reqBody)}`;
    }

    tokenItem.tkey = tkey;

    // add the token item to the tokenlist and return it
    if (tokenCache === 'local') {
      tokenList.push(tokenItem);
      return callback(token);
    }

    // try to add the token item to the redis local memory
    return g_redis.set(tkey, JSON.stringify(tokenItem), (err) => {
      if (err) {
        log.error(`${origin}: ${tkey} not stored in redis`);
      }

      // return the token
      return callback(token);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue adding token');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: retrieve the token from cache and determine if
 * it valid. if valid, return it otherwise return null
 */
function validToken(user, reqBody, invalidToken, callback) {
  const origin = `${id}-connectorRest-validToken`;
  log.trace(origin);

  try {
    // time to see if the token is expired
    const expireCheck = new Date().getTime() + 60000;

    // get the token from redis
    let tkey = `${id}__%%__${user}`;

    if (reqBody) {
      tkey += `__%%__${JSON.stringify(reqBody)}`;
    }

    // determine where to get the token
    if (tokenCache === 'redis') {
      return g_redis.get(tkey, (err, res) => {
        if (err) {
          log.error(`${origin}: Error on retrieve token for ${tkey}`);
        }

        // if there was no token returned
        if (!res) {
          return callback(null);
        }

        const parsedToken = JSON.parse(res);

        // if the token expired (or will expire within a minute),
        // or it is invalid (failed) remove it from the token list
        if (parsedToken.expire < expireCheck || parsedToken.token.token === invalidToken) {
          return g_redis.del(tkey, (derr) => {
            if (derr) {
              log.debug(`${origin}: Expired token for ${tkey} not removed from redis`);
            }

            return callback(null);
          });
        }

        // return the retrieved token
        return callback(parsedToken.token);
      });
    }

    let retToken = null;

    // find this user's token in the list
    for (let i = 0; i < tokenList.length; i += 1) {
      if (tokenList[i].tkey === tkey) {
        // if the token expired (or will expire within a minute),
        // or it is invalid (failed) remove it from the token list
        if (tokenList[i].expire < expireCheck
            || tokenList[i].token.token === invalidToken) {
          tokenList.splice(i, 1);
          break;
        }

        retToken = tokenList[i].token;
        break;
      }
    }
    return callback(retToken);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue validating cached token');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: getTokenItem is used to retrieve a token items from
 *    memory based on the user provided
 */
function getTokenItem(pathForToken, user, reqBody, invalidToken, callProperties, callback) {
  const origin = `${id}-connectorRest-getTokenItem`;
  log.trace(origin);

  try {
    // Lock the tokenList while getting the token
    return tlock.acquire(tokenlock, (done) => {
      validToken(user, reqBody, invalidToken, (retToken, verror) => {
        if (verror) {
          done(null, verror);
        }

        // If valid token found, return it and skip the rest
        if (retToken !== null) {
          done(retToken, null);
        } else {
          // No valid token found, Need to get a new token and add it to the token list
          buildTokenRequest(pathForToken, reqBody, callProperties, (dyntoken, berror) => {
            if (berror) {
              done(null, berror);
            }

            let timeout = tokenTimeout;

            // if we should use the timeout from the token request
            if (timeout === 0) {
              timeout = findExpireInResult(dyntoken);
            } else {
              // otherwise add the timeout to the current time
              timeout += new Date().getTime();
            }

            const tokenObj = {
              token: findPrimaryTokenInResult(dyntoken),
              tokenp2: findSecondaryTokenInResult(dyntoken)
            };

            // if this is worth caching
            if (timeout && timeout > 0) {
              // since this is adding the token for future use, do not care when it comes back
              addTokenItem(user, reqBody, tokenObj, timeout, (addedtoken, aerror) => {
                if (aerror) {
                  done(null, aerror);
                }
                done(tokenObj, null);
              });
            } else {
              done(tokenObj, null);
            }
          });
        }
      });
    }, (ret, error) => {
      if (error) {
        return callback(null, error);
      }

      return callback(ret);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue getting token item');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: addAuthToRequest determines the place to add the authentication
 * and adds the authentication string
 */
function addAuthToRequest(request, authStrs, callProperties, callback) {
  const origin = `${id}-connectorRest-addAuthToRequest`;
  log.trace(origin);

  try {
    const newAuthStrs = [];
    for (let a = 0; a < authStrs.length; a += 1) {
      // handle any base64 encoding required on the authStrs
      if (authStrs[a].indexOf('{b64}') >= 0) {
        // get the range to be encoded
        const sIndex = authStrs[a].indexOf('{b64}');
        const eIndex = authStrs[a].indexOf('{/b64}');

        // if start but no end - return an error
        if (sIndex >= 0 && eIndex < sIndex + 5) {
          const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Encode', [authStrs[a]], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        // get the string to be encoded
        const bufString = authStrs[a].substring(sIndex + 5, eIndex);

        // encode the string
        const encString = Buffer.from(bufString).toString('base64');
        let tempAuthStr = '';

        // build the new auth field with the encoded string
        if (sIndex > 0) {
          // add the start of the string that did not need encoding
          tempAuthStr = authStrs[a].substring(0, sIndex);
        }
        // add the encoded string
        tempAuthStr += encString;
        if (eIndex + 5 < authStrs[a].length) {
          // add the end of the string that did not need encoding
          tempAuthStr += authStrs[a].substring(eIndex + 6);
        }

        // put the temp string into the auth string we will add to the request
        newAuthStrs.push(tempAuthStr);
      } else {
        newAuthStrs.push(authStrs[a]);
      }
    }

    // add the authentication field to the request
    let useAuthField = authField;
    if (callProperties && callProperties.authentication && callProperties.authentication.auth_field) {
      if (Array.isArray(callProperties.authentication.auth_field)) {
        useAuthField = callProperties.authentication.auth_field;
      } else {
        useAuthField = [callProperties.authentication.auth_field];
      }
    }

    // Auth Strings need to be at least as long as auth fields so if less, buffer with the first auth string
    if (useAuthField.length > newAuthStrs.length) {
      for (let t = newAuthStrs.length; t < useAuthField.length; t += 1) {
        newAuthStrs.push(newAuthStrs[0]);
      }
    }

    // need to go through and put in place all of the auth strings.
    for (let af = 0; af < useAuthField.length; af += 1) {
      const authPath = useAuthField[af].split('.');

      // if the token is going to be put in the url (url.xxxx)
      if (authPath[0] === 'urlpath') {
        // if there is already query info, insert the token
        if (request.header.path.indexOf('?') >= 0) {
          let temp = request.header.path.substring(0, request.header.path.indexOf('?'));
          temp += `/${newAuthStrs[af]}?`;
          temp += request.header.path.substring(request.header.path.indexOf('?') + 1);
          request.header.path = temp;
        } else {
          // append the token
          request.header.path += `/${newAuthStrs[af]}`;
        }
      } else if (authPath[0] === 'url') {
        // if there is already query info, insert the token
        if (request.header.path.indexOf('?') >= 0) {
          let temp = request.header.path.substring(0, request.header.path.indexOf('?'));
          temp += `?${newAuthStrs[af]}&`;
          temp += request.header.path.substring(request.header.path.indexOf('?') + 1);
          request.header.path = temp;
        } else {
          // append the token
          request.header.path += `?${newAuthStrs[af]}`;
        }
      } else if (authPath[0] === 'body' && typeof request.body === 'string') {
        // if adding to body and it is already stringified
        let jbody = JSON.parse(request.body);
        // get to the field in the object (header.xxxx or body.xxxxx)
        for (let a = 1; a < authPath.length; a += 1) {
          // if we are at the point to add the token
          if (a === authPath.length - 1) {
            jbody[authPath[a]] = newAuthStrs[af];
          } else {
            // if the field is not defined in the request, add it
            if (jbody[authPath[a]] === undefined) {
              jbody[authPath[a]] = {};
            }

            // set the jbody to the next field in the path
            jbody = jbody[authPath[a]];
          }
        }
        request.body = JSON.stringify(jbody);
        request.header.headers['Content-length'] = Buffer.byteLength(request.body);
      } else {
        let tempField = request;

        // get to the field in the object (header.xxxx or body.xxxxx)
        for (let a = 0; a < authPath.length; a += 1) {
          // if we are at the point to add the token
          if (a === authPath.length - 1) {
            tempField[authPath[a]] = newAuthStrs[af];
          } else {
            // if the field is not defined in the request, add it
            if (tempField[authPath[a]] === undefined) {
              tempField[authPath[a]] = {};
            }

            // set the tempField to the next field in the path
            tempField = tempField[authPath[a]];
          }
        }
      }
    }

    // auth string is already in the request object
    return callback(newAuthStrs);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue adding authentication');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: requestAuthenticate determines the authentication for System,
 * and takes appropriate action to authenticate and then makes the request
 */
function requestAuthenticate(request, entitySchema, invalidToken, callProperties, callback) {
  const origin = `${id}-connectorRest-requestAuthenticate`;
  log.trace(origin);

  try {
    // set up the right credentials - passed in overrides default
    let useUser = username;
    let usePass = password;

    if (callProperties && callProperties.authentication && callProperties.authentication.username) {
      useUser = callProperties.authentication.username;
    }
    if (callProperties && callProperties.authentication && callProperties.authentication.password) {
      usePass = callProperties.authentication.password;
    }

    if (authMethod === 'request_token') {
      // are we working with reusing tokens until they expire?
      if (tokenTimeout >= 0) {
        // get the token from the token list
        return getTokenItem(request.header.path, useUser, request.authData, invalidToken, callProperties, (tres, terror) => {
          if (terror) {
            return callback(null, terror);
          }

          // if we got a valid token, use it
          if (!tres || !tres.token) {
            const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Get Token', [useUser], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }

          // format the authentication string
          log.debug(`${origin}: ${JSON.stringify(tres)} being used for user: ${useUser}`);
          const authStrs = [];
          if (callProperties && callProperties.authentication && callProperties.authentication.auth_field_format) {
            if (Array.isArray(callProperties.authentication.auth_field_format)) {
              for (let a = 0; a < callProperties.authentication.auth_field_format.length; a += 1) {
                let authStr = callProperties.authentication.auth_field_format[a].replace('{token}', tres.token);
                authStr = authStr.replace('{tokenp2}', tres.tokenp2);
                authStr = authStr.replace('{username}', useUser);
                authStr = authStr.replace('{password}', usePass);
                authStrs.push(authStr);
              }
            } else {
              let authStr = callProperties.authentication.auth_field_format.replace('{token}', tres.token);
              authStr = authStr.replace('{tokenp2}', tres.tokenp2);
              authStr = authStr.replace('{username}', useUser);
              authStr = authStr.replace('{password}', usePass);
              authStrs.push(authStr);
            }
          } else {
            for (let a = 0; a < authFormat.length; a += 1) {
              let authStr = authFormat[a].replace('{token}', tres.token);
              authStr = authStr.replace('{tokenp2}', tres.tokenp2);
              authStr = authStr.replace('{username}', useUser);
              authStr = authStr.replace('{password}', usePass);
              authStrs.push(authStr);
            }
          }

          return addAuthToRequest(request, authStrs, callProperties, (authReq, aerror) => {
            if (aerror) {
              return callback(aerror);
            }

            request.tokenUsed = tres.token;

            // actually make the request now that the authentication has been added
            return makeRequest(request, entitySchema, callProperties, null, 0, callback);
          });
        });
      }

      // if no token timeout then no token list - just get the token and continue
      return buildTokenRequest(request.header.path, request.authData, callProperties, (dyntoken, berror) => {
        if (berror) {
          return callback(null, berror);
        }

        // format the authentication string
        const tokenObj = {
          token: findPrimaryTokenInResult(dyntoken),
          tokenp2: findSecondaryTokenInResult(dyntoken)
        };

        if (!tokenObj || !tokenObj.token) {
          const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Get Token', [useUser], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        log.debug(`${origin}: ${JSON.stringify(tokenObj)} being used for user: ${useUser}`);
        const authStrs = [];
        if (callProperties && callProperties.authentication && callProperties.authentication.auth_field_format) {
          if (Array.isArray(callProperties.authentication.auth_field_format)) {
            for (let a = 0; a < callProperties.authentication.auth_field_format.length; a += 1) {
              let authStr = callProperties.authentication.auth_field_format[a].replace('{token}', tokenObj.token);
              authStr = authStr.replace('{tokenp2}', tokenObj.tokenp2);
              authStr = authStr.replace('{username}', useUser);
              authStr = authStr.replace('{password}', usePass);
              authStrs.push(authStr);
            }
          } else {
            let authStr = callProperties.authentication.auth_field_format.replace('{token}', tokenObj.token);
            authStr = authStr.replace('{tokenp2}', tokenObj.tokenp2);
            authStr = authStr.replace('{username}', useUser);
            authStr = authStr.replace('{password}', usePass);
            authStrs.push(authStr);
          }
        } else {
          for (let a = 0; a < authFormat.length; a += 1) {
            let authStr = authFormat[a].replace('{token}', tokenObj.token);
            authStr = authStr.replace('{tokenp2}', tokenObj.tokenp2);
            authStr = authStr.replace('{username}', useUser);
            authStr = authStr.replace('{password}', usePass);
            authStrs.push(authStr);
          }
        }

        return addAuthToRequest(request, authStrs, callProperties, (authReq, aerror) => {
          if (aerror) {
            return callback(aerror);
          }

          // actually make the request now that the authentication has been added
          return makeRequest(request, entitySchema, callProperties, null, 0, callback);
        });
      });
    }

    if (authMethod === 'jwt_token') {
      // format the authentication string
      let useToken = staticToken;

      // create payload and secret (from prepoerties)
      const secret = usePass;
      const payload = {
        iss: useUser,
        exp: new Date().getTime() + (tokenTimeout / 1000) + 30
      };

      // sign token with API Secret
      useToken = jwt.sign(payload, secret);

      const authStrs = [];
      if (callProperties && callProperties.authentication && callProperties.authentication.auth_field_format) {
        if (Array.isArray(callProperties.authentication.auth_field_format)) {
          for (let a = 0; a < callProperties.authentication.auth_field_format.length; a += 1) {
            let authStr = callProperties.authentication.auth_field_format[a].replace('{token}', useToken);
            authStr = authStr.replace('{username}', useUser);
            authStr = authStr.replace('{password}', usePass);
            authStrs.push(authStr);
          }
        } else {
          let authStr = callProperties.authentication.auth_field_format.replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      } else {
        for (let a = 0; a < authFormat.length; a += 1) {
          let authStr = authFormat[a].replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      }

      return addAuthToRequest(request, authStrs, callProperties, (authReq, aerror) => {
        if (aerror) {
          return callback(aerror);
        }

        // actually make the request now that the authentication has been added
        return makeRequest(request, entitySchema, callProperties, null, 0, callback);
      });
    }

    if (authMethod === 'static_token') {
      // format the authentication string
      let useToken = staticToken;
      if (callProperties && callProperties.authentication && callProperties.authentication.token) {
        useToken = callProperties.authentication.token;
      }

      const authStrs = [];
      if (callProperties && callProperties.authentication && callProperties.authentication.auth_field_format) {
        if (Array.isArray(callProperties.authentication.auth_field_format)) {
          for (let a = 0; a < callProperties.authentication.auth_field_format.length; a += 1) {
            let authStr = callProperties.authentication.auth_field_format[a].replace('{token}', useToken);
            authStr = authStr.replace('{username}', useUser);
            authStr = authStr.replace('{password}', usePass);
            authStrs.push(authStr);
          }
        } else {
          let authStr = callProperties.authentication.auth_field_format.replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      } else {
        for (let a = 0; a < authFormat.length; a += 1) {
          let authStr = authFormat[a].replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      }

      return addAuthToRequest(request, authStrs, callProperties, (authReq, aerror) => {
        if (aerror) {
          return callback(aerror);
        }

        // actually make the request now that the authentication has been added
        return makeRequest(request, entitySchema, callProperties, null, 0, callback);
      });
    }

    if (authMethod === 'basic user_password') {
      // format the authentication string
      let useToken = staticToken;
      if (callProperties && callProperties.authentication && callProperties.authentication.token) {
        useToken = callProperties.authentication.token;
      }

      const authStrs = [];
      if (callProperties && callProperties.authentication && callProperties.authentication.auth_field_format) {
        if (Array.isArray(callProperties.authentication.auth_field_format)) {
          for (let a = 0; a < callProperties.authentication.auth_field_format.length; a += 1) {
            let authStr = callProperties.authentication.auth_field_format[a].replace('{token}', useToken);
            authStr = authStr.replace('{username}', useUser);
            authStr = authStr.replace('{password}', usePass);
            authStrs.push(authStr);
          }
        } else {
          let authStr = callProperties.authentication.auth_field_format.replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      } else {
        for (let a = 0; a < authFormat.length; a += 1) {
          let authStr = authFormat[a].replace('{token}', useToken);
          authStr = authStr.replace('{username}', useUser);
          authStr = authStr.replace('{password}', usePass);
          authStrs.push(authStr);
        }
      }

      return addAuthToRequest(request, authStrs, callProperties, (authReq, aerror) => {
        if (aerror) {
          return callback(aerror);
        }

        // actually make the request now that the authentication has been added
        return makeRequest(request, entitySchema, callProperties, null, 0, callback);
      });
    }
    // if no_authentication, there is no change to the request!

    // actual call to make the request
    return makeRequest(request, entitySchema, callProperties, null, 0, callback);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue authentication');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: archiveResult is used to archive the results of the request along with
 *    the times that were taken at various steps in the process (wait, request and overall)
 */
function archiveResult(reqResult, callback) {
  const origin = `${id}-connectorRest-archiveResult`;
  log.trace(origin);

  try {
    if (!archiving) {
      return 'skipped';
    }

    // Add an Id to the result object that we are going to store in the database
    const data = reqResult;
    data._id = uuid.v4();

    dbUtilInst.findAndModify(archiveColl, { _id: data._id }, null, data, true, (error, result) => {
      if (error) {
        const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Save To Database', [error], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      return callback(result);
    });

    // save the result object to the database
    // return brokers.persistence.save(archiveColl, data._id, data, (result, error) => {
    // if (error) {
    //   const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Save To Database', [error], null, null, null);
    //   log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
    //   return callback(null, errorObj);
    // }

    // return callback(result);
    // });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue archiving');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: handleEndResponse prepares the response to send back
 */
function handleEndResponse(request, makeResp, overallTime, callback) {
  const origin = `${id}-connectorRest-handleEndResponse`;
  log.trace(origin);

  try {
    log.info(`${origin}: Request call to ${request.header.method} ${request.header.path}: Call took: ${makeResp.tripTime || 0}`);

    const reqResult = {
      phInstance,
      request,
      overallEnd: makeResp.tripTime || 0,
      response: makeResp
    };

    // archive the results
    try {
      archiveResult(reqResult, (archived, aerror) => {
        if (aerror) {
          log.spam(`${origin}: Error Archived: ${JSON.stringify(aerror)}`);
        } else {
          log.spam(`${origin}: Result Archived: ${JSON.stringify(archived)}`);
        }
      });
    } catch (ex) {
      log.spam(`${origin}: Swallowing the archive error`);
    }

    // If the request was successful
    if (makeResp && makeResp.code && Number(makeResp.code) >= 200 && Number(makeResp.code) <= 299) {
      const newResp = {
        icode: `AD.${makeResp.code}`,
        response: makeResp.response,
        headers: makeResp.headers,
        requestCookie: makeResp.requestCookie,
        reqHdr: makeResp.reqHdr,
        metrics: {
          code: makeResp.code,
          timeouts: makeResp.timeouts || 0,
          redirects: makeResp.redirects || 0,
          retries: makeResp.retries || 0,
          tripTime: parseFloat(makeResp.tripTime) || 0,
          isThrottling: false
        }
      };
      if (returnRequest) {
        newResp.request = makeResp.request;
      }

      // return the response object
      return callback(newResp);
    }

    // if not successful - check to see if response or error
    let errorObj = {};
    if (makeResp) {
      if (!returnRequest && makeResp.request) {
        delete makeResp.request;
      }

      if (makeResp.code === -2) {
        errorObj = transUtilInst.formatErrorObject(origin, 'Request Timeout', [makeResp.code], makeResp.code, makeResp, null);
      } else {
        errorObj = transUtilInst.formatErrorObject(origin, 'Error On Request', [makeResp.code], makeResp.code, makeResp, null);
      }

      errorObj.metrics = {
        code: makeResp.code,
        timeouts: makeResp.timeouts || 0,
        redirects: makeResp.redirects || 0,
        retries: makeResp.retries || 0,
        tripTime: parseFloat(makeResp.tripTime) || 0,
        isThrottling: false
      };
    } else {
      errorObj = transUtilInst.formatErrorObject(origin, 'Error On Request', ['unknown'], null, null, null);
      errorObj.metrics = {
        code: 'unknown',
        timeouts: 0,
        redirects: 0,
        retries: 0,
        tripTime: 0,
        isThrottling: false
      };
    }
    errorObj.response = makeResp.response;

    if (makeResp && makeResp.code === -1) {
      if (typeof errorObj.IAPerror.raw_response === 'object') {
        log.error(`${origin}: ${errorObj.IAPerror.displayString}: ${JSON.stringify(errorObj.IAPerror.raw_response)}`);
      } else {
        log.error(`${origin}: ${errorObj.IAPerror.displayString}: ${errorObj.IAPerror.raw_response}`);
      }
    } else {
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
    }

    return callback(null, errorObj);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue handling response');
    errorObj.metrics = {};
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: handleEndThrottleResponse prepares the response to send back
 *    specifically if throttling is turned on - to clean up the request in the queue
 */
function handleEndThrottleResponse(request, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, callback) {
  const origin = `${id}-connectorRest-handleEndThrottleResponse`;
  log.trace(origin);

  const reqDiff = process.hrtime(reqTime);
  const reqEnd = (reqDiff[0] * NS_PER_SEC) + reqDiff[1];
  const overallDiff = process.hrtime(overallTime);
  const overallEnd = `${Math.round(((overallDiff[0] * NS_PER_SEC) + overallDiff[1]) / 1000000)}ms`;

  try {
    log.info(`${origin}: Request ${claimedLic.request_id} Transaction ${myTransTime} call to ${request.header.method} ${request.header.path}: Call took ${reqEnd} - Overall Time: ${overallEnd}`);

    // finish the turn so the space can be freed
    return throttleEng.finishTurn(claimedLic, reqEnd, (fres, ferror) => {
      const reqResult = {
        phInstance,
        requestId: claimedLic.request_id,
        transNum: claimedLic.transNum,
        request,
        start: claimedLic.start,
        end: new Date().getTime(),
        waitEnd,
        reqEnd,
        overallEnd,
        response: makeResp
      };

      if (!ferror) {
        reqResult.end = fres.end;
      }

      // archive the results
      try {
        archiveResult(reqResult, (archived, aerror) => {
          if (aerror) {
            log.spam(`${origin}: Error Archived: ${JSON.stringify(aerror)}`);
          } else {
            log.spam(`${origin}: Result Archived: ${JSON.stringify(archived)}`);
          }
        });
      } catch (ex) {
        log.spam(`${origin}: Swallowing the archive error`);
      }

      // If the request was successful
      if (makeResp !== null && makeResp.code !== undefined
          && Number(makeResp.code) >= 200 && Number(makeResp.code) <= 299) {
        const newResp = {
          icode: `AD.${makeResp.code}`,
          response: makeResp.response,
          headers: makeResp.headers,
          requestCookie: makeResp.requestCookie,
          reqHdr: makeResp.reqHdr,
          event: claimedLic.event,
          metrics: {
            code: makeResp.code,
            timeouts: makeResp.timeouts || 0,
            redirects: makeResp.redirects || 0,
            retries: makeResp.retries || 0,
            tripTime: parseFloat(makeResp.tripTime) || 0,
            queueTime: waitEnd,
            isThrottling: true
          }
        };
        if (returnRequest) {
          newResp.request = makeResp.request;
        }

        // return the response object
        return callback(newResp);
      }

      // if not successful - check to see if response or error
      let errorObj = {};
      if (makeResp) {
        if (!returnRequest && makeResp.request) {
          delete makeResp.request;
        }

        if (makeResp.code === -2) {
          errorObj = transUtilInst.formatErrorObject(origin, 'Request Timeout', [makeResp.code], makeResp.code, makeResp, null);
        } else {
          errorObj = transUtilInst.formatErrorObject(origin, 'Error On Request', [makeResp.code], makeResp.code, makeResp, null);
        }

        errorObj.metrics = {
          code: makeResp.code,
          timeouts: makeResp.timeouts || 0,
          redirects: makeResp.redirects || 0,
          retries: makeResp.retries || 0,
          tripTime: parseFloat(makeResp.tripTime) || 0,
          queueTime: waitEnd,
          isThrottling: true
        };
      } else {
        errorObj = transUtilInst.formatErrorObject(origin, 'Error On Request', ['unknown'], null, null, null);
        errorObj.metrics = {
          code: 'unknown',
          timeouts: 0,
          redirects: 0,
          retries: 0,
          tripTime: 0,
          queueTime: waitEnd,
          isThrottling: true
        };
      }
      errorObj.response = makeResp.response;

      if (makeResp && makeResp.code === -1) {
        log.error(`${origin}: ${errorObj.IAPerror.displayString}: ${errorObj.IAPerror.raw_response}`);
      } else {
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      }

      errorObj.event = claimedLic.event;
      return callback(null, errorObj);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue handling response');
    errorObj.metrics = {};
    errorObj.event = claimedLic.event;
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: re-attempt after an invalid response and figure out
 * how to handle the new response.
 */
function retryInvalidResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback) {
  const origin = `${id}-connectorRest-retryInvalidResponse`;
  log.trace(origin);

  try {
    // Need to get a new token and retry the request
    return requestAuthenticate(request, entitySchema, request.tokenUsed, callProperties, (mres, merror) => {
      if (merror) {
        return callback(null, merror);
      }

      let retryError = limitRetryError;
      let retries = numRetries;
      if (callProperties && callProperties.request && callProperties.request.limit_retry_error) {
        if (typeof callProperties.request.limit_retry_error === 'number'
            || typeof callProperties.request.limit_retry_error === 'string') {
          retryError = [Number(callProperties.request.limit_retry_error)];
        } else if (Array.isArray(callProperties.request.limit_retry_error)) {
          retryError = [];
          for (let l = 0; l < callProperties.request.limit_retry_error.length; l += 1) {
            if (typeof callProperties.request.limit_retry_error[l] === 'number') {
              retryError.push(Number(callProperties.request.limit_retry_error[l]));
            } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                && callProperties.request.limit_retry_error[l].indexOf('-') < 0) {
              retryError.push(Number(callProperties.request.limit_retry_error[l]));
            } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                && callProperties.request.limit_retry_error[l].indexOf('-') >= 0) {
              const srange = Number(callProperties.request.limit_retry_error[l].split('-')[0]);
              const erange = Number(callProperties.request.limit_retry_error[l].split('-')[1]);
              for (let r = srange; r <= erange; r += 1) {
                retryError.push(r);
              }
            }
          }
        }
      }
      if (callProperties && callProperties.request && callProperties.request.number_retries) {
        retries = callProperties.request.number_retries;
      }

      // if the response is valid - good data or legitimate data error
      // stop trying if we have tried enough
      if (numTries > retries || (mres !== null && mres.code !== undefined && !retryError.includes(Number(mres.code))
          && (authMethod !== 'request_token' || Number(mres.code) !== Number(tokenError)))) {
        const newresp = mres;
        newresp.retries = numTries;

        if (throttleEnabled && (!callProperties || !callProperties.host)) {
          return handleEndThrottleResponse(request, myTransTime, claimedLic, newresp, reqTime, overallTime, waitEnd, callback);
        }

        return handleEndResponse(request, newresp, overallTime, callback);
      }
      if (mres !== null && mres.code !== undefined && Number(mres.code) === Number(tokenError) && authMethod === 'request_token') {
        // if we took a invalid token error - System out of licenses - try again
        return handleInvalidToken(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, numTries + 1, entitySchema, callback);
      }
      if (mres !== null && mres.code !== undefined && retryError.includes(Number(mres.code))) {
        // if we took a throughput limitation - System out of licenses - try again
        return handleLimitResponse(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, numTries + 1, entitySchema, callback);
      }

      // if we took an http error - like aborting
      return handleAbortResponse(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, numTries + 1, entitySchema, callback);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue retrying invalid');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: handleInvalidToken handles issues when a token has timed out
 */
function handleInvalidToken(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback) {
  const origin = `${id}-connectorRest-handleInvalidToken`;
  log.trace(origin);
  let reqDiff = process.hrtime(reqTime);
  let reqEnd = (reqDiff[0] * NS_PER_SEC) + reqDiff[1];
  log.warn(`${origin}: Request ${claimedLic.request_id} Transaction ${myTransTime} invalid token appear to be hit after ${reqEnd} nanoseconds - Entering handling invalid token`);
  reqDiff = undefined;
  reqEnd = undefined;

  try {
    // Need to retry the request - will pull new token
    return retryInvalidResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue handling invalid token');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: handleLimitResponse handles throughput issues with System
 */
function handleLimitResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback) {
  const origin = `${id}-connectorRest-handleLimitResponse`;
  log.trace(origin);
  let reqDiff = process.hrtime(reqTime);
  let reqEnd = (reqDiff[0] * NS_PER_SEC) + reqDiff[1];
  log.warn(`${origin}: Request ${claimedLic.request_id} Transaction ${myTransTime} limits appear to be hit after ${reqEnd} nanoseconds - Entering handling limiting requests`);
  reqDiff = undefined;
  reqEnd = undefined;
  let myTimeout = attemptTimeout;

  // if there is a timeout from the action (schema) use it instead
  if (entitySchema && entitySchema.timeout && entitySchema.timeout > 0) {
    myTimeout = entitySchema.timeout;
  }
  if (callProperties && callProperties.request && callProperties.request.attempt_timeout) {
    myTimeout = callProperties.request.attempt_timeout;
  }

  try {
    // Run this after an interval to see if throughput issues clear
    const timeoutObject = setTimeout(() => {
      log.debug(`${origin}: Processing Re-request`);

      // nothing to do but retry
      return retryInvalidResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback);
    }, myTimeout / 5);
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue handling limit');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: handleAbortResponse handles timeout issues with System
 *    like when the system has died
 */
function handleAbortResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback) {
  const origin = `${id}-connectorRest-handleAbortResponse`;
  log.trace(origin);
  let reqDiff = process.hrtime(reqTime);
  let reqEnd = (reqDiff[0] * NS_PER_SEC) + reqDiff[1];
  log.warn(`${origin}: Request ${claimedLic.request_id} Transaction ${myTransTime} aborted after ${reqEnd} nanoseconds - Entering handling aborted requests`);
  reqDiff = undefined;
  reqEnd = undefined;

  try {
    // wait for System to come back with a good healthcheck
    return waitForSystem(callProperties)
      .then(() => {
        log.debug(`${origin}: Cleared to reattempt`);

        // restart the current request
        return retryInvalidResponse(request, callProperties, myTransTime, claimedLic, makeResp, reqTime, overallTime, waitEnd, numTries, entitySchema, callback);
      });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue handling abort');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: noQueueRequest is used to perform normal requests.
 *    Normal being request that can run as they come in - there are no license or
 *    throttling limitations on these requests.
 */
function noQueueRequest(request, callProperties, overallTime, entitySchema, callback) {
  const origin = `${id}-connectorRest-noQueueRequest`;
  log.trace(origin);

  try {
    // call to make the request
    return requestAuthenticate(request, entitySchema, null, callProperties, (mres, merror) => {
      if (merror) {
        return callback(null, merror);
      }

      let retryError = limitRetryError;
      let retries = numRetries;
      if (callProperties && callProperties.request && callProperties.request.limit_retry_error) {
        if (typeof callProperties.request.limit_retry_error === 'number'
            || typeof callProperties.request.limit_retry_error === 'string') {
          retryError = [Number(callProperties.request.limit_retry_error)];
        } else if (Array.isArray(callProperties.request.limit_retry_error)) {
          retryError = [];
          for (let l = 0; l < callProperties.request.limit_retry_error.length; l += 1) {
            if (typeof callProperties.request.limit_retry_error[l] === 'number') {
              retryError.push(Number(callProperties.request.limit_retry_error[l]));
            } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                && callProperties.request.limit_retry_error[l].indexOf('-') < 0) {
              retryError.push(Number(callProperties.request.limit_retry_error[l]));
            } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                && callProperties.request.limit_retry_error[l].indexOf('-') >= 0) {
              const srange = Number(callProperties.request.limit_retry_error[l].split('-')[0]);
              const erange = Number(callProperties.request.limit_retry_error[l].split('-')[1]);
              for (let r = srange; r <= erange; r += 1) {
                retryError.push(r);
              }
            }
          }
        }
      }
      if (callProperties && callProperties.request && callProperties.request.number_retries) {
        retries = callProperties.request.number_retries;
      }

      // if invalid token, handle that and retry
      if (mres !== null && mres.code !== undefined && Number(mres.code) === Number(tokenError)
          && authMethod === 'request_token') {
        // if we took an invalid token - try one more time
        return handleInvalidToken(request, callProperties, 0, { request_id: 0 }, mres, overallTime, overallTime, 0, 1, entitySchema, callback);
      }

      // if the response is valid - good data or legitimate data error
      if (retries < 1 || (mres !== null && mres.code !== undefined && !retryError.includes(Number(mres.code))
          && (authMethod !== 'request_token' || Number(mres.code) !== Number(tokenError)))) {
        return handleEndResponse(request, mres, overallTime, callback);
      }

      if (mres !== null && mres.code !== undefined && retryError.includes(Number(mres.code))) {
        // if we took a out of licenses - try one more time
        return handleLimitResponse(request, callProperties, 0, { request_id: 0 }, mres, overallTime, overallTime, 0, 1, entitySchema, callback);
      }

      // if we took an http error - like aborting
      return handleAbortResponse(request, callProperties, 0, { request_id: 0 }, mres, overallTime, overallTime, 0, 1, entitySchema, callback);
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue with request');
    return callback(null, errorObj);
  }
}

/*
 * INTERNAL FUNCTION: makeThrottleRequest is used to perform throttled request.
 *    Throttled request means that Pronghorn is only allowed to run X concurrent
 *    requests to the system.
 *
 *    It will put the request in a queue item, wait until
 *    it can get its turn, then run the request and return the results
 */
function queueThrottleRequest(request, callProperties, myRequestId, myTransTime, priority, event, overallTime, entitySchema, callback) {
  const origin = `${id}-connectorRest-queueThrottleRequest`;
  log.trace(`${origin} - ${myRequestId}`);

  try {
    // request place in Queue
    return throttleEng.requestQueueItem(myRequestId, myTransTime, priority, event, (myItem, qerror) => {
      if (qerror) {
        return callback(null, qerror);
      }

      let waitStart = process.hrtime();

      // check if it is my turn
      return throttleEng.waitingMyTurn(myItem, (claimedLic, werror) => {
        let waitDiff = process.hrtime(waitStart);
        const waitEnd = (waitDiff[0] * NS_PER_SEC) + waitDiff[1];
        log.debug(`${origin}: Request ${myRequestId} Transaction ${myTransTime} wait took: ${waitEnd} nanoseconds`);
        const reqTime = process.hrtime();

        if (werror) {
          return callback(null, werror);
        }

        // clean up used variables from memory setting to undefined minimizes memory
        waitStart = undefined;
        waitDiff = undefined;

        // call to make the request
        return requestAuthenticate(request, entitySchema, null, callProperties, (mres, merror) => {
          if (merror) {
            return callback(null, merror);
          }

          let retryError = limitRetryError;
          let retries = numRetries;
          if (callProperties && callProperties.request && callProperties.request.limit_retry_error) {
            if (typeof callProperties.request.limit_retry_error === 'number'
                || typeof callProperties.request.limit_retry_error === 'string') {
              retryError = [Number(callProperties.request.limit_retry_error)];
            } else if (Array.isArray(callProperties.request.limit_retry_error)) {
              retryError = [];
              for (let l = 0; l < callProperties.request.limit_retry_error.length; l += 1) {
                if (typeof callProperties.request.limit_retry_error[l] === 'number') {
                  retryError.push(Number(callProperties.request.limit_retry_error[l]));
                } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                    && callProperties.request.limit_retry_error[l].indexOf('-') < 0) {
                  retryError.push(Number(callProperties.request.limit_retry_error[l]));
                } else if (typeof callProperties.request.limit_retry_error[l] === 'string'
                    && callProperties.request.limit_retry_error[l].indexOf('-') >= 0) {
                  const srange = Number(callProperties.request.limit_retry_error[l].split('-')[0]);
                  const erange = Number(callProperties.request.limit_retry_error[l].split('-')[1]);
                  for (let r = srange; r <= erange; r += 1) {
                    retryError.push(r);
                  }
                }
              }
            }
          }
          if (callProperties && callProperties.request && callProperties.request.number_retries) {
            retries = callProperties.request.number_retries;
          }

          // if invalid token, handle that and retry
          if (mres !== null && mres.code !== undefined && Number(mres.code) === Number(tokenError)
              && authMethod === 'request_token') {
            // if we took an invalid token - try one more time
            return handleInvalidToken(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, 1, entitySchema, callback);
          }

          // if the response is valid - good data or legitimate data error
          if (retries < 1 || (mres !== null && mres.code !== undefined && !retryError.includes(Number(mres.code))
              && (authMethod !== 'request_token' || Number(mres.code) !== Number(tokenError)))) {
            return handleEndThrottleResponse(request, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, callback);
          }

          if (mres !== null && mres.code !== undefined && retryError.includes(Number(mres.code))) {
            // if we took a out of licenses - try one more time
            return handleLimitResponse(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, 1, entitySchema, callback);
          }

          // if we took an http error - like aborting
          return handleAbortResponse(request, callProperties, myTransTime, claimedLic, mres, reqTime, overallTime, waitEnd, 1, entitySchema, callback);
        });
      });
    });
  } catch (e) {
    // handle any exception
    const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue with queue request');
    return callback(null, errorObj);
  }
}

class ConnectorRest {
  /**
   * Connector
   * @constructor
   */
  constructor(prongid, properties, transUtilCl, propUtilCl, dbUtilCl) {
    this.myid = prongid;
    id = prongid;

    this.transUtil = transUtilCl;
    transUtilInst = this.transUtil;
    this.propUtil = propUtilCl;
    propUtilInst = this.propUtil;
    this.dbUtil = dbUtilCl;
    dbUtilInst = this.dbUtil;

    // this uniquely identifies this adapter on this pronghorn system
    phInstance = `${id}-${os.hostname()}`;
    archiveColl = id + archiveColl;
    this.tlockInst = new AsyncLockCl();
    tlock = this.tlockInst;
    this.hlockInst = new AsyncLockCl();
    hlock = this.hlockInst;
    this.hclockInst = new AsyncLockCl();
    hclock = this.hclockInst;
    this.slock = new AsyncLockCl();
    crest = this;

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

    this.throttleEngInst = new ThrottleCl(id, properties, this.transUtil, this.dbUtil);
    throttleEng = this.throttleEngInst;

    this.connect((status) => {
      const origin = `${this.myid}-connectorRest-constructor`;
      log.info(`${origin}: connect status ${status}`);
    });
  }

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

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

    // REPLACE WITH SCHEMA VALIDATOR???
    // get all of my properties - use for initial connectivity check
    // set the host (required - default is null)
    if (typeof props.host === 'string') {
      host = props.host;
    }

    // set the port (required - default is null)
    if (typeof props.port === 'number' || typeof props.port === 'string') {
      port = Number(props.port);
    }

    // set the base path (optional - default is null)
    if (typeof props.base_path === 'string') {
      basepath = props.base_path;
    }

    // set the version (optional - default is null)
    if (typeof props.version === 'string') {
      version = props.version;
    }

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

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

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

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

      // set the token user field (required - default is username)
      if (typeof props.authentication.token_user_field === 'string') {
        tokenUserField = props.authentication.token_user_field;
      }

      // set the token password field (required - default is password)
      if (typeof props.authentication.token_password_field === 'string') {
        tokenPwdField = props.authentication.token_password_field;
      }

      // set the token result field (required - default is token)
      if (typeof props.authentication.token_result_field === 'string') {
        tokenResField = props.authentication.token_result_field;
      }

      // set the token URI path (required - default is null)
      if (typeof props.authentication.token_URI_path === 'string') {
        tokenPath = props.authentication.token_URI_path;
      }

      // set the invalid token error (optional - default is null)
      if (typeof props.authentication.invalid_token_error === 'number'
          || typeof props.authentication.invalid_token_error === 'string') {
        tokenError = Number(props.authentication.invalid_token_error);
      }

      // set the token timeout (optional - default is null)
      if (typeof props.authentication.token_timeout === 'number'
          || typeof props.authentication.token_timeout === 'string') {
        tokenTimeout = Number(props.authentication.token_timeout);
      }

      // set the token cache (required - default is local)
      if (typeof props.authentication.token_cache === 'string') {
        tokenCache = props.authentication.token_cache;
      }

      // set the auth field (required - default is null)
      if (typeof props.authentication.auth_field === 'string') {
        authField = [props.authentication.auth_field];
      } else if (Array.isArray(props.authentication.auth_field)) {
        authField = props.authentication.auth_field;
      }

      // set the auth format (required - default is null)
      if (typeof props.authentication.auth_field_format === 'string') {
        authFormat = [props.authentication.auth_field_format];
      } else if (Array.isArray(props.authentication.auth_field_format)) {
        authFormat = props.authentication.auth_field_format;
      }

      // set the auth logging (optional - default is false)
      if (typeof props.authentication.auth_logging === 'boolean') {
        authLogging = props.authentication.auth_logging;
      }
    }

    // set the stub mode (optional - default is false)
    if (typeof props.stub === 'boolean') {
      stub = props.stub;
    }

    // set the protocol (optional - default is http)
    if (typeof props.protocol === 'string') {
      protocol = props.protocol;
    }

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

    if (props.throttle) {
      // set the throttle enabled (optional - default is false)
      if (typeof props.throttle.throttle_enabled === 'boolean') {
        throttleEnabled = props.throttle.throttle_enabled;
      }

      // set the throttle number of pronghorns (optional - default is 1)
      if (props.throttle.number_pronghorns && Number(props.throttle.number_pronghorns) >= 1) {
        numberPhs = Number(props.throttle.number_pronghorns);
      }
    }

    if (props.request) {
      // set the number of redirects (optional - default is 0)
      if (typeof props.request.number_redirects === 'number'
          || typeof props.request.number_redirects === 'string') {
        numRedirects = Number(props.request.number_redirects);
      }

      // set the number of retries (optional - default is 3)
      if (typeof props.request.number_retries === 'number'
          || typeof props.request.number_retries === 'string') {
        numRetries = Number(props.request.number_retries);
      }

      // set the request retry error (optional - default is 0)
      if (typeof props.request.limit_retry_error === 'number'
          || typeof props.request.limit_retry_error === 'string') {
        limitRetryError = [Number(props.request.limit_retry_error)];
      } else if (Array.isArray(props.request.limit_retry_error)) {
        limitRetryError = [];
        for (let l = 0; l < props.request.limit_retry_error.length; l += 1) {
          if (typeof props.request.limit_retry_error[l] === 'number') {
            limitRetryError.push(Number(props.request.limit_retry_error[l]));
          } else if (typeof props.request.limit_retry_error[l] === 'string'
              && props.request.limit_retry_error[l].indexOf('-') < 0) {
            limitRetryError.push(Number(props.request.limit_retry_error[l]));
          } else if (typeof props.request.limit_retry_error[l] === 'string'
              && props.request.limit_retry_error[l].indexOf('-') >= 0) {
            const srange = Number(props.request.limit_retry_error[l].split('-')[0]);
            const erange = Number(props.request.limit_retry_error[l].split('-')[1]);
            for (let r = srange; r <= erange; r += 1) {
              limitRetryError.push(r);
            }
          }
        }
      }

      // set the request attempt timeout (optional - default is 5000)
      if (typeof props.request.attempt_timeout === 'number'
          || typeof props.request.attempt_timeout === 'string') {
        attemptTimeout = Number(props.request.attempt_timeout);
      }

      // set the request archiving flag (optional - default is false)
      if (props.request.global_request && typeof props.request.global_request === 'object') {
        globalRequest = props.request.global_request;
      }

      // set the request healthcheck on timeout flag (optional - default is false)
      if (typeof props.request.healthcheck_on_timeout === 'boolean') {
        healthcheckOnTimeout = props.request.healthcheck_on_timeout;
      }

      // set the request archiving flag (optional - default is false)
      if (typeof props.request.archiving === 'boolean') {
        archiving = props.request.archiving;
      }

      // set the request return request (optional - default is false)
      if (typeof props.request.return_request === 'boolean') {
        returnRequest = props.request.return_request;
      }
    }

    if (props.proxy) {
      // set the proxy enabled (optional - default is false)
      if (typeof props.proxy.enabled === 'boolean') {
        proxyEnabled = props.proxy.enabled;
      }

      // set the proxy host (optional - default is null)
      if (typeof props.proxy.host === 'string') {
        proxyHost = props.proxy.host;
      }

      // set the proxy port (optional - default is null)
      if (typeof props.proxy.port === 'number' || typeof props.proxy.port === 'string') {
        proxyPort = Number(props.proxy.port);
      }

      // set the proxy protocol (optional - default is same as protocol)
      if (typeof props.proxy.protocol === 'string') {
        proxyProtocol = props.proxy.protocol;
      } else {
        proxyProtocol = protocol;
      }

      // set the proxy username (optional - default is null)
      if (typeof props.proxy.username === 'string') {
        proxyUser = props.proxy.username;
      }

      // set the proxy password (optional - default is null)
      if (typeof props.proxy.password === 'string') {
        proxyPassword = props.proxy.password;
      }
    }

    if (props.ssl) {
      // set the ssl enabled (optional - default is false)
      if (typeof props.ssl.enabled === 'boolean') {
        sslEnabled = props.ssl.enabled;
      }

      // set the ssl ecdhCurve (optional - default is false)
      if (typeof props.ssl.ecdhCurve === 'string' && props.ssl.ecdhCurve === 'auto') {
        ecdhAuto = true;
      }

      // set the ssl accept invalid cert (optional - default is false)
      if (typeof props.ssl.accept_invalid_cert === 'boolean') {
        sslAcceptInvalid = props.ssl.accept_invalid_cert;
      }

      // set the ssl ca file (optional - default is null)
      if (typeof props.ssl.ca_file === 'string') {
        sslCAFile = props.ssl.ca_file;
      }

      // set the ssl key file (optional - default is null)
      if (typeof props.ssl.key_file === 'string') {
        sslKeyFile = props.ssl.key_file;
      }

      // set the ssl cert file (optional - default is null)
      if (typeof props.ssl.cert_file === 'string') {
        sslCertFile = props.ssl.cert_file;
      }

      // set the ssl ciphers (optional - default is null)
      if (typeof props.ssl.ciphers === 'string') {
        sslCiphers = props.ssl.ciphers;
      }

      // set the ssl ciphers (optional - default is null)
      if (typeof props.ssl.secure_protocol === 'string') {
        secureProtocol = props.ssl.secure_protocol;
      }
    }

    // if this is truly a refresh and we hae a throttle engine, refresh it
    if (this.throttleEngInst) {
      this.throttleEngInst.refreshProperties(properties);
    }
  }

  /**
   * Connect function is used to verify that everything required to connect and communicate
   * has been provided.
   *
   * @function connect
   * @param {Function} callback - a callback function to return the result - connected?
   */
  connect(callback) {
    const origin = `${this.myid}-connectorRest-connect`;
    log.trace(origin);

    // if throttling with the database or archiving, need to make sure the database is there
    if ((throttleEnabled && numberPhs > 1) || archiving) {
      // CLEAN UP LOCK & LICENSES
      return waitForMongo()
        .then(() => {
          if (archiving) {
            const colRes = this.dbUtil.createCollection(archiveColl, (err, res) => {
              if (err) {
                log.error(`${origin}: collection ${archiveColl} could not be created.`);
                return callback(false);
              }
              log.info(`${origin}: Result collection check passed`);
            });
          }

          if (throttleEnabled) {
            this.throttleEngInst.verifyReady((res, rerror) => {
              if (rerror) {
                return callback(false);
              }

              log.debug(`${origin}: Throttle verify - ${res}`);
            });
          }

          // Make sure all the properties are loaded
          if (!host) {
            log.error(`${origin}: FAILED TO LOAD - missing host`);
            return callback(false);
          }
          if (!port) {
            log.error(`${origin}: FAILED TO LOAD - missing port`);
            return callback(false);
          }
          if (authMethod === 'static_token') {
            if (!staticToken) {
              log.error(`${origin}: FAILED TO LOAD - missing static token`);
              return callback(false);
            }
          } else if (authMethod === 'request_token' || authMethod === 'basic user_password') {
            if (!username) {
              log.error(`${origin}: FAILED TO LOAD - missing username`);
              return callback(false);
            }
            if (!password) {
              log.error(`${origin}: FAILED TO LOAD - missing password`);
              return callback(false);
            }
          }
          if (sslEnabled && !sslAcceptInvalid && !sslCAFile) {
            log.error(`${origin}: FAILED TO LOAD - missing required CA File`);
            return callback(false);
          }

          return callback(true);
        });
    }

    // if throttling without database - just need to verify throttle engine is ready
    if (throttleEnabled) {
      this.throttleEngInst.verifyReady((res, rerror) => {
        if (rerror) {
          return callback(false);
        }

        log.debug(`${origin}: Throttle verify - ${res}`);
      });
    }

    // Make sure all the properties are loaded
    if (!host) {
      log.error(`${origin}: FAILED TO LOAD - missing host`);
      return callback(false);
    }
    if (!port) {
      log.error(`${origin}: FAILED TO LOAD - missing port`);
      return callback(false);
    }
    if (authMethod === 'static_token') {
      if (!staticToken) {
        log.error(`${origin}: FAILED TO LOAD - missing static token`);
        return callback(false);
      }
    } else if (authMethod === 'request_token' || authMethod === 'basic user_password') {
      if (!username) {
        log.error(`${origin}: FAILED TO LOAD - missing username`);
        return callback(false);
      }
      if (!password) {
        log.error(`${origin}: FAILED TO LOAD - missing password`);
        return callback(false);
      }
    }
    if (sslEnabled && !sslAcceptInvalid && !sslCAFile) {
      log.error(`${origin}: FAILED TO LOAD - missing required CA File`);
      return callback(false);
    }

    return callback(true);
  }

  /**
   * HealthCheck function is used to provide Pronghorn the status of this adapter.
   *
   * @function healthCheck
   * @param {Object} healthSchema - the schema for the healthcheck (optional)
   * @param {String} payload - the contents to send with the healthcheck
   *                           (optional)
   * @param {Object} headers - this allows for additional headers to
   *                           be added to the request. (optional)
   *                           Can be a stringified Object.
   * @param {Object} callProperties - properties to override on this call (optional)
   * @param {Function} callback - a callback function to return the result of the healthcheck
   */
  healthCheck(healthSchema, payload, headers, callProperties, callback) {
    const origin = `${this.myid}-connectorRest-healthCheck`;
    log.trace(origin);

    try {
      // healthcheck can not be throttled and put in queue because it would be blocked
      // so build the request for a simple call and make the request
      const options = {
        hostname: host,
        port,
        path: healthcheckpath,
        method: 'GET',
        headers
      };

      // passed in properties override defaults
      if (callProperties && callProperties.host) {
        options.hostname = callProperties.host;
      }
      if (callProperties && callProperties.port) {
        options.port = callProperties.port;
      }

      // save headers in memory
      if (headers) {
        // save it in memory
        cacheHHead = headers;
      }

      // if there is a healthcheck schema, over ride the properties
      if (healthSchema) {
        options.path = healthSchema.entitypath;
        options.method = healthSchema.method;

        // save it in memory
        cacheHSchema = healthSchema;
      }

      // if the path has a base path parameter in it, need to replace it
      let bpathStr = '{base_path}';
      if (options.path.indexOf(bpathStr) >= 0) {
        // be able to support this if the base path has a slash before it or not
        if (options.path.indexOf('/{base_path}') >= 0) {
          bpathStr = '/{base_path}';
        }

        // replace with base path if we have one, otherwise remove base path
        if (callProperties && callProperties.base_path) {
          // if no leading /, insert one
          if (callProperties.base_path.indexOf('/') !== 0) {
            options.path = options.path.replace(bpathStr, `/${callProperties.base_path}`);
          } else {
            options.path = options.path.replace(bpathStr, callProperties.base_path);
          }
        } else if (basepath) {
          // if no leading /, insert one
          if (basepath.indexOf('/') !== 0) {
            options.path = options.path.replace(bpathStr, `/${basepath}`);
          } else {
            options.path = options.path.replace(bpathStr, basepath);
          }
        } else {
          options.path = options.path.replace(bpathStr, '');
        }
      }

      // if the path has a version parameter in it, need to replace it
      let versStr = '{version}';
      if (options.path.indexOf(versStr) >= 0) {
        // be able to support this if the version has a slash before it or not
        if (options.path.indexOf('/{version}') >= 0) {
          versStr = '/{version}';
        }

        // replace with version if we have one, otherwise remove version
        if (callProperties && callProperties.version) {
          options.path = options.path.replace(versStr, `/${encodeURIComponent(callProperties.version)}`);
        } else if (version) {
          options.path = options.path.replace(versStr, `/${encodeURIComponent(version)}`);
        } else {
          options.path = options.path.replace(versStr, '');
        }
      }

      // if ssl enabled add the options for ssl
      if (callProperties && callProperties.ssl && Object.hasOwnProperty.call(callProperties.ssl, 'enabled')) {
        if (callProperties.ssl.enabled) {
          if (callProperties.ssl.accept_invalid_cert) {
            // if we are accepting invalid certificates (ok for lab not so much production)
            options.rejectUnauthorized = false;
          } else {
            // if we are not accepting invalid certs, need the ca file in the options
            try {
              options.rejectUnauthorized = true;
              options.ca = [fs.readFileSync(callProperties.ssl.ca_file)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [callProperties.ssl.ca_file], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }

          if (callProperties.ssl.ciphers) {
            options.ciphers = callProperties.ssl.ciphers;
          }
          if (callProperties.ssl.secure_protocol) {
            options.secureProtocol = callProperties.ssl.secure_protocol;
          }

          log.info(`${origin}: Connector SSL connections enabled`);
        }
      } else if (sslEnabled) {
        if (sslAcceptInvalid) {
          // if we are accepting invalid certificates (ok for lab not so much production)
          options.rejectUnauthorized = false;
        } else {
          // if we are not accepting invalid certs, need the ca file in the options
          try {
            options.rejectUnauthorized = true;
            options.ca = [fs.readFileSync(sslCAFile)];
          } catch (e) {
            const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCAFile], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
          // if there is a cert file, try to read in a cert file in the options
          if (sslCertFile) {
            try {
              options.cert = [fs.readFileSync(sslCertFile)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCertFile], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }
          // if there is a key file, try to read in a key file in the options
          if (sslKeyFile) {
            try {
              options.key = [fs.readFileSync(sslKeyFile)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslKeyFile], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }
        }

        if (sslCiphers) {
          options.ciphers = sslCiphers;
        }
        if (secureProtocol) {
          options.secureProtocol = secureProtocol;
        }

        log.info(`${origin}: Connector SSL connections enabled`);
      }

      log.debug(`${origin}: HEALTHCHECK OPTIONS: ${JSON.stringify(options)}`);

      if (payload !== undefined && payload !== null && payload !== '') {
        log.debug(`${origin}: REQUEST: ${payload}`);

        // save it in memory
        cacheHPay = payload;
      }

      // handle ipv6 hostnames
      if (host.indexOf('[') === 0) {
        options.host = host;
        options.hostname = options.hostname.substring(1, options.hostname.length - 1);
        options.family = 6;
      }

      const request = {
        header: options,
        body: payload
      };

      // if there is a healthcheck schema, over ride the properties
      if (healthSchema !== null) {
        request.origPath = healthSchema.entitypath;
      }

      // call to make the request
      return requestAuthenticate(request, healthSchema, null, callProperties, (pres, perror) => {
        if (perror) {
          return callback(null, perror);
        }

        if (pres !== null && pres.code !== undefined
            && (Number(pres.code) < 200 || Number(pres.code) > 299)) {
          let errorObj = null;

          if (pres.code === -2) {
            errorObj = transUtilInst.formatErrorObject(origin, 'Request Timeout', [pres.code], pres.code, pres, null);
          } else {
            errorObj = transUtilInst.formatErrorObject(origin, 'Error On Request', [pres.code], pres.code, pres, null);
          }

          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }
        const returnObj = {
          code: 'AD.200'
        };

        returnObj.response = pres;
        return callback(returnObj);
      });
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue with healthcheck');
      return callback(null, errorObj);
    }
  }

  /**
   * Generic request handler for System requests. Will take in the incoming request which includes
   * a path, a method and an optional user and password. This information will be used to set up
   * the correct request to the system.  It will then process the request and return the results.
   *
   * @function performRequest
   * @param {Object} incoming - the information for the request. Must have a path and a method.
   *                            Can optionally have user and passwd (required)
   * @param {String} entitySchema - the entity schema (required)
   * @param {Object} callProperties - properties to override on this call (optional)
   * @param {Function} callback - a callback function to return the result of the request
   */
  performRequest(incoming, entitySchema, callProperties, callback) {
    const origin = `${this.myid}-connectorRest-performRequest`;
    log.trace(origin);
    const overallTime = process.hrtime();

    try {
      // verify data was received
      if (incoming === null) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['request'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
      if (incoming.path === undefined || incoming.path === null || incoming.path === '') {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['request path'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
      if (incoming.method === undefined || incoming.method === null || incoming.method === '') {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['request method'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      // set up the options for the call
      let options = {
        hostname: host,
        port,
        path: incoming.path,
        method: incoming.method,
        headers: incoming.addlHeaders
      };

      // passed in properties override defaults
      if (callProperties && callProperties.host) {
        options.hostname = callProperties.host;
      }
      if (callProperties && callProperties.port) {
        options.port = callProperties.port;
      }

      // if ssl enabled add the options for ssl
      if (callProperties && callProperties.ssl && Object.hasOwnProperty.call(callProperties.ssl, 'enabled')) {
        if (callProperties.ssl.enabled) {
          if (callProperties.ssl.accept_invalid_cert) {
            // if we are accepting invalid certificates (ok for lab not so much production)
            options.rejectUnauthorized = false;
          } else {
            // if we are not accepting invalid certs, need the ca file in the options
            try {
              options.rejectUnauthorized = true;
              options.ca = [fs.readFileSync(callProperties.ssl.ca_file)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [callProperties.ssl.ca_file], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }

          if (callProperties.ssl.ciphers) {
            options.ciphers = callProperties.ssl.ciphers;
          }
          if (callProperties.ssl.secure_protocol) {
            options.secureProtocol = callProperties.ssl.secure_protocol;
          }

          log.info(`${origin}: Connector SSL connections enabled`);
        }
      } else if (sslEnabled) {
        if (sslAcceptInvalid) {
          // if we are accepting invalid certificates (ok for lab not so much production)
          options.rejectUnauthorized = false;
        } else {
          // if we are not accepting invalid certs, need the ca file in the options
          try {
            options.rejectUnauthorized = true;
            options.ca = [fs.readFileSync(sslCAFile)];
          } catch (e) {
            const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCAFile], null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
          // if there is a cert file, try to read in a cert file in the options
          if (sslCertFile) {
            try {
              options.cert = [fs.readFileSync(sslCertFile)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslCertFile], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }
          // if there is a key file, try to read in a key file in the options
          if (sslKeyFile) {
            try {
              options.key = [fs.readFileSync(sslKeyFile)];
            } catch (e) {
              const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', [sslKeyFile], null, null, null);
              log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
              return callback(null, errorObj);
            }
          }
        }

        if (sslCiphers) {
          options.ciphers = sslCiphers;
        }
        if (secureProtocol) {
          options.secureProtocol = secureProtocol;
        }

        log.info(`${origin}: Connector SSL connections enabled`);
      }

      log.debug(`${origin}: OPTIONS: ${JSON.stringify(options)}`);

      if (incoming.body !== undefined && incoming.body !== null && incoming.body !== '') {
        log.debug(`${origin}:REQUEST: ${incoming.body}`);
      }

      const request = {
        header: options,
        body: incoming.body,
        origPath: incoming.origPath
      };

      // if there is additional authentication data, add it to the request
      if (incoming.authData) {
        request.authData = incoming.authData;
      }

      // clean up used variables from memory setting to undefined minimizes memory
      options = undefined;

      // if throttling is not enables or we are talking to a different host than the default
      if (!throttleEnabled || (callProperties && callProperties.host)) {
        return noQueueRequest(request, callProperties, overallTime, entitySchema, callback);
      }

      let myRequest = requestId;
      let myTransTime = new Date().getTime();

      // Lock the global to increment it and reset the local
      return this.slock.acquire(requestId, (done) => {
        requestId += 1;
        myRequest = requestId;
        myTransTime = new Date().getTime();
        done(true);
      }, (ret) => {
        if (ret) {
          return queueThrottleRequest(request, callProperties, myRequest, myTransTime, incoming.priority, incoming.event, overallTime, entitySchema, callback);
        }

        return queueThrottleRequest(request, callProperties, myRequest, myTransTime, incoming.priority, incoming.event, overallTime, entitySchema, callback);
      });
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue performing the request');
      return callback(null, errorObj);
    }
  }

  /**
   * getQueue is used to get information for all of the requests currently in the queue.
   *
   * @function getQueue
   * @param {Function} callback - a callback function to return the queue
   */
  getQueue(callback) {
    const origin = `${this.myid}-connectorRest-getQueue`;
    log.trace(origin);

    try {
      if (!throttleEnabled) {
        log.error(`${origin}: Throttling not enabled, no queue`);
        return [];
      }

      return throttleEng.getQueue(callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting queue');
      return callback(null, errorObj);
    }
  }
}

module.exports = ConnectorRest;
