/* @copyright Itential, LLC 2018-9 */

// Set globals
/* global log */

/* NodeJS internal utilities */
const fs = require('fs');
const path = require('path');

// The crypto libraries
const cryptoJS = require('crypto-js');

let id = null;

/**
 * @summary Determines the order of version in the list of items
 *          Allowed formats:
 *            #, #.#, #.#.#, etc
 *            v#, v#.#, v#.#.#, etc
 *            ver#, ver#.#, ver#.#.#, etc
 *
 * @function itemOrder
 *
 * @param {String} firstItem - the firstItem (required)
 * @param {String} secondItem - the secondItem (required)
 *
 * @return {Number} -1 if firstItem is first; 0 if secondItem is first; 1 if equal
 */
function itemOrder(firstItem, secondItem) {
  const origin = `${id}-propertyUtil-itemOrder`;
  log.trace(origin);
  let myFirst = firstItem;
  let mySecond = secondItem;

  // strip off 'v' or 'ver' so can handle like other . seperated things
  if (myFirst) {
    if (myFirst.toUpperCase().startsWith('VER')) {
      myFirst = myFirst.substring(3);
    }
    if (myFirst.toUpperCase().startsWith('V')) {
      myFirst = myFirst.substring(1);
    }
  }
  if (mySecond) {
    if (mySecond.toUpperCase().startsWith('VER')) {
      mySecond = mySecond.substring(3);
    }
    if (mySecond.toUpperCase().startsWith('V')) {
      mySecond = mySecond.substring(1);
    }
  }

  // split the items based on versioning '.'
  const firstParts = myFirst.split('.');
  const secondParts = mySecond.split('.');

  // like string compare in case not just #s but issue with different size #s (2 > 10)
  // pad in front with leading 0 (assume number want same digits)
  while (firstParts[0].length < secondParts[0].length) {
    firstParts[0] = `0${firstParts[0]}`;
  }
  while (secondParts[0].length < firstParts[0].length) {
    secondParts[0] = `0${secondParts[0]}`;
  }

  // if first is less
  if (firstParts[0] < secondParts[0]) {
    return -1;
  }
  // if second is less
  if (firstParts[0] > secondParts[0]) {
    return 0;
  }
  // can not continue if firstPart is length 1
  if (firstParts.length === 1) {
    // both length 1 and equal value
    if (secondParts.length === 1) {
      return 1;
    }
    // second one longer length (first 3; second 3.3) - so first is less
    return -1;
  }
  // can not continue if secondPart is length 1
  if (secondParts.length === 1) {
    // first one longer length (first 3.3; second 3) - so second is less
    return 0;
  }

  // both have to be at least length 2 and equal first
  // remove the first item for each
  firstParts.shift();
  secondParts.shift();

  // recursive call - putting back together remaining parts
  return itemOrder(firstParts.join('.'), secondParts.join('.'));
}

/**
 * @summary Goes through the available paths and returns the appropriate one
 *
 * @function returnPath
 *
 * @param {String} value - the text to encrypt (required)
 * @param {Object} eInfo - the encryption information (optional)
 *
 * @return {String} the path to use in the call
 */
