/* @copyright Itential, LLC 2018 */

// Set globals
/* global log */

/* NodeJS internal utilities */
const querystring = require('querystring');
const jsonQuery = require('json-query');
const jsonxml = require('jsontoxml');
const xml2js = require('xml2js');

const globalSchema = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, '/../schemas/globalSchema.json')));

let transUtilInst = null;
let connectorInst = null;

// Other global variables
let id = null;
let versionGl = null;
let basepathGl = null;
let globalRequestGl = null;
let returnRawGl = false;
let encodePath = true;
let encodeUri = true;

// INTERNAL FUNCTIONS
/*
 * INTERNAL FUNCTION: Get the best match for the mock data response
 */
function matchResponse(uriPath, method, type, mockresponses) {
  // 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 Key we really want as it best matches the request
          if (Object.hasOwnProperty.call(mockresponses[p], 'key')) {
            return mockresponses[p].key;
          }
        }
      }
    }
  }

  return null;
}

/*
 * INTERNAL FUNCTION: recursively inspect body data if heirarchical
 */
function checkBodyData(uriPath, method, reqBdObj, mockresponses) {
  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);
        } 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);

            // 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 = matchResponse(uriPath, method, compStr, mockresponses);

            // 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 = matchResponse(uriPath, method, compStr, mockresponses);
        }

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

/**
 * INTERNAL FUNCTION
 *
 * @summary Standard rest entity(ies) extracted as it it would be redundant code.
 *
 * @function handleRestRequest
 * @param {Object} request - the request to make (required)
 * @param {String} entityId - the id of the entity we are retrieving (optional)
 * @param {String} entitySchema - the entity schema (required)
 * @param {Object} callProperties - properties to override on this call (optional)
 * @param {String} filter - json query filter to apply to the returned data (optional)
 * @param {Boolean} retReqHdr - whether to return the request headers (optional)
 * @param {Function} callback - a callback function to return the result of the request
 */
