/**
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 Module = require('module');
const logger = require('../../core/logger')('contrast:hooks:module');
const helpers = require('./helpers');
let origCompile;
let origJs;

/**
 * Patches module._compile to perform rewriting when user code is compiled, and
 * Module._extensions['.js'] so that a previously rewritten file can be looked
 * up on subsequent runs.
 *
 * Calling `require` calls the following methods:
 * ```
 * require
 * ↳ Module.prototype.require
 * ↳ Module._load
 * ↳ Module.prototype.load
 * ↳ Module._extensions['.js']
 * ↳ Module.prototype._compile
 * ```
 *
 * `Module._extensions['.js']` is the step responsible for loading a JS file
 * from the file system, so by hooking that method we are able to load a
 * previously rewritten file when one is present. If not, we fall through to our
 * Module.prototype._compile hook, which preempts the compilation and execution
 * of the javascript file with our in-memory rewriter. The rewritten code is
 * then saved to a cache directory so it can be reused on the next run of the
 * application.
 *
 * @param {import('../../agent')} agent
 */
module.exports = function hookModule(agent) {
  const rewrite = require('../../core/rewrite')(agent);

  logger.debug('patching compilation methods');
  origCompile = Module.prototype._compile;
  Module.prototype._compile = function(content, filename) {
    const result = rewrite.rewriteFile(content, filename);
    let compiled;
    try {
      compiled = origCompile.apply(this, [
        result.code ? result.code : result,
        filename
      ]);

      helpers.cacheWithSourceMap(agent, filename, result);
    } catch (err) {
      // istanbul ignore else; we test this but istanbul is not displaying it correctly
      if (err instanceof SyntaxError) {
        logger.error(
          'Failed to compile rewritten code for %s, err: %o, rewritten code %s, compiling original code.',
          filename,
          err,
          result
        );
        compiled = origCompile.apply(this, [content, filename]);
      } else {
        throw err;
      }
    }

    return compiled;
  };

  origJs = Module._extensions['.js'];
  Module._extensions['.js'] = function(module, filename) {
    const cached = helpers.find(agent, filename);

    // If cached, short circuit the _extensions method and go straight to compile.
    return cached
      ? origCompile.apply(module, [cached, filename])
      : origJs.apply(this, [module, filename]);
  };
};

/**
 * Restores patched methods to their original implementations. Utilized in tests
 * to avoid side effects.
 */
module.exports.restore = function restoreModule() {
  Module.prototype._compile = origCompile;
  Module._extensions['.js'] = origJs;
};
