/* @copyright Itential, LLC 2023 */

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

const aws4 = require('aws4');
const AWS = require('aws-sdk');
const querystring = require('querystring');

/*
 * INTERNAL FUNCTION: update device broker properties from service instance config and adapter sample properties
*/

class AuthenticationHandler {
  /**
   * Adapter Authentication Handler
   * @constructor
   */
  constructor(prongId, properties, reqH) {
    this.myid = prongId;
    this.allProps = properties;
    this.requestHandlerInst = reqH;

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

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

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

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

    try {
      // Form path
      let uriPathTranslated = '';
      if (requestObj.uriQuery && uriPath.indexOf('?') === -1) {
        const query = requestObj.uriQuery;
        uriPathTranslated = `${uriPath}?${querystring.stringify(query)}`;
      } else if (requestObj.uriQuery && uriPath.endsWith('?')) {
        const query = requestObj.uriQuery;
        uriPathTranslated = `${uriPath}${querystring.stringify(query)}`;
      } else {
        uriPathTranslated = uriPath;
      }

      // set up the options for the AWS signature
      const options = {
        host: this.allProps.host,
        method,
        path: uriPathTranslated.replace(/\/\/+/g, '/'),
        service,
        region: this.allProps.region
      };

      // add any provided headers for the call
      if (requestObj.addlHeaders) {
        options.headers = requestObj.addlHeaders;
      }
      // remove ? if there is no query and the last character is ?
      if (options.path.endsWith('?')) {
        options.path = options.path.substring(0, options.path.length - 1);
      }
      // If there is a body (POST, PATCH, PUT) add the body to the call
      if (method !== 'GET' && method !== 'DELETE') {
        if (typeof requestObj.payload === 'string') {
          options.body = requestObj.payload;
        } else {
          options.body = JSON.stringify(requestObj.payload);
        }
      }
      // override anything set in callProperties
      if (requestObj.callProperties) {
        if (requestObj.callProperties.host) {
          options.host = requestObj.callProperties.host;
        }
        if (requestObj.callProperties.region) {
          options.region = requestObj.callProperties.region;
        }
      }
      log.debug(`SIG OPTIONS: ${JSON.stringify(options)}`);

      /* STS AUTHENTICATION */
      if (STSParams) {
        log.info('Using STS for AWS Authentication');

        // set the original AWS access information (from properties)
        AWS.config.update({
          sessionToken: this.allProps.authentication.aws_session_token,
          accessKeyId: this.allProps.authentication.aws_access_key,
          secretAccessKey: this.allProps.authentication.aws_secret_key,
          region: this.allProps.region
        });

        // use STS to get the AWS access information for the user defined in STWS Params
        const sts = new AWS.STS();
        const stsData = {
          RoleArn: STSParams.RoleArn,
          RoleSessionName: STSParams.RoleSessionName,
          DurationSeconds: 3600
        };
        return sts.assumeRole(stsData, (err, data) => {
          if (err) {
            const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, `AWS Assume Role Error ${err}`, null, null, null, null);
            log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
            return callback(null, errorObj);
          }
          // extract the user specific info from the response
          const accessKeyId = data.Credentials.AccessKeyId;
          const secretAccessKey = data.Credentials.SecretAccessKey;
          const sessionToken = data.Credentials.SessionToken;

          // call the signature with the user specific information
          const authOpts = aws4.sign(options, { accessKeyId, secretAccessKey, sessionToken });
          if (sessionToken) {
            authOpts.headers['X-Amz-Security-Token'] = sessionToken;
          }

          // return the headers
          return callback(authOpts.headers);
        });
      }

      /* ADAPTER PROPERTIES AUTHENTICATION */
      if (!roleName && !this.allProps.authentication.aws_iam_role) {
        log.info('Using Adapter PROPERTIES for AWS Authentication');

        // call the signature with the property information
        const authOpts = aws4.sign(options, { accessKeyId: this.allProps.authentication.aws_access_key, secretAccessKey: this.allProps.authentication.aws_secret_key, sessionToken: this.allProps.authentication.aws_session_token });
        if (this.allProps.authentication.aws_session_token) {
          authOpts.headers['X-Amz-Security-Token'] = this.allProps.authentication.aws_session_token;
        }

        // return the headers
        return callback(authOpts.headers);
      }

      /* ROLE NAME AUTHENTICATION */
      log.info('Using roleName for AWS Authentication');

      // determine where to get roleName (task is higher priority)
      let myRole = roleName;
      if (!roleName) {
        myRole = this.allProps.authentication.aws_iam_role;
      }
      const myDate = new Date().getTime();
      const mySess = `${this.myid}-${myDate}`;
      const sts = new AWS.STS();
      const stsData = {
        RoleArn: myRole,
        RoleSessionName: mySess,
        DurationSeconds: 3600
      };

      // change role to the role name provided
      return sts.assumeRole(stsData, (err, data) => {
        if (err) {
          const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, `AWS Assume Role Error ${err}`, null, null, null, null);
          log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
          return callback(null, errorObj);
        }

        // get role keys from response so we can sign the request
        const accessKeyId = data.Credentials.AccessKeyId;
        const secretAccessKey = data.Credentials.SecretAccessKey;
        const sessionToken = data.Credentials.SessionToken;

        // sign the request
        const authOpts = aws4.sign(options, { accessKeyId, secretAccessKey, sessionToken });
        if (sessionToken) {
          authOpts.headers['X-Amz-Security-Token'] = sessionToken;
        }

        // return the headers
        return callback(authOpts.headers);
      });
    } catch (e) {
      const errorObj = this.requestHandlerInst.formatErrorObject(this.myid, meth, 'Caught Exception', null, null, null, e);
      log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
      return callback(null, errorObj);
    }
  }
}

module.exports = AuthenticationHandler;
