/*
 * (c) Copyright IBM Corp. 2021
 * (c) Copyright Instana Inc. and contributors 2020
 */

'use strict';

const constants = require('../constants');
const leftPad = require('../leftPad');
const tracingUtil = require('../tracingUtil');

const VERSION00 = '00';
const LEFT_PAD_16 = '0000000000000000';

// See https://www.w3.org/TR/trace-context-2/#trace-flags for details on the bitmasks.
const SAMPLED_BITMASK = 0b1;
const RANDOM_TRACE_ID_BITMASK = 0b10;

function W3cTraceContext() {
  // whether the traceparent header is valid
  this.traceParentValid = false;
  // the trace context specification version from the traceparent header
  this.version = undefined;
  // the trace id from the traceparent header
  this.traceParentTraceId = undefined;
  // the parent id from the traceparent header
  this.traceParentParentId = undefined;
  // the sampled flag from the traceparent header
  this.sampled = undefined;
  // the random trace ID flag from the traceparent header
  this.randomTraceId = undefined;

  // whether the tracestate header is valid
  this.traceStateValid = false;
  // the non-Instana key-value pairs that come before the in key-value pair
  this.traceStateHead = undefined;
  // the trace ID from the in key-value pair from the tracestate header
  this.instanaTraceId = undefined;
  // the parent ID from the in key-value pair from the tracestate header
  this.instanaParentId = undefined;
  // the non-Instana key-value pairs that come after the in key-value pair
  this.traceStateTail = undefined;
}

/**
 * @param {string} instanaTraceId
 * @param {string} instanaParentId
 * @param {boolean | *} sampled
 * @returns {W3cTraceContext}
 */
W3cTraceContext.fromInstanaIds = function fromInstanaIds(instanaTraceId, instanaParentId, sampled) {
  const paddedTraceId = instanaTraceId.length === 16 ? LEFT_PAD_16 + instanaTraceId : instanaTraceId;
  sampled = typeof sampled === 'boolean' ? sampled : true;
  const traceContext = new W3cTraceContext();
  traceContext.traceParentValid = true;
  traceContext.version = VERSION00;
  traceContext.traceParentTraceId = paddedTraceId;
  traceContext.traceParentParentId = instanaParentId;
  traceContext.sampled = sampled;
  // Instana tracers always generate trace IDs randomly, so we can confidently set this flag.
  traceContext.randomTraceId = true;

  traceContext.traceStateValid = true;
  traceContext.instanaTraceId = instanaTraceId;
  traceContext.instanaParentId = instanaParentId;

  return traceContext;
};

/**
 * @param {string} otelTraceId
 * @param {string} otelParentId
 * @param {boolean | *} sampled
 * @returns {W3cTraceContext}
 */
W3cTraceContext.fromOtelIds = function fromOtelIds(otelTraceId, otelParentId, sampled) {
  const traceContext = new W3cTraceContext();
  traceContext.traceParentValid = true;
  traceContext.version = VERSION00;
  traceContext.traceParentTraceId = otelTraceId;
  traceContext.traceParentParentId = otelParentId;
  traceContext.sampled = sampled;
  return traceContext;
};

/**
 * @param {string} traceId
 * @param {string} parentId
 * @returns
 */
W3cTraceContext.createEmptyUnsampled = function createEmptyUnsampled(traceId, parentId) {
  const paddedTraceId = traceId.length === 16 ? LEFT_PAD_16 + traceId : traceId;
  const traceContext = new W3cTraceContext();
  traceContext.traceParentValid = true;
  traceContext.version = VERSION00;
  traceContext.traceParentTraceId = paddedTraceId;
  traceContext.traceParentParentId = parentId;
  traceContext.sampled = false;
  // We always generate a new trace IDs randomly for this case, so we can confidently set this flag.
  traceContext.randomTraceId = true;
  traceContext.traceStateValid = true;
  return traceContext;
};

/**
 * @returns {string}
 */
W3cTraceContext.prototype.renderTraceParent = function renderTraceParent() {
  if (!this.traceParentValid) {
    return '';
  }
  // Since we only support version 00, we must always downgrade to 00 if we have updated any value.
  return `00-${this.traceParentTraceId}-${this.traceParentParentId}-${this.renderFlags()}`;
};

/**
 * @returns {string}
 */
W3cTraceContext.prototype.renderFlags = function renderFlags() {
  let flagsInt = 0;
  if (this.sampled) {
    // eslint-disable-next-line no-bitwise
    flagsInt |= SAMPLED_BITMASK;
  }
  if (this.randomTraceId) {
    // eslint-disable-next-line no-bitwise
    flagsInt |= RANDOM_TRACE_ID_BITMASK;
  }
  return leftPad(flagsInt.toString(16), 2);
};

