#!/usr/bin/env node
/**
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 { program } = require('commander');
const fs = require('fs');
const readline = require('readline');

const LOG_REGEX = /req-perf-logger - /;

/*
 * transform raw data to more consumable values
 * (nano->ms, calculate percentages)
 */
function transform(data) {
  // lame but make a new object so they get logged in insertion order
  const newData = { path: data.path, method: data.method };
  newData.instrumentationTimeMs = data.instrumentationTime / 10 ** 6;
  newData.totalTimeMs = data.totalTime / 10 ** 6;
  newData.instrumentationOverheadPerc =
    (data.instrumentationTime / data.totalTime) * 100;

  newData.events = [];
  for (let i = 0; i < data.events.length; i++) {
    newData.events[i] = { name: data.events[i].name, times: {} };
    newData.events[i].times.preMs = data.events[i].times.pre / 10 ** 6;
    newData.events[i].times.postMs = data.events[i].times.post / 10 ** 6;
    newData.events[i].times.origMs = data.events[i].times.orig / 10 ** 6;
    newData.events[i].times.preOverheadPerc =
      (data.events[i].times.pre / data.events[i].times.orig) * 100;
    newData.events[i].times.postOverheadPerc =
      (data.events[i].times.post / data.events[i].times.orig) * 100;
    newData.events[i].times.totalOverheadPerc =
      ((data.events[i].times.pre + data.events[i].times.post) /
        data.events[i].times.orig) *
      100;
  }
  return newData;
}

/*
 * merge an entry with already accumulated data
 */
function merge(accum, data) {
  accum.count = accum.count + 1;
  accum.instrumentationTimeMs += data.instrumentationTimeMs;
  accum.totalTimeMs += data.totalTimeMs;
  accum.instrumentationOverheadPerc =
    (accum.instrumentationTimeMs / accum.totalTimeMs) * 100;

  const newDataEvents = data.events.reduce((accum, cur) => {
    accum[cur.name] = cur;
    return accum;
  }, {});

  for (let i = 0; i < accum.events.length; i++) {
    const { name } = accum.events[i];
    if (!newDataEvents[name]) {
      continue;
    }
    accum.events[i].times.preMs += newDataEvents[name].times.preMs;
    accum.events[i].times.postMs += newDataEvents[name].times.postMs;
    accum.events[i].times.origMs += newDataEvents[name].times.origMs;
    accum.events[i].times.preOverheadPerc =
      (accum.events[i].times.preMs / accum.events[i].times.origMs) * 100;
    accum.events[i].times.postOverheadPerc =
      (accum.events[i].times.postMs / accum.events[i].times.origMs) * 100;
    accum.events[i].times.totalOverheadPerc =
      ((accum.events[i].times.preMs + accum.events[i].times.postMs) /
        accum.events[i].times.origMs) *
      100;
    delete newDataEvents[name];
  }

  // append any remaining events from the new data
  Array.prototype.push.apply(accum.events, Object.values(newDataEvents));
  return accum;
}

async function processFile(filename, filter, group) {
  const rl = readline.createInterface({
    input: fs.createReadStream(filename),
    crlfDelay: Infinity
  });

  const merged = {};
  for await (const line of rl) {
    if (line.match(LOG_REGEX)) {
      const data = transform(JSON.parse(line.split(LOG_REGEX)[1]));
      if (filter && !data.path.match(filter)) {
        continue;
      }
      if (!group) {
        console.log(JSON.stringify(data, null, 4));
      } else {
        const key = filter
          ? `regex:${data.method}`
          : `${data.path}:${data.method}`;
        if (filter) {
          // path is meaningless if we're merging w/ a filter
          delete data.path;
        }
        if (!merged[key]) {
          merged[key] = data;
          merged[key].count = 1;
        } else {
          merged[key] = merge(merged[key], data);
        }
      }
    }
  }

  if (group) {
    console.log(JSON.stringify(Object.values(merged), null, 4));
  }
}

program
  .arguments('<log_file>')
  .option(
    '-g, --group',
    'group like requests together (defaults by req path, but uses filter regex if specified)'
  )
  .option('-f, --filter <regex>', 'filter (and group if specified) by regex');

program.parse(process.argv);
if (program.args.length === 0) {
  program.help();
}

let filterRegex;
if (program.filter) {
  try {
    filterRegex = new RegExp(program.filter);
  } catch (e) {
    console.error('invalid filter regex');
    process.exit(-1); // eslint-disable-line no-process-exit

  }
}

processFile(program.args[0], filterRegex, program.group);
