/**
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 logger = require('../../logger')('contrast:async-storage:hooks');
const { AsyncStorage } = require('../index');
const { Scopes } = require('../scopes');
const { ASYNC_CONTEXT } = require('../../../constants').PATCH_TYPES;
const requireHook = require('../../../hooks/require');
const patcher = require('../../../hooks/patcher');
const { bindFnArgAtIndex } = require('./utils');

module.exports = function init() {
  // done only to stub these fns for tests
  const { patchSequence, patchPool, patchQuery } = module.exports;

  // this callback _must return_ the patched function to set export
  requireHook.resolve(
    { name: 'mysql', file: 'lib/protocol/sequences/Sequence.js' },
    (Sequence) => patchSequence(Sequence)
  );

  // Note: This may not be necessary, though comments in source code re domains
  // suggest cls issues may be possible:
  // https://github.com/mysqljs/mysql/blob/master/lib/Pool.js#L231
  requireHook.resolve({ name: 'mysql', file: 'lib/Pool.js' }, (Pool) =>
    patchPool(Pool)
  );

  requireHook.resolve(
    { name: 'mysql2', file: 'lib/commands/query.js' },
    (Query) => patchQuery(Query)
  );
};

module.exports.patchSequence = patchSequence;
module.exports.sequencePostHook = sequencePostHook;
module.exports.patchPool = patchPool;
module.exports.poolPreHook = poolPreHook;
module.exports.patchQuery = patchQuery;
module.exports.queryPreHook = queryPreHook;
module.exports.runInAllowAllScope = runInAllowAllScope;

/**
 * Patches the Sequence constructor which the protocol classes inherit.
 * This _must return_ the patched value to set the export in require hook.
 * @param {function} sequenceCtor Sequence constructor fn
 * @returns {function}
 */
function patchSequence(sequenceCtor) {
  return patcher.patch(sequenceCtor, {
    name: 'mysql.lib/protocol/sequences/Sequence',
    patchType: ASYNC_CONTEXT,
    alwaysRun: true,
    post: sequencePostHook
  });
}

/**
 * Typically in a constructor the data.result would be the instance. But mysql
 * has the subclasses e.g. Query, do Sequence.call(this, cb). In this case the
 * data.obj is the instance.
 * @param {object} data.obj sequence instance
 */
function sequencePostHook({ obj, funcKey: identifier }) {
  // done only to stub this fn for tests
  const { runInAllowAllScope } = module.exports;
  try {
    if (obj && obj._callback && typeof obj._callback === 'function') {
      const cb = obj._callback;
      obj._callback = AsyncStorage.bind(runInAllowAllScope(cb, identifier));
    }
    AsyncStorage.getNamespace().bindEmitter(obj);
  } catch (err) {
    logger.warn('Unable to patch Sequence constructor: %o', err);
  }
}

// Created only for the purpose of testing
function runInAllowAllScope (cb, identifier) {
  return function (...args) {
    return Scopes.runInAllowAllScope(() => cb.call(this, ...args), identifier);
  };
}

/**
 * Patches Pool.prototype.getConnection.
 * @param {function} poolCtor Pool constructor fn
 */
function patchPool(poolCtor) {
  patcher.patch(poolCtor.prototype, 'getConnection', {
    name: 'mysql.lib.Pool',
    patchType: ASYNC_CONTEXT,
    alwaysRun: true,
    pre: poolPreHook
  });
}

/**
 * Binds callback (when present) to cls.
 * @param {object} data.args getConnection arguments
 */
function poolPreHook({ args, funcKey: identifier }) {
  try {
    if (args.length && typeof args[0] === 'function') {
      bindFnArgAtIndex({ args, idx: 0, identifier });
    }
  } catch (err) {
    logger.warn('Unable to patch Pool.prototype.getConnection: %o', err);
  }
}

/**
 * Patches the Query constructor.
 * This _must return_ the patched value to set the export in require hook.
 * @param {function} queryCtor Query constructor fn
 * @returns {function}
 */
function patchQuery(queryCtor) {
  return patcher.patch(queryCtor, {
    name: 'mysql2.lib/commands/query.js.Query',
    patchType: ASYNC_CONTEXT,
    alwaysRun: true,
    pre: queryPreHook
  });
}

/**
 * Binds callback (when present) to the context the constructor is called in..
 * @param {object} data the argument for the preHook
 * @param {object} data.args the arguments passed to the Query constructor
 * @param {object} data.funcKey Contrast funcKey identifier for a hooked Query function
 */
function queryPreHook({ args, funcKey: identifier }) {
  try {
    if (args.length && args[1] && typeof args[1] === 'function') {
      bindFnArgAtIndex({ args, idx: 1, identifier });
    }
  } catch (err) {
    logger.warn('Unable to patch Query constructor: %o', err);
  }
}