/**
 * @returns {boolean}
 */
W3cTraceContext.prototype.hasTraceState = function hasTraceState() {
  return ((this.instanaTraceId && this.instanaParentId) || this.traceStateHead || this.traceStateTail) != null;
};

/**
 * @returns {string}
 */
W3cTraceContext.prototype.renderTraceState = function renderTraceState() {
  if (!this.traceStateValid) {
    return '';
  }
  /** @type {Array.<*>} */
  let allKeyValuePairs = [];
  const instanaKeyValuePair = this.renderInstanaTraceStateValue();
  if (this.traceStateHead) {
    allKeyValuePairs = allKeyValuePairs.concat(this.traceStateHead);
  }
  if (instanaKeyValuePair) {
    allKeyValuePairs.push(instanaKeyValuePair);
  }
  if (this.traceStateTail) {
    allKeyValuePairs = allKeyValuePairs.concat(this.traceStateTail);
  }
  return allKeyValuePairs.join(',');
};

W3cTraceContext.prototype.renderInstanaTraceStateValue = function renderInstanaTraceStateValue() {
  if (this.instanaTraceId && this.instanaParentId) {
    return `${constants.w3cInstanaEquals + this.instanaTraceId};${this.instanaParentId}`;
  } else {
    return null;
  }
};

W3cTraceContext.prototype.resetTraceState = function resetTraceState() {
  this.traceStateValid = true;
  this.traceStateHead = null;
  this.instanaTraceId = null;
  this.instanaParentId = null;
  this.traceStateTail = null;
};

/**
 * Modifies this trace context object:
 * - updates the foreing parent ID in traceparent to the given given value (not that we do not set the
 *   foreign trace ID),
 * - sets the sampled flag in traceparent to true,
 * - upserts the in key-value pair in tracestate to the given trace ID and span ID, and moved to the leftmost position.
 * @param {string} instanaTraceId
 * @param {string} instanaParentId
 */
W3cTraceContext.prototype.updateParent = function updateParent(instanaTraceId, instanaParentId) {
  this.instanaTraceId = instanaTraceId;
  this.instanaParentId = instanaParentId;
  this.traceParentParentId = instanaParentId;
  if (this.traceStateHead && this.traceStateTail) {
    this.traceStateTail = this.traceStateHead.concat(this.traceStateTail);
  } else if (this.traceStateHead) {
    this.traceStateTail = this.traceStateHead;
  }
  // ^ If only this.traceStateTail has content, we do not need to update this.traceStateHead nor this.traceStateTail;
  // we only need to make sure all foreign key-value pairs come to the right of the in key-value pair, which they
  // do, if they are only in traceStateTail.

  // Remove everything from traceStateHead to move the in key-value pair to the leftmost position.
  this.traceStateHead = null;
  this.sampled = true;
};

/**
 * @param {string} longTraceId
 */
W3cTraceContext.prototype.restartTrace = function restartTrace(longTraceId) {
  this.traceParentValid = true;
  this.version = VERSION00;
  this.instanaTraceId = longTraceId ? tracingUtil.generateRandomLongTraceId() : tracingUtil.generateRandomTraceId();

  this.traceParentTraceId = longTraceId ? this.instanaTraceId : LEFT_PAD_16 + this.instanaTraceId;
  this.traceParentParentId = this.instanaParentId = tracingUtil.generateRandomSpanId();
  this.sampled = true;

  this.traceStateValid = true;
  this.traceStateHead = null;
  this.traceStateTail = null;
};

W3cTraceContext.prototype.disableSampling = function disableSampling() {
  if (this.sampled) {
    // See https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field
    // "The parent-id field MUST be set to a new value with the sampled flag update."
    this.traceParentParentId = tracingUtil.generateRandomSpanId();
  }
  this.sampled = false;
};

W3cTraceContext.prototype.clone = function clone() {
  return Object.assign(new W3cTraceContext(), this);
};

W3cTraceContext.prototype.getMostRecentForeignTraceStateMember = function getMostRecentForeignTraceStateMember() {
  const traceStateToInspect = this.traceStateHead ? this.traceStateHead : this.traceStateTail;
  if (!traceStateToInspect || traceStateToInspect.length === 0) {
    return undefined;
  }
  return traceStateToInspect[0];
};

W3cTraceContext.VERSION00 = VERSION00;
W3cTraceContext.SAMPLED_BITMASK = SAMPLED_BITMASK;
W3cTraceContext.RANDOM_TRACE_ID_BITMASK = RANDOM_TRACE_ID_BITMASK;

module.exports = W3cTraceContext;
