/**
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 contrastCluster = require('cluster');
const findCacheDir = require('./util/find-cache-dir');
const _ = require('lodash');
const os = require('os');
const path = require('path');
const agentEmitter = require('./agent-emitter');
const FeatureSet = require('./feature-set');
const ExclusionFactory = require('./core/exclusions/exclusion-factory');
const {
  AGENT_INFO: { NAME, VERSION },
  FRAMEWORKS
} = require('./constants');

class ContrastAgent {
  constructor() {
    /**
     * Instance of AppInfo containing application information.
     * @type {import('./app-info')}
     */
    this.appInfo = null;

    /** @type {import('./telemetry') */
    this.telemetry = null;

    /**
     * Holds user settings for the agent
     * @member */
    this.config = null;
    /**
     * ... description not available
     * @member */
    this.command = null;
    /**
     * The currently enabled features.
     * @member */
    this.tsFeatureSet = new FeatureSet();
    /**
     * used in hooking, and figuring out if we are a parent cluster's agent
     * @member */
    this.cluster = contrastCluster;
    this.cluster.isPrimary = !contrastCluster.isWorker;
    /**
     * Process IDs of processes that have had their initial
     * worker online event swallowed
     */
    this.workerEmitPids = new Set();
    /**
     * Process IDs of processes that have had their initial
     * cluster online event swallowed
     */
    this.clusterEmitPids = new Set();
    /**
     * ... indicates if agent is started
     * via cli or preload(-r)
     * @member */
    this.isCli = false;
    /**
     * ... description not available
     * @member */
    this.hashCache = [];
    /**
     * ... description not available
     * @member */
    this.staticVisitors = [];
    /**
     * ... description not available
     * @member */
    this.intervals = {};
    /**
     * emitter for pre/post patcher events, assess response scanning events, internal assess
     * and protect events to report to TeamServer(or speedracer)
     * @member
     */
    this.agentEmitter = agentEmitter;
    /**
     * stores unique http frameworks we see loaded, which helps us
     * know which source/sink emitters to deploy.
     */
    this.frameworks = new Set();
    this.listenForFrameworks();
    /**
     * process.argv before we removed our stuff
     */
    this.argv = process.argv;
    this.tsFeatureSet._subscribers.push(this.agentEmitter);
    this.exclusions = new ExclusionFactory({
      featureSet: this.tsFeatureSet
    });
    this.agentEmitter.on('application-settings', (applicationSettings) => {
      this.exclusions.updateSettings({
        settings: applicationSettings,
        assess: this.isInAssessMode(),
        protect: this.isInDefendMode()
      });
    });
  }

  /**
   * Watch module loads to know which framework to load providers for.
   */
  listenForFrameworks() {
    this.agentEmitter.on('require', (moduleName) => {
      if (FRAMEWORKS.indexOf(moduleName) !== -1) {
        this.addFramework(moduleName);
      }
    });
  }

  /**
   * Whether the agent is in "screening" mode. This will affect how the agent
   * reports e.g. to message queue vs TS/SR reporting.
   */
  get isScreening() {
    if (this._isScreening === undefined) {
      this._isScreening = this.config.get('agent.node.dev.screener.enable');
    }
    return this._isScreening;
  }

  /**
   * Whether the agent's feature set has Assess mode enabled.
   *
   * @returns {Boolean}
   */
  isInAssessMode() {
    const conf = this.config.get('assess.enable');
    if (typeof conf === 'undefined') {
      return _.get(this.tsFeatureSet, 'serverFeatures.assess.enabled');
    }

    return conf;
  }

  /**
   * Whether the agent's feature set has Protect (Defend) mode enabled.
   *
   * @returns {Boolean}
   */
  isInDefendMode() {
    const conf = this.config.get('protect.enable');
    if (typeof conf === 'undefined') {
      return _.get(this.tsFeatureSet, 'serverFeatures.defend.enabled');
    }

    return conf;
  }

  /**
   * Whether the agent's feature set has route coverage enabled.
   *
   * @returns {Boolean}
   */
  isRouteCoverageEnabled() {
    return (
      this.config.get('agent.route_coverage.enable') && this.isInAssessMode()
    );
  }

  /**
   * Ends all of the agent's tasks that run on time intervals.
   * @memberof Agent
   */
  clearIntervals() {
    Object.keys(this.intervals).forEach(function(intervalName) {
      clearInterval(agent.intervals[intervalName]);
    });
  }

  /**
   * Registers framework in agent set.
   * @param {string} framework
   */
  addFramework(framework) {
    this.frameworks.add(framework);
  }
  getCacheDir() {
    if (this._cacheDir) return this._cacheDir;

    const mode = this.isInAssessMode() ? 'assess' : 'protect';
    const configuredPath = this.config.get('agent.node.rewrite_cache.path');
    const appName = this.appInfo.name;

    this._cacheDir = path.join(
      configuredPath
        ? path.resolve(configuredPath)
        : findCacheDir({ name: NAME }) || os.tmpdir(),
      appName.replace('/', '_'),
      VERSION,
      mode
    );

    return this._cacheDir;
  }
}

const agent = new ContrastAgent();
module.exports = agent;
module.exports.ContrastAgent = ContrastAgent;