function returnPath(actionInfo, entityName, actionName, choosePath, startV, endV) {
  const origin = `${id}-propertyUtil-returnPath`;
  log.trace(origin);

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

  // if there is no entitypath - throw error
  if (!actionInfo.entitypath) {
    // add the specific pieces of the error object
    errorObj.type = 'Invalid Action File';
    errorObj.vars = ['missing entity path for action', entityName];

    // log (if not system entity) and return the error
    if (entityName !== '.system') {
      log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing entity path`);
      errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing entity path`];
    }

    throw new Error(JSON.stringify(errorObj));
  }

  // if entitypath is not a string or an object - throw error
  if ((typeof actionInfo.entitypath !== 'string' && typeof actionInfo.entitypath !== 'object') || Array.isArray(actionInfo.entitypath)) {
    // add the specific pieces of the error object
    errorObj.type = 'Invalid Action File';
    errorObj.vars = ['invalid entity path for action', entityName];

    // log (if not system entity) and return the error
    if (entityName !== '.system') {
      log.warn(`${origin}: Entity ${entityName} action ${actionName} - invalid entity path`);
      errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - invalid entity path`];
    }

    throw new Error(JSON.stringify(errorObj));
  }

  // if choosepath & it is before start - return error
  if (choosePath && startV && itemOrder(startV, choosePath) === 0) {
    // add the specific pieces of the error object
    errorObj.type = 'Action Not Available';
    errorObj.vars = ['not available until', startV];

    // log (if not system entity) and return the error
    if (entityName !== '.system') {
      log.warn(`${origin}: Entity ${entityName} action ${actionName} - not available until version ${startV}`);
      errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - not available until version ${startV}`];
    }

    throw new Error(JSON.stringify(errorObj));
  }

  // if choosepath & it is after end - return error
  if (choosePath && endV && itemOrder(choosePath, endV) === 0) {
    // add the specific pieces of the error object
    errorObj.type = 'Action Not Available';
    errorObj.vars = ['end of life in', endV];

    // log (if not system entity) and return the error
    if (entityName !== '.system') {
      log.warn(`${origin}: Entity ${entityName} action ${actionName} - end of life in version ${endV}`);
      errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - end of life in version ${endV}`];
    }

    throw new Error(JSON.stringify(errorObj));
  }

  // if the entitypath is a string - return it
  if (typeof actionInfo.entitypath === 'string') {
    // return the path we have
    return actionInfo.entitypath;
  }

  // if the entitypath is an object instead of a string need to figure out which path to use
  const ekeys = Object.keys(actionInfo.entitypath);

  // if entitypath is object with no fields - throw error
  if (ekeys.length === 0) {
    // add the specific pieces of the error object
    errorObj.type = 'Invalid Action File';
    errorObj.vars = ['missing entity path for action', entityName];

    // log (if not system entity) and return the error
    if (entityName !== '.system') {
      log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing entity path`);
      errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing entity path`];
    }

    throw new Error(JSON.stringify(errorObj));
  }

  // if nothing for me to base choice on
  if (!choosePath) {
    // the first one is the default so return it
    return actionInfo.entitypath[ekeys[0]];
  }

  // initialization
  let foundPath = '';
  let beforePath = ekeys[0];

  // go through the paths for an exact match
  for (let ep = 0; ep < ekeys.length; ep += 1) {
    // see if the key matches the choosepath value, if it does set the entitypath
    if (choosePath && ekeys[ep] === choosePath) {
      foundPath = actionInfo.entitypath[ekeys[ep]];
      break;
    }

    // see if path key is before choosePath but after current beforePath (in between) - reset beforePath
    if (itemOrder(choosePath, ekeys[ep]) === 0 && itemOrder(beforePath, ekeys[ep]) === -1) {
      beforePath = ekeys[ep];
    }
  }

  // if exact match was found
  if (foundPath) {
    return foundPath;
  }

  // if no exact then use the beforePath
  return actionInfo.entitypath[beforePath];
}

class AdapterPropertyUtil {
  /**
   * Adapter Translator Utility
   * @constructor
   */
  constructor(prongId, directory) {
    id = prongId;
    this.myid = prongId;
    this.baseDir = directory;
  }

  // GENERIC UTILITY CALLS FOR PROPERTIES
  /**
   * @summary Get the entity schema and information for the action
   *
   * @function getEntitySchemaFromFS
   * @param {String} entityName - the name of the entity (required)
   * @param {String} actionName - the name of the action to take (required)
   *
   * @return {Object} entitySchema - the entity schema object
   */
  getEntitySchemaFromFS(entityName, actionName, choosepath) {
    const origin = `${this.myid}-propertyUtil-getEntitySchemaFromFS`;
    log.trace(origin);

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

    try {
      // verify required data
      if (!entityName || typeof entityName !== 'string') {
        // add the specific pieces of the error object
        errorObj.type = 'Missing Data';
        errorObj.vars = ['Entity'];
        log.error(`${origin}: Entity is required to get entity schema for action`);
        throw new Error(JSON.stringify(errorObj));
      }
      if (!actionName || typeof actionName !== 'string') {
        // add the specific pieces of the error object
        errorObj.type = 'Missing Data';
        errorObj.vars = ['Action'];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Action is required to get entity schema for action`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // get the path for the specific action file
      const actionFile = path.join(this.baseDir, `/entities/${entityName}/action.json`);

      // if the file does not exist - error
      if (!fs.existsSync(actionFile)) {
        // add the specific pieces of the error object
        errorObj.type = 'Missing File';
        errorObj.vars = [actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Could not find file - ${actionFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

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

      // handle possible errors in the action file
      if (!entityActions || typeof entityActions !== 'object') {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['invalid format', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Invalid entity action file, please verify file: ${actionFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }
      if (!entityActions.actions || !Array.isArray(entityActions.actions)) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing array of actions', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Invalid action file syntax ${actionFile} - must contain an array of actions`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      let actionInfo = null;

      // get the specific action information
      for (let i = 0; i < entityActions.actions.length; i += 1) {
        if (entityActions.actions[i].name === actionName) {
          actionInfo = entityActions.actions[i];
        }
      }

      // if there are no actions - invalid
      if (actionInfo === null) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing action', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Entity ${entityName} - action file missing action: ${actionName}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // verify required action info - protocol, method, entitypath and schema
      if (!actionInfo.protocol) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing protocol', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Entity ${entityName} action ${actionName} - missing protocol`);
        }
        throw new Error(JSON.stringify(errorObj));
      }
      if (!actionInfo.method) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing method', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Entity ${entityName} action ${actionName} - missing method`);
        }
        throw new Error(JSON.stringify(errorObj));
      }
      if (!actionInfo.entitypath) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing entity path', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Entity ${entityName} action ${actionName} - missing entity path`);
        }
        throw new Error(JSON.stringify(errorObj));
      }
      if (!actionInfo.schema && (!actionInfo.requestSchema || !actionInfo.responseSchema)) {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Action File';
        errorObj.vars = ['missing schema', actionFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Entity ${entityName} action ${actionName} - missing schema`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // get the schema file name
      let reqSchemaName = actionInfo.schema;
      let respSchemaName = actionInfo.schema;

      // if there is a separate request schema file it overrides the default schema
      if (actionInfo.requestSchema) {
        reqSchemaName = actionInfo.requestSchema;
      }
      // if there is a separate response schema file it overrides the default schema
      if (actionInfo.responseSchema) {
        respSchemaName = actionInfo.responseSchema;
      }

      // get the path for the specific schema file
      const reqSchemaFile = path.join(this.baseDir, `/entities/${entityName}/${reqSchemaName}`);
      const respSchemaFile = path.join(this.baseDir, `/entities/${entityName}/${respSchemaName}`);
      const errorSchemaFile = path.join(this.baseDir, '/entities/.system/errorSchema');

      // if the file does not exist - error
      if (!fs.existsSync(reqSchemaFile)) {
        // add the specific pieces of the error object
        errorObj.type = 'Missing File';
        errorObj.vars = [reqSchemaFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Could not find file - ${reqSchemaFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // if the file does not exist - error
      if (!fs.existsSync(respSchemaFile)) {
        // add the specific pieces of the error object
        errorObj.type = 'Missing File';
        errorObj.vars = [respSchemaFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Could not find file - ${respSchemaFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // Read the entity schema from the file system
      const entitySchema = {
        requestSchema: JSON.parse(fs.readFileSync(reqSchemaFile, 'utf-8')),
        responseSchema: JSON.parse(fs.readFileSync(respSchemaFile, 'utf-8'))
      };

      // handle possible errors on the schema
      if (!entitySchema.requestSchema || typeof entitySchema.requestSchema !== 'object') {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Schema File';
        errorObj.vars = ['invalid format', reqSchemaFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Invalid entity request schema, please verify file: ${reqSchemaFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }
      if (!entitySchema.responseSchema || typeof entitySchema.responseSchema !== 'object') {
        // add the specific pieces of the error object
        errorObj.type = 'Invalid Schema File';
        errorObj.vars = ['invalid format', respSchemaFile];

        // log (if not system entity) and throw the error
        if (entityName !== '.system') {
          log.error(`${origin}: Invalid entity response schema, please verify file: ${respSchemaFile}`);
        }
        throw new Error(JSON.stringify(errorObj));
      }

      // if the error schema file exist - read it in
      if (fs.existsSync(errorSchemaFile)) {
        entitySchema.errorSchema = JSON.parse(fs.readFileSync(errorSchemaFile, 'utf-8'));

        // if the error schema file is bad, warn about it but continue and use the global on!
        if (entitySchema.errorSchema && typeof entitySchema.errorSchema !== 'object') {
          log.warn(`${origin}: Invalid error schema, please verify file: ${errorSchemaFile}`);
        }
      }

      // Merge the information into the entity schema
      entitySchema.name = actionName;
      entitySchema.protocol = actionInfo.protocol;
      entitySchema.method = actionInfo.method;
      entitySchema.timeout = actionInfo.timeout;
      entitySchema.entitypath = actionInfo.entitypath;
      entitySchema.querykey = '?';
      entitySchema.responseObjects = [];
      entitySchema.mockresponses = [];

      // Determine the path to use for this call
      entitySchema.entitypath = returnPath(actionInfo, entityName, actionName, choosepath, actionInfo.startVersion, actionInfo.endVersion);

      // if info provided, replace the defaults
      if (actionInfo.querykey) {
        entitySchema.querykey = actionInfo.querykey;
      }
      if (actionInfo.responseObjects) {
        entitySchema.responseObjects = actionInfo.responseObjects;
      }
      if (actionInfo.headers) {
        entitySchema.headers = actionInfo.headers;
      }
      if (actionInfo.requestDatatype) {
        entitySchema.requestDatatype = actionInfo.requestDatatype;
      } else if (actionInfo.datatype) {
        entitySchema.requestDatatype = actionInfo.datatype;
      } else {
        entitySchema.requestDatatype = 'JSON';
      }
      if (actionInfo.responseDatatype) {
        entitySchema.responseDatatype = actionInfo.responseDatatype;
      } else if (actionInfo.datatype) {
        entitySchema.responseDatatype = actionInfo.datatype;
      } else {
        entitySchema.responseDatatype = 'JSON';
      }
      if (Object.hasOwnProperty.call(actionInfo, 'sendEmpty')) {
        entitySchema.sendEmpty = actionInfo.sendEmpty;
      }
      if (Object.hasOwnProperty.call(actionInfo, 'sendGetBody')) {
        entitySchema.sendGetBody = actionInfo.sendGetBody;
      }
      if (actionInfo.sso) {
        entitySchema.sso = actionInfo.sso;
      }

      // need to make sure we have supported datatypes - PLAIN is best if not supported - no translation or encoding
      if (entitySchema.requestDatatype.toUpperCase() !== 'JSON' && entitySchema.requestDatatype.toUpperCase() !== 'XML'
        && entitySchema.requestDatatype.toUpperCase() !== 'URLENCODE' && entitySchema.requestDatatype.toUpperCase() !== 'URLQUERY'
        && entitySchema.requestDatatype.toUpperCase() !== 'FORM' && entitySchema.requestDatatype.toUpperCase() !== 'JSON2XML') {
        entitySchema.requestDatatype = 'PLAIN';
      }
      if (entitySchema.responseDatatype.toUpperCase() !== 'JSON' && entitySchema.responseDatatype.toUpperCase() !== 'XML'
        && entitySchema.responseDatatype.toUpperCase() !== 'URLENCODE' && entitySchema.responseDatatype.toUpperCase() !== 'XML2JSON') {
        entitySchema.responseDatatype = 'PLAIN';
      }

      // go through each response object to see if there is mock data
      for (let i = 0; i < entitySchema.responseObjects.length; i += 1) {
        entitySchema.responseObjects[i].name = entitySchema.entitypath;
        entitySchema.responseObjects[i].method = entitySchema.method;

        // if there is mock data, read the mock data from the file system
        if (entitySchema.responseObjects[i].mockFile) {
          const mockResponse = {
            name: entitySchema.responseObjects[i].name,
            method: entitySchema.responseObjects[i].method,
            type: entitySchema.responseObjects[i].type,
            file: entitySchema.responseObjects[i].mockFile
          };

          // get the mock file name
          const tempName = entitySchema.responseObjects[i].mockFile;

          // if the file name was provided
          if (tempName) {
            const mockFileName = path.join(this.baseDir, `/entities/${entityName}/${tempName}`);

            // if the file does not exist - throw warning
            if (fs.existsSync(mockFileName)) {
              // set the normal headers based on the type of data for the call
              if (entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'PLAIN') {
                // read the mock date from the file system
                mockResponse.response = fs.readFileSync(mockFileName, 'utf-8');
                if (!mockResponse.response) {
                  mockResponse.response = 'mock file empty!';
                }
              } else if (entitySchema.responseDatatype && (entitySchema.responseDatatype.toUpperCase() === 'XML'
                || entitySchema.responseDatatype.toUpperCase() === 'XML2JSON')) {
                // read the mock date from the file system
                mockResponse.response = fs.readFileSync(mockFileName, 'utf-8');
                if (!mockResponse.response) {
                  mockResponse.response = '<mock>file empty!</mock>';
                }
              } else {
                // read the mock date from the file system
                try {
                  // parse the mockdata file to store it as an object
                  mockResponse.response = JSON.parse(fs.readFileSync(mockFileName, 'utf-8'));
                } catch (excep) {
                  log.warn(`${origin}: Could not parse file - ${mockFileName}`);
                  mockResponse.response = { mock: 'file empty or parse error!' };
                }
              }
            } else {
              log.warn(`${origin}: Could not find file - ${mockFileName}`);
              mockResponse.response = null;
            }
          } else {
            mockResponse.response = null;
          }

          // add the response to the array of mock responses
          entitySchema.mockresponses.push(mockResponse);
        }
      }

      // return the entity schema
      return entitySchema;
    } catch (e) {
      let internal = null;
      errorObj.type = 'Caught Exception';
      errorObj.vars = [];
      errorObj.exception = e;

      // determine if we already had an internal message
      try {
        internal = JSON.parse(e.message);
      } catch (ex) {
        // message was not internal
        log.error(`${origin}: Issue parsing entity schema: ${e}`);
        internal = null;
      }

      // return the appropriate error message
      if (internal && internal.origin && internal.type) {
        throw e;
      } else {
        throw new Error(JSON.stringify(errorObj));
      }
    }
  }

  /**
   * @summary Build the entitySchema structure from DB config
   *
   * @function getEntitySchemaFromDB
   * @param {Object} dbObj - the database connect information (optional)
   * @param {String} entityName - the name of the entity (required)
   * @param {String} actionName - the name of the action to take (required)
   *
   * @return {Object} entitySchema - the entity schema object
   */
  getEntitySchemaFromDB(dbObj, entityName, actionName, choosepath, dbUtils, callback) {
    const origin = `${this.myid}-propertyUtil-getEntitySchemaFromDB`;
    log.trace(origin);

    // create the generic part of an error object
    const errorObj = {
      origin,
      isError: true
    };
    try {
      // verify required data
      if (!entityName || typeof entityName !== 'string') {
        // add the specific pieces of the error object
        errorObj.type = 'Missing Data';
        errorObj.vars = ['Entity'];
        log.debug(`${origin}: Entity is required to get entity schema for action`);
        errorObj.error = [`${origin}: Entity is required to get entity schema for action`];
        return callback(null, errorObj);
      }
      if (!actionName || typeof actionName !== 'string') {
        // add the specific pieces of the error object
        errorObj.type = 'Missing Data';
        errorObj.vars = ['Action'];

        // log (if not system entity) and return the error
        if (entityName !== '.system') {
          log.debug(`${origin}: Action is required to get entity schema for action`);
          errorObj.error = [`${origin}: Action is required to get entity schema for action`];
        }
        return callback(null, errorObj);
      }

      let entitySchema = null;

      // set up the options-filter
      const dbOpts = {
        filter: {
          id: this.myid,
          entity: entityName
        },
        entity: entityName
      };

      // call to get the adapter schema from the database
      return dbUtils.find('adapter_configs', dbOpts, dbObj, null, (dbError, dbResult) => {
        if (dbError) {
          // add the specific pieces of the error object
          errorObj.type = 'Database Error';
          errorObj.vars = ['dbError'];
          log.debug(`${origin}: Database Error: ${dbError}`);
          errorObj.error = [`${origin}: Database Error: ${dbError}`];
          return callback(null, errorObj);
        }
        if (!dbResult) {
          // add the specific pieces of the error object
          errorObj.type = 'Missing Data';
          errorObj.vars = ['Entity'];
          log.debug(`${origin}: Entity was not found in the database`);
          errorObj.error = [`${origin}: Entity was not found in the database`];
          return callback(null, errorObj);
        }

        // Read the action from the entity object
        const entityActions = dbResult[0];

        // handle possible errors in the action file
        if (!entityActions || typeof entityActions !== 'object') {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['invalid format', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Invalid entity action file, please verify file: ${entityName}`);
            errorObj.error = [`${origin}: Invalid entity action file, please verify file: ${entityName}`];
          }
          return callback(null, errorObj);
        }
        if (!entityActions.actions || !Array.isArray(entityActions.actions)) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing array of actions', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Invalid action file syntax ${entityName} - must contain an array of actions`);
            errorObj.error = [`${origin}: Invalid action file syntax ${entityName} - must contain an array of actions`];
          }
          return callback(null, errorObj);
        }

        let actionInfo = null;

        // get the specific action information
        for (let i = 0; i < entityActions.actions.length; i += 1) {
          if (entityActions.actions[i].name === actionName) {
            actionInfo = entityActions.actions[i];
          }
        }

        // if there are no actions - invalid
        if (actionInfo === null) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing action', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Entity ${entityName} - action file missing action: ${actionName}`);
            errorObj.error = [`${origin}: Entity ${entityName} - action file missing action: ${actionName}`];
          }
          return callback(null, errorObj);
        }

        // verify required action info - protocol, method, entitypath and schema
        if (!actionInfo.protocol) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing protocol', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing protocol`);
            errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing protocol`];
          }
          return callback(null, errorObj);
        }
        if (!actionInfo.method) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing method', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing method`);
            errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing method`];
          }
          return callback(null, errorObj);
        }
        if (!actionInfo.entitypath) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing entity path', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing entity path`);
            errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing entity path`];
          }
          return callback(null, errorObj);
        }
        if (!actionInfo.schema && (!actionInfo.requestSchema || !actionInfo.responseSchema)) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Action File';
          errorObj.vars = ['missing schema', entityName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Entity ${entityName} action ${actionName} - missing schema`);
            errorObj.error = [`${origin}: Entity ${entityName} action ${actionName} - missing schema`];
          }
          return callback(null, errorObj);
        }

        // get the schema file name
        let reqSchemaName = actionInfo.schema;
        let respSchemaName = actionInfo.schema;

        // if there is a separate request schema file it overrides the default schema
        if (actionInfo.requestSchema) {
          reqSchemaName = actionInfo.requestSchema;
        }
        // if there is a separate response schema file it overrides the default schema
        if (actionInfo.responseSchema) {
          respSchemaName = actionInfo.responseSchema;
        }

        if (!entityActions.schema || !Array.isArray(entityActions.schema)) {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Schema File';
          errorObj.vars = ['missing array of schemas', reqSchemaName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Invalid schema file syntax ${reqSchemaName} - must contain an array of schemas`);
            errorObj.error = [`${origin}: Invalid schema file syntax ${reqSchemaName} - must contain an array of schemas`];
          }
          return callback(null, errorObj);
        }

        let reqSchemaInfo = null;
        let respSchemaInfo = null;

        // get the specific action information
        for (let i = 0; i < entityActions.schema.length; i += 1) {
          if (entityActions.schema[i].name === reqSchemaName) {
            reqSchemaInfo = entityActions.schema[i].schema;
          }
          if (entityActions.schema[i].name === respSchemaName) {
            respSchemaInfo = entityActions.schema[i].schema;
          }
        }

        // if there are no actions - invalid
        if (reqSchemaInfo === null) {
          // add the specific pieces of the error object
          errorObj.type = 'Missing File';
          errorObj.vars = [reqSchemaName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Could not find file - ${reqSchemaName}`);
            errorObj.error = [`${origin}: Could not find file - ${reqSchemaName}`];
          }
          return callback(null, errorObj);
        }

        // if there are no actions - invalid
        if (respSchemaInfo === null) {
          // add the specific pieces of the error object
          errorObj.type = 'Missing File';
          errorObj.vars = [respSchemaName];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Could not find file - ${respSchemaName}`);
            errorObj.error = [`${origin}: Could not find file - ${respSchemaName}`];
          }
          return callback(null, errorObj);
        }

        // handle possible errors on the schema
        if (typeof reqSchemaInfo !== 'object') {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Schema File';
          errorObj.vars = ['invalid format', reqSchemaInfo];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Invalid entity request schema, please verify file: ${reqSchemaInfo}`);
            errorObj.error = [`${origin}: Invalid entity request schema, please verify file: ${reqSchemaInfo}`];
          }
          return callback(null, errorObj);
        }
        if (typeof respSchemaInfo !== 'object') {
          // add the specific pieces of the error object
          errorObj.type = 'Invalid Schema File';
          errorObj.vars = ['invalid format', respSchemaInfo];

          // log (if not system entity) and return the error
          if (entityName !== '.system') {
            log.warn(`${origin}: Invalid entity response schema, please verify file: ${respSchemaInfo}`);
            errorObj.error = [`${origin}: Invalid entity response schema, please verify file: ${respSchemaInfo}`];
          }
          return callback(null, errorObj);
        }

        // Read the entity schema from the file system
        entitySchema = {
          requestSchema: reqSchemaInfo,
          responseSchema: respSchemaInfo
        };

        // Merge the information into the entity schema
        entitySchema.name = actionName;
        entitySchema.protocol = actionInfo.protocol;
        entitySchema.method = actionInfo.method;
        entitySchema.timeout = actionInfo.timeout;
        entitySchema.entitypath = actionInfo.entitypath;
        entitySchema.querykey = '?';
        entitySchema.responseObjects = [];
        entitySchema.mockresponses = [];

        // Determine the path to use for this call
        entitySchema.entitypath = returnPath(actionInfo, entityName, actionName, choosepath, actionInfo.startVersion, actionInfo.endVersion);

        // if info provided, replace the defaults
        if (actionInfo.querykey) {
          entitySchema.querykey = actionInfo.querykey;
        }
        if (actionInfo.responseObjects) {
          entitySchema.responseObjects = actionInfo.responseObjects;
        }
        if (actionInfo.headers) {
          entitySchema.headers = actionInfo.headers;
        }
        if (actionInfo.requestDatatype) {
          entitySchema.requestDatatype = actionInfo.requestDatatype;
        } else if (actionInfo.datatype) {
          entitySchema.requestDatatype = actionInfo.datatype;
        } else {
          entitySchema.requestDatatype = 'JSON';
        }
        if (actionInfo.responseDatatype) {
          entitySchema.responseDatatype = actionInfo.responseDatatype;
        } else if (actionInfo.datatype) {
          entitySchema.responseDatatype = actionInfo.datatype;
        } else {
          entitySchema.responseDatatype = 'JSON';
        }
        if (Object.hasOwnProperty.call(actionInfo, 'sendEmpty')) {
          entitySchema.sendEmpty = actionInfo.sendEmpty;
        }
        if (Object.hasOwnProperty.call(actionInfo, 'sendGetBody')) {
          entitySchema.sendGetBody = actionInfo.sendGetBody;
        }
        if (actionInfo.sso) {
          entitySchema.sso = actionInfo.sso;
        }

        // need to make sure we have supported datatypes - PLAIN is best if not supported - no translation or encoding
        if (entitySchema.requestDatatype.toUpperCase() !== 'JSON' && entitySchema.requestDatatype.toUpperCase() !== 'XML'
          && entitySchema.requestDatatype.toUpperCase() !== 'URLENCODE' && entitySchema.requestDatatype.toUpperCase() !== 'URLQUERY'
          && entitySchema.requestDatatype.toUpperCase() !== 'FORM' && entitySchema.requestDatatype.toUpperCase() !== 'JSON2XML') {
          entitySchema.requestDatatype = 'PLAIN';
        }
        if (entitySchema.responseDatatype.toUpperCase() !== 'JSON' && entitySchema.responseDatatype.toUpperCase() !== 'XML'
          && entitySchema.responseDatatype.toUpperCase() !== 'URLENCODE' && entitySchema.responseDatatype.toUpperCase() !== 'XML2JSON') {
          entitySchema.responseDatatype = 'PLAIN';
        }

        // go through each response object to see if there is mock data
        for (let i = 0; i < entitySchema.responseObjects.length; i += 1) {
          entitySchema.responseObjects[i].name = entitySchema.entitypath;
          entitySchema.responseObjects[i].method = entitySchema.method;

          // if there is mock data, read the mock data from the file system
          if (entitySchema.responseObjects[i].mockFile) {
            const mockResponse = {
              name: entitySchema.responseObjects[i].name,
              method: entitySchema.responseObjects[i].method,
              type: entitySchema.responseObjects[i].type,
              file: entitySchema.responseObjects[i].mockFile
            };

            // get the mock file name
            const tempName = entitySchema.responseObjects[i].mockFile;

            // if the file name was provided
            if (tempName) {
              // need to get just the file name
              const mockFileArray = tempName.split('/');
              const mockFileName = mockFileArray[mockFileArray.length - 1];
              const mockResp = entityActions.mockdatafiles[mockFileName];

              if (mockResp) {
                // set the normal headers based on the type of data for the call
                if (entitySchema.responseDatatype && entitySchema.responseDatatype.toUpperCase() === 'PLAIN') {
                  // read the mock date from the file system
                  mockResponse.response = mockResp;
                } else if (entitySchema.responseDatatype && (entitySchema.responseDatatype.toUpperCase() === 'XML'
                  || entitySchema.responseDatatype.toUpperCase() === 'XML2JSON')) {
                  // read the mock date from the file system
                  mockResponse.response = mockResp;
                } else {
                  // read the mock date from the file system
                  try {
                    // parse the mockdata file to store it as an object
                    mockResponse.response = mockResp;
                  } catch (excep) {
                    log.warn(`${origin}: Could not parse file - ${mockFileName}`);
                    mockResponse.response = '';
                  }
                }
              } else {
                log.warn(`${origin}: Could not find file - ${mockFileName}`);
                mockResponse.response = null;
              }
            } else {
              mockResponse.response = null;
            }

            // add the response to the array of mock responses
            entitySchema.mockresponses.push(mockResponse);
          }
        }

        // return the entity schema
        return callback(entitySchema, null);
      });
    } catch (e) {
      errorObj.type = 'Caught Exception';
      errorObj.vars = [];
      errorObj.exception = e;

      // return the error message
      log.info(`unable to get adapter config from database: ${e}`);
      return callback(null, errorObj);
    }
  }

  /**
   * @summary Get the entity schema and information for the action
   *
   * @function getEntitySchema
   * @param {String} entityName - the name of the entity (required)
   * @param {String} actionName - the name of the action to take (required)
   *
   * @return {Object} entitySchema - the entity schema object
   */
  getEntitySchema(entityName, actionName, choosepath, dbUtils, callback) {
    const origin = `${this.myid}-propertyUtil-getEntitySchema`;
    log.trace(origin);
    // need to try to get the entity schema from the adapter database
    try {
      // call to get the adapter schema from the database
      return this.getEntitySchemaFromDB(null, entityName, actionName, choosepath, dbUtils, (dbresp, dberror) => {
        // if we got an error back - just means db config not in place
        if (dberror || !dbresp || (dbresp && Object.keys(dbresp).length === 0)) {
          log.debug('unable to get adapter config from adapter database');

          // Temporarily hardcode the adapter properties
          const iapDB = {
            dburl: 'mongodb://127.0.0.1:27017',
            dboptions: {},
            database: 'pronghorn'
          };

          // need to try to get the entity schema from the iap database
          return this.getEntitySchemaFromDB(iapDB, entityName, actionName, choosepath, dbUtils, (iapdbresp, iapdberror) => {
            // if we got an error back - just means db config not in place
            if (iapdberror || !iapdbresp || (iapdbresp && Object.keys(iapdbresp).length === 0)) {
              log.debug('unable to get adapter config from iap database');

              // need to try to get the entity schema from the filesystem
              log.debug('returning adapter config from file system');
              try {
                return callback(this.getEntitySchemaFromFS(entityName, actionName, choosepath), null);
              } catch (exc) {
                log.error(`Exception caught on File System ${exc.message}`);
                return callback(null, exc);
              }
            }

            // return the iap db config
            log.debug(`returning adapter config from iap database ${iapdbresp}`);
            return callback(iapdbresp, null);
          });
        }

        // return the adapter db config
        log.debug(`returning adapter config from adapter database ${dbresp}`);
        return callback(dbresp, null);
      });
    } catch (exaddb) {
      log.debug(`unable to get adapter config: ${exaddb}`);
      return callback(null, exaddb);
    }
  }

  /**
   * @summary Takes in propertiesSchema and creates an object with all of the
   * defaults set.
   *
   * @function setDefaults
   * @param {Object} propSchema - the proeprty schema
   *
   * @return {Object} the object with default values from the property schema
   */
  setDefaults(propSchema) {
    const origin = `${this.myid}-propertyUtil-setDefaults`;
    log.trace(origin);

    const defaults = {};

    // verify the input for the method
    if (!propSchema || !propSchema.properties) {
      return defaults;
    }

    const propKeys = Object.keys(propSchema.properties);

    // loop through all of the properties in the schema
    for (let k = 0; k < propKeys.length; k += 1) {
      const thisProp = propSchema.properties[propKeys[k]];

      // if this key is to a reference
      if (thisProp.$ref) {
        const refs = thisProp.$ref.split('/');

        // references should be to a sub of another section like definitions
        // like - #/definitions/credentials
        if (refs.length >= 3) {
          // recursive call with reference object
          defaults[propKeys[k]] = this.setDefaults(propSchema[refs[1]][refs[2]]);
        }
      } else if (Object.hasOwnProperty.call(thisProp, 'default')) {
        // if there is a default put into the object
        defaults[propKeys[k]] = thisProp.default;
      }
    }

    // return the defaults
    return defaults;
  }

  /**
   * @summary Takes in an item that may have sensitive data and scrubs it before
   * it would get logged.
   *
   * @function scrubSensitiveInfo
   * @param {String/Object} inData - the data to scrub
   * @param {Array} addItems - additional items to scrub
   *
   * @return {Object} the object with default values from the property schema
   */
  scrubSensitiveInfo(inData, addItems) {
    const origin = `${this.myid}-propertyUtil-scrubSensitiveInfo`;
    log.trace(origin);

    // no reason to scan numbers, booleans or functions
    if (!inData || typeof inData === 'number' || typeof inData === 'boolean' || typeof inData === 'function') {
      return inData;
    }

    // This is the array of sensitive keys
    let sensList = ['authorization', 'x-auth-token', 'x-csrf-token', 'x-amz-security-token', 'x-aws-ec2-metadata-token', 'cookie', 'set-cookie', 'token', 'tokenp2', 'user', 'username', 'passwd', 'password', 'api-key', 'client-id', 'client-secret', 'session', 'session-id', 'jsessionid'];

    // add any additional items to scrub
    if (addItems && Array.isArray(addItems) && addItems.length > 0) {
      sensList = sensList.concat(addItems);
    }

    // going to use copy of data so we do not mess up input - if object will still need to assign it
    let actualData = inData;

    // if we are scrubbbing a string (e.g. URL)
    if (typeof actualData === 'string') {
      // if it is a Stringified JSON
      try {
        // need to see if it is stringified JSON
        actualData = JSON.parse(inData);
        // if this was able to be parsed, we should handle it as an object
      } catch (ex) {
        // if not JSON, can only scrub the query (e.g. after ?)
        actualData = inData;
        const dataParts = actualData.split('?');

        // if there is no query data - we are done
        if (dataParts.length === 0) {
          return actualData;
        }

        // start what we return
        let count = 0;
        let retData = '';
        let queryData;
        // If there is query daya
        if (actualData.indexOf('?') > -1) {
          retData = `${dataParts[0]}?`;
          // query format - key=value& or key=value{end of url}
          queryData = dataParts[1].split('&');
        } else {
          // If its a urlencode body
          queryData = dataParts[0].split('&');
        }

        // analyze the query fields
        for (let i = 0; i < queryData.length; i += 1) {
          // key of the query field
          const key = queryData[i].split('=')[0];
          let found = false;

          // add any & to separate query params
          if (count > 0) {
            retData += '&';
          }

          // go through sensitive word list - maybe can use find in
          for (let j = 0; j < sensList.length; j += 1) {
            if (key.toUpperCase() === sensList[j].toUpperCase()) {
              // if sensitive, mask
              retData += `${key}=** masked **`;
              found = true;
              count += 1;
              break;
            }
          }

          // if not sensitive - just append back on url
          if (!found) {
            retData += queryData[i];
          }
        }

        // return the scrubbed string
        return retData;
      }
    }

    // want to make a copy and not alter the original object or array
    const retData = JSON.parse(JSON.stringify(actualData));

    // if we are scrubbing an array
    if (Array.isArray(retData)) {
      // need to go through each item in the array
      for (let i = 0; i < retData.length; i += 1) {
        retData[i] = this.scrubSensitiveInfo(retData[i]);
      }

      // return the scrubbed array
      return retData;
    }

    // if we are scrubbing an object (or string that has been parsed)
    if (typeof retData === 'object') {
      // go through each item in the object
      Object.keys(retData).forEach((key) => {
        // go deep through an object with recursive call
        if (typeof retData[key] === 'object') {
          retData[key] = this.scrubSensitiveInfo(retData[key]);
        } else {
          // go through sensitive word list - maybe can use find in
          for (let j = 0; j < sensList.length; j += 1) {
            if (key.toUpperCase() === sensList[j].toUpperCase()) {
              // if sensitive, mask
              retData[key] = '=** masked **';
              break;
            }
          }
        }
      });

      // return the scrubbed object
      return retData;
    }

    // if something we do not handle yet - just return the data
    return actualData;
  }

  /**
   * @summary Takes in properties and the secondary properties and merges them so the returned
   * object has secondary properties where no primary property values were provided.
   *
   * @function mergeProperties
   * @param {Object} properties - the primary propererties (required)
   * @param {Object} secondary - the secondary propererties (required)
   *
   * @return {Object} the properties with the merged in secondaries
   */
  mergeProperties(properties, secondary) {
    const origin = `${this.myid}-propertyUtil-mergeProperties`;
    log.trace(origin);

    // verify the input for the method
    if (!properties || typeof properties !== 'object') {
      return secondary;
    }
    if (!secondary || typeof secondary !== 'object') {
      return properties;
    }

    const combinedProps = secondary;
    const propKeys = Object.keys(properties);

    // loop through all of the primary properties to insert them into the conbined data
    for (let k = 0; k < propKeys.length; k += 1) {
      const thisProp = properties[propKeys[k]];

      // if this key is to an object
      if (thisProp && typeof thisProp === 'object' && combinedProps[propKeys[k]]) {
        // recursive call with primary and secondary object
        combinedProps[propKeys[k]] = this.mergeProperties(thisProp, combinedProps[propKeys[k]]);
      } else if ((thisProp !== undefined && thisProp !== null && thisProp !== '')
        || (combinedProps[propKeys[k]] === undefined || combinedProps[propKeys[k]] === null || combinedProps[propKeys[k]] === '')) {
        // if no secondary or primary has value merge it - overriding the secondary
        combinedProps[propKeys[k]] = thisProp;
      }
    }

    // return the merged properties
    return combinedProps;
  }

  /**
   * @summary Takes in property text and an encoding/encryption and returns the resulting
   * encoded/encrypted string
   *
   * @function encryptProperty
   * @param {String} property - the property to encrypt (required)
   * @param {String} technique - the technique to use to encrypt (required)
   *
   * @return {String} the encrypted/encoded string
   */
  encryptProperty(property, technique) {
    const origin = `${this.myid}-propertyUtil-encryptProperty`;
    log.trace(origin);

    try {
      // verify the input for the method
      if (!property) {
        return null;
      }
      if (!technique) {
        return property;
      }

      // if encoding, return the encoded string
      if (technique.toUpperCase() === 'BASE64') {
        return `{code}${Buffer.from(property).toString('base64')}`;
      }
      // if encrypting, return the encrypted string
      if (technique.toUpperCase() === 'ENCRYPT') {
        return `{crypt}${cryptoJS.AES.encrypt(property, this.myid)}`;
      }

      log.warn(`${origin}: Encyrpt technique ${technique} not supported!`);
      return property;
    } catch (e) {
      log.error(`${origin}: Encyrpt technique ${technique} took exception: ${e}`);
      return null;
    }
  }

  /**
   * @summary Takes in encrypted or encoded property and decodes/decrypts it to return
   * the actual property
   *
   * @function decryptProperty
   * @param {String} property - the property to decrypt
   *
   * @return {String} the string
   */
  decryptProperty(property) {
    const origin = `${this.myid}-propertyUtil-decryptProperty`;
    log.trace(origin);

    try {
      // verify the input for the method
      if (!property) {
        return null;
      }

      if (property.indexOf('{code}') === 0) {
        // remove the start
        const b64prop = property.substring(6);

        // decode the string
        return Buffer.from(b64prop, 'base64').toString();
      }

      if (property.indexOf('{crypt}') === 0) {
        // remove the start
        const cryptprop = property.substring(7);

        // decrypt the string
        return cryptoJS.AES.decrypt(cryptprop, this.myid).toString(cryptoJS.enc.Utf8);
      }

      log.warn(`${origin}: Invalid property ${property} - should start with {code} or {crypt}!`);
      return property;
    } catch (e) {
      log.error(`${origin}: Decrypt property took exception: ${e}`);
      return null;
    }
  }
}

module.exports = AdapterPropertyUtil;