function handleRestRequest(request, entityId, entitySchema, callProperties, filter, retReqHdr, callback) {
  const origin = `${id}-restHandler-handleRestRequest`;
  log.trace(origin);

  try {
    // perform the request to get entity(ies)
    return connectorInst.performRequest(request, entitySchema, callProperties, (resObj, perror) => {
      if (perror) {
        let retError = null;
        const retErrorObj = perror;

        // if the request header is not needed remove it
        if (retReqHdr === false && retErrorObj.reqHdr) {
          delete retErrorObj.reqHdr;
        }

        // set the normal headers based on the type of data for the call
        if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'PLAIN') {
          // return the error response
          return callback(null, retErrorObj);
        }
        if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'XML') {
          // return the error response
          return callback(null, retErrorObj);
        }

        if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'XML2JSON') {
          try {
            // if we have a response, try to parse it to JSON
            const parser = new xml2js.Parser({ explicitArray: false, attrkey: '_attr' });
            if (perror.response) {
              return parser.parseString(perror.response, (error, result) => {
                if (error) {
                  log.warn(`${origin}: Unable to parse xml to json ${error}`);
                  return callback(null, retErrorObj);
                }
                retErrorObj.response = result;
                return callback(null, retErrorObj);
              });
            }
            return callback(null, retErrorObj);
          } catch (ex) {
            log.warn(`${origin}: Unable to parse json ${ex}`);
            return callback(null, retErrorObj);
          }
        }

        // process the error response - parse it if possible
        if (perror.response) {
          if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'URLENCODE') {
            // return the response
            retError = querystring.parse(perror.response.trim());
          } else {
            try {
              retError = JSON.parse(perror.response.trim());
            } catch (ex) {
              // otherwise log parse failure but still return the unparsed error
              log.error(`${origin}: An error occurred parsing the error JSON: ${ex}`);
            }
          }
        }

        // if the return error message was JSON then return the parsed object
        if (retError !== null) {
          // if there is a local error schema in the entity use that one
          if (Object.hasOwnProperty.call(entitySchema, 'errorSchema')) {
            retErrorObj.response = transUtilInst.mapFromOutboundEntity(retError, entitySchema.errorSchema);
          } else {
            // if there is a no local error schema in the entity use that one
            retErrorObj.response = transUtilInst.mapFromOutboundEntity(retError, globalSchema);
          }
        }

        // return the error response
        return callback(null, retErrorObj);
      }

      let respObjKey = null;

      if (entitySchema.responseObjects) {
        const responseKeys = entitySchema.responseObjects;
        const uriPath = request.origPath;
        const method = request.method.toUpperCase();
        const reqBody = request.body;
        const reqPath = request.path;

        // 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());
          }

          respObjKey = checkBodyData(uriPath, method, reqBdObj, responseKeys);
        }

        // if there are path variables, see if there is something that matches a specific variable
        if (respObjKey === null && 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 (basepathGl && basepathGl !== '/') {
            actTemp[0] = actTemp[0].replace(basepathGl, '');
          }

          // 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 (versionGl && versionGl !== '/') {
            actTemp[0] = actTemp[0].replace(`/${versionGl}`, '');
          }

          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) {
                respObjKey = matchResponse(uriPath, method, actArray[i], responseKeys);

                if (respObjKey !== null) {
                  break;
                }
              }
            }
          }
        }

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

          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}`;
              respObjKey = matchResponse(uriPath, method, qval, responseKeys);

              if (respObjKey !== null) {
                break;
              }
            }
          }
        }

        // if there is a request body, see if there is a specific response for body
        if (respObjKey === null && reqBody) {
          respObjKey = matchResponse(uriPath, method, 'WITHBODY', responseKeys);
        }

        // if there are path variables, see if there is a specific response for path vars
        if (respObjKey === null && 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 (basepathGl && basepathGl !== '/') {
            actTemp[0] = actTemp[0].replace(basepathGl, '');
          }

          // 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 (versionGl && versionGl !== '/') {
            actTemp[0] = actTemp[0].replace(`/${versionGl}`, '');
          }

          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) {
                respObjKey = matchResponse(uriPath, method, `WITHPATHV${cnt}`, responseKeys);

                if (respObjKey !== null) {
                  break;
                }
                cnt += 1;
              }
            }
          }
        }

        // if there are queiries or options, see if there is a specific response for query or options
        if (respObjKey === null && uriPath.indexOf('?') >= 0) {
          respObjKey = matchResponse(uriPath, method, 'WITHQUERY', responseKeys);

          if (respObjKey === null) {
            respObjKey = matchResponse(uriPath, method, 'WITHOPTIONS', responseKeys);
          }
        }

        if (respObjKey === null) {
          respObjKey = matchResponse(uriPath, method, 'DEFAULT', responseKeys);
        }

        if (respObjKey === null) {
          respObjKey = '';
        }
      }

      let retResponse = resObj.response;
      const retObject = resObj;

      // if the request header is not needed remove it
      if (retReqHdr === false && retObject.reqHdr) {
        delete retObject.reqHdr;
      }

      // if we want the raw response
      if (returnRawGl || (callProperties && callProperties.request && callProperties.request.return_raw)) {
        retObject.raw = resObj.response;
      }

      // if id, log response (if all - too much to log all)
      if (entityId === 'nomap') {
        log.debug(`${origin}: RESPONSE: ${resObj.response}`);

        // if no mapping (do not care about response just that we did not
        // error) set response
        retObject.response = 'success';
        return callback(retObject);
      }

      // if the data is not json we can not perform the extended capabilities so just return it
      if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'PLAIN') {
        log.debug(`${origin}: RESPONSE: ${resObj.response}`);

        // return the response
        return callback(retObject);
      }
      if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'XML') {
        log.debug(`${origin}: RESPONSE: ${resObj.response}`);

        // return the response
        return callback(retObject);
      }

      if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'XML2JSON') {
        log.debug(`${origin}: RESPONSE: ${resObj.response}`);

        try {
          const parser = new xml2js.Parser({ explicitArray: false, attrkey: '_attr' });
          return parser.parseString(resObj.response, (error, result) => {
            if (error) {
              log.warn(`${origin}: Unable to parse xml to json ${error}`);
              return callback(retObject);
            }
            retObject.response = result;
            return callback(retObject);
          });
        } catch (ex) {
          log.warn(`${origin}: Unable to get json from xml ${ex}`);
          return callback(retObject);
        }
      }

      // what if nothing comes back - nothing to do
      if (resObj.response === null || resObj.response === '' || resObj.response === '""') {
        log.warn(`${origin}: No data returned on call`);
        return callback(retObject);
      }

      if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'URLENCODE') {
        // return the response
        retResponse = querystring.parse(resObj.response.trim());
      } else {
        // process the response - parse it
        try {
          retResponse = JSON.parse(resObj.response.trim());
        } catch (ex) {
          // otherwise log parse failure and return the unparsed response
          log.warn(`${origin}: An error occurred parsing the resulting JSON: ${ex}`);
          return callback(retObject);
        }
      }

      // Make the call to translate the received Entity to Pronghorn Entity
      if (respObjKey !== '') {
        // get the return data and added the translated response to the return Object
        const returnFieldData = jsonQuery(respObjKey, { data: retResponse }).value;

        // if the response is an array, log the first item
        if (Array.isArray(returnFieldData)) {
          log.debug(`${origin}: RESPONSE (FIRST): ${JSON.stringify(returnFieldData[0])}`);
        } else {
          log.debug(`${origin}: RESPONSE: ${JSON.stringify(returnFieldData)}`);
        }

        // if the data is not an object - just return what it is
        if (!Array.isArray(returnFieldData) && typeof returnFieldData !== 'object') {
          retObject.response = returnFieldData;
          return callback(retObject);
        }

        retObject.response = transUtilInst.mapFromOutboundEntity(returnFieldData, entitySchema.responseSchema);

        // if filtering, filter the data
        if (filter) {
          retObject.response = jsonQuery(filter, { data: retObject.response }).value;
        }

        return callback(retObject);
      }

      if (Array.isArray(retResponse)) {
        // if the response is an array, log the first item
        log.debug(`${origin}: RESPONSE (FIRST): ${JSON.stringify(retResponse[0])}`);
      } else {
        log.debug(`${origin}: RESPONSE: ${JSON.stringify(retResponse)}`);
      }

      // if the data is not an object - just return what it is
      if (!Array.isArray(retResponse) && typeof retResponse !== 'object') {
        retObject.response = retResponse;
        return callback(retObject);
      }

      // Apply global schema
      // retResponse = transUtilInst.mapFromOutboundEntity(retResponse, globalSchema);

      // added the translated response to the return Object
      retObject.response = transUtilInst.mapFromOutboundEntity(retResponse, entitySchema.responseSchema);

      // if filtering, filter the data
      if (filter) {
        retObject.response = jsonQuery(filter, { data: retObject.response }).value;
      }

      return callback(retObject);
    });
  } catch (e) {
    // create the error object
    const errorObj = {
      origin,
      type: 'Caught Exception',
      vars: [],
      exception: e
    };

    log.error(`${origin}: Caught Exception: ${e}`);
    return callback(null, errorObj);
  }
}

/**
 * @summary Build the path for the request
 *
 * @function buildRequestPath
 * @param {String} entity - the name of the entity action is on. (required)
 * @param {String} action - the name of the action being executed. (required)
 * @param {Object} entitySchema - the entity schema for the entity and action. (required)
 * @param {String} reqPath - the entitypath from the entity action (required)
 * @param {Array} uriPathVars - the array of path variables (optional)
 * @param {Object} uriQuery - the object containing the query to add to the url (optional)
 * @param {Object} uriOptions - the object containing the options to add to the url (optional)
 */
function buildRequestPath(entity, action, entitySchema, reqPath, uriPathVars, uriQuery, uriOptions, callProperties) {
  const origin = `${id}-restHandler-buildRequestPath`;
  log.trace(origin);

  // create the generic part of an error object
  const errorObj = {
    origin
  };

  try {
    let uriPath = reqPath;

    // if the path has a base path parameter in it, need to replace it
    let bpathStr = '{base_path}';
    if (uriPath.indexOf(bpathStr) >= 0) {
      // be able to support this if the base path has a slash before it or not
      if (uriPath.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) {
          uriPath = uriPath.replace(bpathStr, `/${callProperties.base_path}`);
        } else {
          uriPath = uriPath.replace(bpathStr, callProperties.base_path);
        }
      } else if (basepathGl) {
        // if no leading /, insert one
        if (basepathGl.indexOf('/') !== 0) {
          uriPath = uriPath.replace(bpathStr, `/${basepathGl}`);
        } else {
          uriPath = uriPath.replace(bpathStr, basepathGl);
        }
      } else {
        uriPath = uriPath.replace(bpathStr, '');
      }
    }

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

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

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

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

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

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

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

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

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

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

        uriPath = tempStr;
      } else if (eIndex > 0 && eIndex < uriPath.length) {
        // add the end of the path
        uriPath = uriPath.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
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing entity path', `${entity}/${action}`];

        // log and throw the error
        log.error(`${origin}: Path is required for ${entity}-${action}`);
        throw new Error(JSON.stringify(errorObj));
      }
    }

    // prepare the uri options we received
    let thisOdata = transUtilInst.formatInputData(uriOptions);

    // only add global options if there are global options to add
    if (globalRequestGl && globalRequestGl.uriOptions
        && Object.keys(globalRequestGl.uriOptions).length > 0) {
      thisOdata = transUtilInst.mergeObjects(thisOdata, globalRequestGl.uriOptions);
    }
    let optionString = '';

    // need to format the option string
    if (thisOdata !== null) {
      optionString += querystring.stringify(thisOdata);
    }

    // prepare the query parameters we received
    const thisQdata = transUtilInst.formatInputData(uriQuery);

    // if this is a get with query parameters, need to make them part of
    // the request
    if (uriPath.indexOf('{query}') >= 0 && thisQdata !== null && Object.keys(thisQdata).length > 0) {
      // request type set for determining required fields
      thisQdata.ph_request_type = action;

      // map the data we received for query
      const systemQuery = transUtilInst.mapToOutboundEntity(thisQdata, entitySchema.requestSchema);

      if (!systemQuery) {
        // 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
        errorObj.type = 'Query Not Translated';
        errorObj.vars = [];

        // log and throw the error
        log.error(`${origin}: Query not translated`);
        throw new Error(JSON.stringify(errorObj));
      }

      let addquery = '';

      // make sure we still have queries (that what was in the query is
      // legit)
      if (Object.keys(systemQuery).length > 0) {
        // need to format the option string
        if (entitySchema.querykey) {
          addquery = entitySchema.querykey;
        }
        if (systemQuery !== null) {
          // if we are encoding - use querystring since it does it all!
          if (encodeUri === true) {
            addquery += querystring.stringify(systemQuery);
          } else {
            // if not encoding we need to build
            const qkeys = Object.keys(systemQuery);

            // add each query parameter and its value
            for (let k = 0; k < qkeys.length; k += 1) {
              // need to add separator for everything after the first one
              if (k > 0) {
                addquery += '&';
              }
              // adds key=value
              addquery += `${qkeys[k]}=${systemQuery[qkeys[k]]}`;
            }
          }
        }
      }

      // if there is a query key in the path and the query
      if (addquery.indexOf('?') >= 0 && uriPath.indexOf('?') >= 0) {
        // need to remove one of them
        const squery = uriPath.indexOf('?');
        let equery = uriPath.indexOf('{query}');
        equery += 7;
        let tempPath = uriPath.substring(0, squery);
        tempPath += addquery;

        if (equery < uriPath.length) {
          tempPath += uriPath.substring(equery + 7);
        }

        uriPath = tempPath;
      } else {
        // if not, just replace the query
        uriPath = uriPath.replace('{query}', addquery);
      }

      // if there are options, add them to the URL
      if (optionString !== '') {
        uriPath += `&${optionString}`;
      }

      // verify that the uriPath starts with a slash and only 1 slash
      while (uriPath.indexOf('//') === 0) {
        uriPath = uriPath.substring(1);
      }
      if (uriPath.indexOf('/') !== 0) {
        uriPath = `/${uriPath}`;
      }

      // remove any double slashes that may be in the path - can happen if base path is / or ends in a /
      uriPath = uriPath.replace(/\/\//g, '/');

      const result = {
        path: uriPath
      };

      return result;
    }

    // if there is a query key in the path
    if (uriPath.indexOf('{query}') >= 0 && uriPath.indexOf('?') >= 0) {
      // need to remove the key as well
      const squery = uriPath.indexOf('?');
      let equery = uriPath.indexOf('{query}');
      equery += 7;
      let tempPath = uriPath.substring(0, squery);

      if (equery < uriPath.length) {
        tempPath += uriPath.substring(equery);
      }

      uriPath = tempPath;
    } else if (uriPath.indexOf('{query}') >= 0) {
      // if not, just replace the query
      uriPath = uriPath.replace('{query}', '');
    }

    // if there are options, add them to the URL
    if (optionString !== '') {
      if (uriPath.indexOf('?') < 0) {
        uriPath += `?${optionString}`;
      } else {
        uriPath += `${optionString}`;
      }
    }

    // verify that the uriPath starts with a slash and only 1 slash
    while (uriPath.indexOf('//') === 0) {
      uriPath = uriPath.substring(1);
    }
    if (uriPath.indexOf('/') !== 0) {
      uriPath = `/${uriPath}`;
    }

    // remove any double slashes that may be in the path - can happen if base path is / or ends in a /
    uriPath = uriPath.replace(/\/\//g, '/');

    const result = {
      path: uriPath
    };

    return result;
  } catch (e) {
    return transUtilInst.checkAndThrow(e, origin, 'Issue building request path');
  }
}

/**
 * @summary Method to merge the headers for the request
 *
 * @function mergeHeaders
 * @param {Object} addlHeaders - the headers from the request (optional)
 * @param {Object} entitySchema - the entity schema for the entity and action. (optional)
 *
 * @return {Object} - the merged headers
 */
function mergeHeaders(addlHeaders, entitySchema) {
  const origin = `${id}-restHandler-mergeHeaders`;
  log.trace(origin);

  // prepare the additional headers we received
  let thisAHdata = transUtilInst.formatInputData(addlHeaders);

  // only add action headers if there are action headers to add
  if (entitySchema && entitySchema.headers && Object.keys(entitySchema.headers).length > 0) {
    thisAHdata = transUtilInst.mergeObjects(thisAHdata, entitySchema.headers);
  }

  // only add global headers if there are global headers to add
  if (globalRequestGl && globalRequestGl.addlHeaders && Object.keys(globalRequestGl.addlHeaders).length > 0) {
    thisAHdata = transUtilInst.mergeObjects(thisAHdata, globalRequestGl.addlHeaders);
  }

  // if no header data passed in create empty
  if (!thisAHdata) {
    thisAHdata = {};
  }

  // 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 (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'PLAIN') {
      // add the Plain headers if they were not set already
      thisAHdata['Content-Type'] = 'text/plain';
    } else if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'XML') {
      // add the XML headers if they were not set already
      thisAHdata['Content-Type'] = 'application/xml';
    } else if (entitySchema && entitySchema.requestDatatype && entitySchema.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 (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'PLAIN') {
      // add the Plain headers if they were not set already
      thisAHdata.Accept = 'text/plain';
    } else if (entitySchema && entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'XML') {
      // add the XML headers if they were not set already
      thisAHdata.Accept = 'application/xml';
    } else if (entitySchema && entitySchema.responseDatatype && entitySchema.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'];
  }

  return thisAHdata;
}

/**
 * @summary Build the payload for the request
 *
 * @function buildPayload
 * @param {String} entity - the name of the entity action is on. (required)
 * @param {String} action - the name of the action being executed. (required)
 * @param {Object} entitySchema - the entity schema for the entity and action. (required)
 * @param {Object} payload - an object that contains the payload prior to translation
 *                              (optional). Can be a stringified Object.
 */
function buildPayload(entity, action, entitySchema, payload) {
  const origin = `${id}-restHandler-buildPayload`;
  log.trace(origin);

  try {
    // prepare the body parameters we received
    let thisBdata = transUtilInst.formatInputData(payload);

    // return the payload if just sending plain text and got plain text
    if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'PLAIN') {
      if (!thisBdata) {
        thisBdata = '';
      }
      if (typeof thisBdata !== 'object') {
        return thisBdata;
      }
    }
    // return the payload if just sending xml and got xml (as string)
    if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'XML') {
      if (!thisBdata) {
        thisBdata = '';
      }
      if (typeof thisBdata !== 'object') {
        return thisBdata;
      }
    }

    // only add body if there is payload to add and it is not a GET or if it is a GET and we need to send a body
    if ((entitySchema.method.toUpperCase() !== 'GET' || entitySchema.sendGetBody) && globalRequestGl && globalRequestGl.payload) {
      // if both are objects, merge the objects
      if (!Array.isArray(thisBdata) && typeof thisBdata === 'object' && !Array.isArray(globalRequestGl.payload)
          && typeof globalRequestGl.payload === 'object' && Object.keys(globalRequestGl.payload).length > 0) {
        thisBdata = transUtilInst.mergeObjects(thisBdata, globalRequestGl.payload);
      } else if (Array.isArray(thisBdata) && !Array.isArray(globalRequestGl.payload) && typeof globalRequestGl.payload === 'object'
          && Object.keys(globalRequestGl.payload).length > 0) {
        // if payload is an array of objects, add the global payload to each object - only one level deep
        for (let a = 0; a < thisBdata.length; a += 1) {
          if (!Array.isArray(thisBdata[a]) && typeof thisBdata[a] === 'object') {
            thisBdata[a] = transUtilInst.mergeObjects(thisBdata[a], globalRequestGl.payload);
          }
        }
      } else {
        log.warn(`${origin}: Payload and Global Payload can not be merged!`);
      }
    }

    // handle input as JSON
    if (!thisBdata) {
      thisBdata = {};
    }

    // request type set for determining required fields
    thisBdata.ph_request_type = action;

    // map the data we received to a [System] Entity
    const systemEntity = transUtilInst.mapToOutboundEntity(thisBdata, entitySchema.requestSchema);

    if (!systemEntity) {
      // create the error object
      const errorObj = {
        origin,
        type: 'Payload Not Translated',
        vars: []
      };

      // log and throw the error
      log.error(`${origin}: Payload not translated`);
      throw new Error(JSON.stringify(errorObj));
    }

    if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'URLENCODE') {
      return querystring.stringify(systemEntity);
    }

    if (entitySchema && entitySchema.requestDatatype && entitySchema.requestDatatype.toUpperCase() === 'JSON2XML') {
      try {
        return jsonxml(systemEntity);
      } catch (ex) {
        log.warn(`${origin}: Unable to get xml from json ${ex}`);
        return '';
      }
    }

    // return the payload
    return JSON.stringify(systemEntity);
  } catch (e) {
    return transUtilInst.checkAndThrow(e, origin, 'Issue building payload');
  }
}

class RestHandler {
  /**
   * Rest Entity
   * @constructor
   */
  constructor(prongId, properties, connectorCl, transUtilCl) {
    id = prongId;
    this.myid = prongId;

    // reference to the needed classes for class methods
    this.connector = connectorCl;
    connectorInst = this.connector;
    this.transUtil = transUtilCl;
    transUtilInst = this.transUtil;

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

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

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

    this.version = properties.version;
    versionGl = this.version;
    this.basepath = properties.base_path;
    basepathGl = this.basepath;
    this.globalRequest = null;
    this.encode = properties.encode_pathvars;
    encodePath = this.encode;
    this.encodeQ = properties.encode_queryvars;
    encodeUri = this.encodeQ;

    // only need to set returnRaw if the property is true - defaults to false
    if (properties.request && properties.request.return_raw) {
      returnRawGl = properties.request.return_raw;
    }

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

  /**
   * @summary Formats and makes the REST call
   *
   * @function genericRestRequest
   * @param {String} entity - the name of the entity for this request.
   *                          (required)
   * @param {String} action - the name of the action being executed. (required)
   * @param {Object} entitySchema - the schema for the entity the request is
   *                                for. (required)
   * @param {Object} requestObj - an object that contains all of the possible
   *                              parts of the request (payload, uriPathVars,
   *                              uriQuery, uriOptions and addlHeaders
   *                              (optional). Can be a stringified Object.
   * @param {Boolean} translate - whether to translate the response. Defaults
   *                              to true. If no translation will just return
   *                              "success" or an error message
   * @param {Function} callback - a callback function to return the result of the request
   */
  genericRestRequest(entity, action, entitySchema, requestObj, translate, callback) {
    const origin = `${this.myid}-restHandler-genericRestRequest`;
    log.trace(`${origin}: ${entity}-${action}`);

    try {
      // verify parameters passed are valid
      if (!entity) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['Entity'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
      if (!action) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['Action'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }
      if (!entitySchema) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['Entity Schema'], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      // verify path for call
      if (!entitySchema.entitypath) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Invalid Action File', ['missing entity path', `${entity}/${action}`], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      // verify method for call
      if (!entitySchema.method) {
        const errorObj = this.transUtil.formatErrorObject(origin, 'Invalid Action File', ['missing method', `${entity}/${action}`], null, null, null);
        log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
        return callback(null, errorObj);
      }

      // set the translate flag - defaults to true
      let translateFlag = true;
      if (typeof translate === 'boolean') {
        translateFlag = translate;
      }

      // get the path from the entity schema
      let uriPath = entitySchema.entitypath;
      const callMeth = entitySchema.method;

      // break out the variables in the requestObj
      let payload = null;
      let uriPathVars = null;
      let uriQuery = null;
      let uriOptions = null;
      let addlHeaders = null;
      let authData = null;
      let callProperties = null;
      let filter = null;
      let priority = -1;
      let event = null;
      let retReqHdr = false;

      if (requestObj !== null) {
        if (requestObj.payload) {
          ({ payload } = requestObj);
        }
        if (requestObj.uriPathVars) {
          ({ uriPathVars } = requestObj);
        }
        if (requestObj.uriQuery) {
          ({ uriQuery } = requestObj);
        }
        if (requestObj.uriOptions) {
          ({ uriOptions } = requestObj);
        }
        if (requestObj.addlHeaders) {
          ({ addlHeaders } = requestObj);
        }
        if (requestObj.authData) {
          ({ authData } = requestObj);
        }
        if (requestObj.callProperties) {
          ({ callProperties } = requestObj);
        }
        if (requestObj.filter) {
          ({ filter } = requestObj);
        }
        if (requestObj.priority) {
          ({ priority } = requestObj);
        }
        if (requestObj.event) {
          ({ event } = requestObj);
        }
        if (requestObj.retReqHdr) {
          ({ retReqHdr } = requestObj);
        }
      }

      // build the request path from the information provided
      const result = buildRequestPath(entity, action, entitySchema, uriPath, uriPathVars, uriQuery, uriOptions, callProperties);

      // reset the local variables
      uriPath = result.path;

      // merge the additional headers
      const thisAHdata = mergeHeaders(addlHeaders, entitySchema);

      // build the request path from the information provided
      const bodyString = buildPayload(entity, action, entitySchema, payload);

      if ((callMeth !== 'GET' || entitySchema.sendGetBody) && bodyString !== '{}') {
        thisAHdata['Content-length'] = Buffer.byteLength(bodyString);
      }

      // set up the request to be sent
      const request = {
        method: callMeth,
        path: uriPath,
        addlHeaders: thisAHdata,
        body: bodyString,
        origPath: entitySchema.entitypath,
        priority,
        event
      };

      if (authData) {
        request.authData = authData;
      }

      // actual call for the request
      if (translateFlag) {
        return handleRestRequest(request, 'map', entitySchema, callProperties, filter, retReqHdr, callback);
      }

      return handleRestRequest(request, 'nomap', entitySchema, callProperties, filter, retReqHdr, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue during generic request');
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Formats and makes the healthcheck call
   *
   * @function healthcheckRest
   * @param {Object} healthSchema - the schema for the healthcheck (optional)
   * @param {Object} requestObj - an object that contains all of the possible
   *                              parts of the request (payload, uriPathVars,
   *                              uriQuery, uriOptions and addlHeaders
   *                              (optional). Can be a stringified Object.
   * @param {Function} callback - a callback function to return the result of the healthcheck
   */
  healthcheckRest(healthSchema, requestObj, callback) {
    const origin = `${this.myid}-restHandler-healthcheckRest`;
    log.trace(origin);

    try {
      // break out the variables in the requestObj
      let payload = null;
      let uriPathVars = null;
      let uriQuery = null;
      let uriOptions = null;
      let addlHeaders = {};
      let authData = null;
      let callProperties = null;
      let filter = null;
      let priority = -1;
      let event = null;
      let retReqHdr = false;

      if (requestObj !== null) {
        if (requestObj.payload) {
          ({ payload } = requestObj);
        }
        if (requestObj.uriPathVars) {
          ({ uriPathVars } = requestObj);
        }
        if (requestObj.uriQuery) {
          ({ uriQuery } = requestObj);
        }
        if (requestObj.uriOptions) {
          ({ uriOptions } = requestObj);
        }
        if (requestObj.addlHeaders) {
          ({ addlHeaders } = requestObj);
        }
        if (requestObj.authData) {
          ({ authData } = requestObj);
        }
        if (requestObj.callProperties) {
          ({ callProperties } = requestObj);
        }
        if (requestObj.filter) {
          ({ filter } = requestObj);
        }
        if (requestObj.priority) {
          ({ priority } = requestObj);
        }
        if (requestObj.event) {
          ({ event } = requestObj);
        }
        if (requestObj.retReqHdr) {
          ({ retReqHdr } = requestObj);
        }
      }

      if (healthSchema) {
        // verify path for call
        if (!healthSchema.entitypath) {
          const errorObj = this.transUtil.formatErrorObject(origin, 'Invalid Action File', ['missing entity path', '.system/healthcheck'], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        // verify method for call
        if (!healthSchema.method) {
          const errorObj = this.transUtil.formatErrorObject(origin, 'Invalid Action File', ['missing method', '.system/healthcheck'], null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        // get the path from the entity schema
        let uriPath = healthSchema.entitypath;
        const callMeth = healthSchema.method;

        // build the request path from the information provided
        const result = buildRequestPath('.system', 'healthcheck', healthSchema, uriPath, uriPathVars, uriQuery, uriOptions, callProperties);

        // reset the local variables
        uriPath = result.path;

        // merge the additional headers
        const thisAHdata = mergeHeaders(addlHeaders, healthSchema);

        // build the request path from the information provided
        const bodyString = buildPayload('.system', 'healthcheck', healthSchema, payload);

        if ((callMeth !== 'GET' || healthSchema.sendGetBody) && bodyString !== '{}') {
          thisAHdata['Content-length'] = Buffer.byteLength(bodyString);
        }

        // set up the request to be sent
        const request = {
          method: callMeth,
          path: uriPath,
          addlHeaders: thisAHdata,
          body: bodyString,
          origPath: healthSchema.entitypath,
          priority,
          event
        };

        if (authData) {
          request.authData = authData;
        }

        // actual call for the request
        return handleRestRequest(request, 'nomap', healthSchema, callProperties, filter, retReqHdr, callback);
      }

      // call to run healthcheck - if using properties
      return this.connector.healthCheck(healthSchema, null, addlHeaders, callProperties, callback);
    } catch (e) {
      // handle any exception
      const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue during healthcheck 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}-restHandler-getQueue`;
    log.trace(origin);

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

module.exports = RestHandler;
