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

/**
 * Houses models for AppActivity messages
 * @module
 */

const debug = require('../core/logger')('contrast:app:activityqueue');
const technologies = require('../assess/technologies');
const AppActivity = require('./models/app-activity/app-activity');
const Scopes = require('../core/async-storage/scopes');
const agentEmitter = require('../agent-emitter');
const { AsyncStorage, KEYS } = require('../core/async-storage');

const DEADZONE_NAME = 'AppActivityQueue';

/**
 * @memberof module:appActivityQueue
 */
class AppActivityQueue {
  constructor(settings) {
    this.settings = mapSettings(settings);
    this.activity = new AppActivity();
    this.libUsages = new Map();
    const now = Date.now();
    this.lastReport = now; // the last time a report was sent to TS
    this.lastUpdate = now; // the last time TS responded with a changed feature set
    const inventoryEnabled =
      settings && settings.inventory && settings.inventory.enable;
    this.architectures = new Map();

    agentEmitter.on('require', function(name) {
      if (technologies.indexOf(name) > -1) {
        Scopes.runInNoInstrumentationScope(() => {
          handleTechs(technologies, name, inventoryEnabled);
        }, DEADZONE_NAME);
      }
    });
  }

  updateInventory(data) {
    this.activity.inventory.addData(data);
  }

  updateArchitectures(arch) {
    const archName = arch.vendor + arch.url + arch.remotePort + arch.remoteHost;
    if (!this.architectures.has(archName)) {
      this.architectures.set(archName, arch);
    }
  }

  /**
   * Add a libUsage event to the queue
   * @param {Object} libUsage event
   */
  updateLibUsage(libUsage) {
    if (!this.libUsages.has(libUsage.hash)) {
      this.libUsages.set(libUsage.hash, libUsage);
    } else {
      this.libUsages.get(libUsage.hash).files.push(libUsage.files[0]);
    }
  }

  /**
   * Get the current libUsage events from the queue and clear the queue
   *
   * @returns {Array} an Array of libUsage events
   */
  getLibUsage() {
    const rv = Array.from(this.libUsages.values());
    this.libUsages = new Map();
    return rv;
  }

  getArchitectures() {
    const rv = Array.from(this.architectures.values());
    this.architectures = new Map();
    return rv;
  }

  updateDefendInfo(finding) {
    const id = finding.ruleId;
    const limit = this.settings.protect.samples[finding.status];
    const count = this.activity.defend.attackCounts[finding.status];

    // Return if current count meets or exceeds configured limit.
    if (limit <= count) {
      debug.info(`attack report thwarted by limiter: ${id}`);
      return;
    }

    debug.info(`${id}: Adding RASP attack activity`);
    this.activity.defend.addAttack(finding);

    // store on the async context.
    const defend = AsyncStorage.get(KEYS.DEFEND);
    if (defend) {
      defend.addAttack(finding);
    }
  }

  get() {
    const queue = this.activity.toSerializable();
    this.lastReport = Date.now();
    queue.lastUpdate = this.lastReport - this.lastUpdate;
    this.activity = new AppActivity();
    return queue;
  }

  // Reset the last update timer
  notify() {
    this.lastUpdate = Date.now();
  }
}

/**
 * Various settings mapping/renaming for consistency in naming in agent code etc.
 * Note: This does not modifiy the <code>config</code> parameter.
 * @param {Object} config The agent configuration
 * @returns {Object}
 */
function mapSettings(config) {
  if (!config) {
    return {};
  }

  const settings = JSON.parse(JSON.stringify(config)); // Deep clone
  const defaultSampleSize = Infinity;

  if (!settings.protect) settings.protect = {};
  if (!settings.protect.samples) settings.protect.samples = {};

  const names = [
    ['effective'],
    ['blocked'],
    ['probed'],
    ['blocked_at_perimeter', 'blockedAtPerimeter']
  ];

  names.forEach(([oldPath, newPath]) => {
    if (newPath) {
      settings.protect.samples[newPath] =
        settings.protect.samples[oldPath] || defaultSampleSize;
    }
  });

  delete settings.protect.samples.blocked_at_perimeter;

  return settings;
}

/**
 * Handles updating techs list when modules are required
 * @param {Array} technologies
 * @param {string} name
 */
function handleTechs(technologies, name, enabled) {
  const update = { techs: name };
  if (enabled) {
    agentEmitter.emit('inventory', update);
  }
}

module.exports = AppActivityQueue;
module.exports.mapSettings = mapSettings;
