/* @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');

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

  // GENERIC UTILITY CALLS FOR PROPERTIES
  /**
   * @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) {
    const origin = `${this.myid}-propertyUtil-getEntitySchema`;
    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}`);

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

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

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