/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';
const patcher = require('./patcher');
const { PATCH_TYPES } = require('../constants');
const logger = require('../core/logger')('contrast:hooks:fn-to-string');

const IS_CLASS_REGEX = /^class\W/;
const IS_FUNCTION_REGEX = /^(async\s+)?function\W/;
const IS_RETURN_REGEX = /^return\W/;
const METHOD_CONTEXT = 'function _contrast_';
const FUNCTION_CONTEXT = 'const _contrast_fn = ';

/**
 * Create some code context around a function to make it syntactically valid
 * the three general types of syntactic function definitions are:
 *   - functions (declared with the function keyword)
 *   - arrow functions
 *   - class methods
 * @param   {string} code the user code
 * @returns {string}      the contextualized function
 */
const contextualizeFunction = (code) => {
  // Classes themselves are functions, so we need to exit early to avoid
  // clobbering the `class` keyword.
  if (IS_CLASS_REGEX.exec(code)) return code;
  const [orig] = code.split('{');

  let lineOne = orig;

  // if the function is a return, we don't need to add context.
  if (!IS_RETURN_REGEX.exec(lineOne)) {
    // When stringified, class methods look like normal function without the
    // `function` keyword. We can normalize this by prepending `function`.
    if (!IS_FUNCTION_REGEX.exec(lineOne) && lineOne.indexOf('=>') === -1) {
      lineOne = `${METHOD_CONTEXT}${lineOne}`;
    }
    lineOne = `${FUNCTION_CONTEXT}${lineOne}`;

    code = code.replace(orig, lineOne);
  }

  return code;
};

/**
 * hooks the Function.prototype.toString method
 * undo our rewritten code so users won't see it
 */
module.exports = function hookFunctionToString(agent) {
  const rewrite = require('../core/rewrite')(agent);

  logger.debug('deploying function toString patch');

  // function --> unwritten toString
  const results = new WeakMap();

  patcher.patch(Function.prototype, 'toString', {
    name: 'Function',
    patchType: PATCH_TYPES.MISC,
    alwaysRun: true,
    pre(data) {
      if (results.has(data.obj)) {
        data.result = patcher.preempt(results.get(data.obj));
      }
    },
    post(data) {
      if (results.has(data.obj)) {
        data.result = results.get(data.obj);
        return;
      }
      let code = data.result;
      // only fix the code if it has something we replaced
      if (code.indexOf('ContrastMethods') !== -1) {
        // cannot parse lone function expressions that are not part of an assignment.
        try {
          code = contextualizeFunction(code);
          code = rewrite.unwrite(code);
          code = code.replace(METHOD_CONTEXT, '');
          code = code.replace(FUNCTION_CONTEXT, '');
          code = code.replace(/;$/, ''); // removes trailing semicolon
        } catch (err) {
          logger.warn('Failed to unwrite code: %s, at: %o', data.result, err);
          code = data.result;
        }
      }

      results.set(data.obj, code);
      data.result = code;
    }
  });
};

module.exports.contextualizeFunction = contextualizeFunction;
