/**
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 { default: axios } = require('axios');
const { createHash } = require('crypto');
const findCacheDir = require('./util/find-cache-dir');
const { promises: fs } = require('fs');
const { default: getMac } = require('getmac');
const os = require('os');
const path = require('path');
const process = require('process');
const { v4: uuid } = require('uuid');
const { AGENT_INFO } = require('./constants');
const logger = require('./core/logger')('contrast:telemetry');

const TELEMETRY_URL = 'https://telemetry.nodejs.contrastsecurity.com/';
const TELEMETRY_VERSION = 'v0'; // TODO: set this to v1 when format is established.

const {
  CONTRAST_AGENT_TELEMETRY_OPTOUT,
  CONTRAST_DEV,
  CONTRAST_SCREENER
} = process.env;

// TODO: LINK
const DISCLAIMER = `The Contrast Node Agent collects usage data in order to help us improve compatibility and security coverage.
The data is anonymous and does not contain application data. It is collected by Contrast and is never shared.
You can opt-out of telemetry by setting the CONTRAST_AGENT_TELEMETRY_OPTOUT environment variable to '1' or 'true'.
`;
// Read more about the Node Agent's telemetry: TODO
// `;

const LOG_MESSAGE = `Telemetry is now active.
You can opt-out of telemetry by setting the CONTRAST_AGENT_TELEMETRY_OPTOUT environment variable to '1' or 'true'.
`;

module.exports = class Telemetry {
  /**
   * @param {import('./agent')} agent
   * @param {any} config
   */
  constructor(agent, config) {
    const { appInfo } = agent;
    this.appInfo = appInfo;
    this.config = config;
    this.disabled =
      CONTRAST_AGENT_TELEMETRY_OPTOUT === 'true' ||
      CONTRAST_AGENT_TELEMETRY_OPTOUT === '1';

    if (this.disabled) {
      logger.info(
        'Telemetry opt-out in effect. All usage data collection is suppressed.'
      );
      return;
    }
    this.printDisclaimer();

    try {
      // Returns the "first" valid MAC address for the machine, throwing if none
      // is found.
      const mac = getMac();

      const hash = createHash('sha256').update(mac);
      if (appInfo.containerId) {
        hash.update(appInfo.containerId);
      }
      this.instanceId = hash.copy().digest('hex');
      this.applicationId = hash.update(appInfo.name).digest('hex');
    } catch (err) {
      // If getMac fails we fall back to generating a UUID. "Unstable"
      // identifiers such as these are prefixed with an underscore.
      let id = uuid();
      if (appInfo.containerId) {
        id += `_${appInfo.containerId}`;
      }

      this.instanceId = `_${id}`;
      this.applicationId = this.instanceId;
    }

    this.metrics = this.createMetricsClient();
    this.tags = {
      isCustomerEnv: this.isCustomerEnv(),
      applicationId: this.applicationId,
      osArch: appInfo.os.architecture,
      osPlatform: appInfo.os.platform,
      osRelease: appInfo.os.release,
      isContainer: !!appInfo.containerId,
      agent: AGENT_INFO.NAME,
      agentVersion: AGENT_INFO.VERSION,
      isAssess: agent.isInAssessMode(),
      isProtect: agent.isInDefendMode(),
      nodeVersion: appInfo.nodeVersion,
      nodeVersionMajor: appInfo.nodeVersionMajor,
      telemetryVersion: TELEMETRY_VERSION
    };

    this.sendStartupData();
  }

  async printDisclaimer() {
    const filePath = path.resolve(
      findCacheDir({ name: AGENT_INFO.NAME }) || os.tmpdir(),
      '.telemetry-disclaimer'
    );

    try {
      await fs.stat(filePath);
      // If we've previously shown our disclaimer we log the shorter message.
      logger.info(LOG_MESSAGE);
    } catch (err) {
      // When there's no file `stat` throws an error. That means we haven't
      // yet shown our disclaimer.
      console.log(DISCLAIMER);
      logger.info(DISCLAIMER);
      try {
        await fs.writeFile(filePath, DISCLAIMER);
      } catch (err) {
        // if we can't write, oh well.
      }
    }
  }

  /**
   * @returns {import('axios').AxiosInstance}
   */
  createMetricsClient() {
    const userAgent = `${AGENT_INFO.NAME}/${AGENT_INFO.VERSION}`;

    return axios.create({
      baseURL: new URL('/api/v1/telemetry/metrics/node', TELEMETRY_URL).href,
      headers: {
        'User-Agent': userAgent
      }
    });
  }

  /**
   * Logic to determine whether we are reporting a customer's application.
   * Naive for the time being.
   * @returns {boolean}
   */
  isCustomerEnv() {
    if (CONTRAST_SCREENER || CONTRAST_DEV) return false;
    return true;
  }

  /**
   * @param {string} path
   * @param {{ [field: string]: number ]}} fields
   */
  async sendMetrics(path, fields) {
    return this.metrics.post(path, [
      {
        timestamp: new Date().toISOString(),
        instance: this.instanceId,
        tags: this.tags,
        fields
      }
    ]);
  }

  async sendStartupData() {
    try {
      // snapshotted when we send our startup data.
      const memoryUsage = process.memoryUsage();

      await this.sendMetrics('startup', {
        numCpus: os.cpus().length,
        memTotal: os.totalmem(),
        memHeapTotal: memoryUsage.heapTotal,
        memHeapUsed: memoryUsage.heapUsed,
        memExternal: memoryUsage.external,
        memRss: memoryUsage.rss,
        uptime: process.uptime()
      });
    } catch (err) {
      logger.warn('telemetry update failed: %s', err.toJSON());
    }
  }
};
