import { getGlobalSingleton, timestampInSeconds, logger } from '@sentry/utils';
import { getClient } from '../currentScopes.js';
import { DEBUG_BUILD } from '../debug-build.js';
import '../tracing/errors.js';
import { getActiveSpan, getRootSpan, spanToJSON } from '../utils/spanUtils.js';
import { startSpanManual } from '../tracing/trace.js';
import { handleCallbackErrors } from '../utils/handleCallbackErrors.js';
import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, SET_METRIC_TYPE, GAUGE_METRIC_TYPE } from './constants.js';

/**
 * Gets the metrics aggregator for a given client.
 * @param client The client for which to get the metrics aggregator.
 * @param Aggregator Optional metrics aggregator class to use to create an aggregator if one does not exist.
 */
function getMetricsAggregatorForClient(
  client,
  Aggregator,
) {
  const globalMetricsAggregators = getGlobalSingleton(
    'globalMetricsAggregators',
    () => new WeakMap(),
  );

  const aggregator = globalMetricsAggregators.get(client);
  if (aggregator) {
    return aggregator;
  }

  const newAggregator = new Aggregator(client);
  client.on('flush', () => newAggregator.flush());
  client.on('close', () => newAggregator.close());
  globalMetricsAggregators.set(client, newAggregator);

  return newAggregator;
}

function addToMetricsAggregator(
  Aggregator,
  metricType,
  name,
  value,
  data = {},
) {
  const client = data.client || getClient();

  if (!client) {
    return;
  }

  const span = getActiveSpan();
  const rootSpan = span ? getRootSpan(span) : undefined;
  const transactionName = rootSpan && spanToJSON(rootSpan).description;

  const { unit, tags, timestamp } = data;
  const { release, environment } = client.getOptions();
  const metricTags = {};
  if (release) {
    metricTags.release = release;
  }
  if (environment) {
    metricTags.environment = environment;
  }
  if (transactionName) {
    metricTags.transaction = transactionName;
  }

  DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`);

  const aggregator = getMetricsAggregatorForClient(client, Aggregator);
  aggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp);
}

/**
 * Adds a value to a counter metric
 *
 * @experimental This API is experimental and might have breaking changes in the future.
 */
function increment(aggregator, name, value = 1, data) {
  addToMetricsAggregator(aggregator, COUNTER_METRIC_TYPE, name, ensureNumber(value), data);
}

/**
 * Adds a value to a distribution metric
 *
 * @experimental This API is experimental and might have breaking changes in the future.
 */
function distribution(aggregator, name, value, data) {
  addToMetricsAggregator(aggregator, DISTRIBUTION_METRIC_TYPE, name, ensureNumber(value), data);
}

/**
 * Adds a timing metric.
 * The metric is added as a distribution metric.
 *
 * You can either directly capture a numeric `value`, or wrap a callback function in `timing`.
 * In the latter case, the duration of the callback execution will be captured as a span & a metric.
 *
 * @experimental This API is experimental and might have breaking changes in the future.
 */
function timing(
  aggregator,
  name,
  value,
  unit = 'second',
  data,
) {
  // callback form
  if (typeof value === 'function') {
    const startTime = timestampInSeconds();

    return startSpanManual(
      {
        op: 'metrics.timing',
        name,
        startTime,
        onlyIfParent: true,
      },
      span => {
        return handleCallbackErrors(
          () => value(),
          () => {
            // no special error handling necessary
          },
          () => {
            const endTime = timestampInSeconds();
            const timeDiff = endTime - startTime;
            distribution(aggregator, name, timeDiff, { ...data, unit: 'second' });
            span.end(endTime);
          },
        );
      },
    );
  }

  // value form
  distribution(aggregator, name, value, { ...data, unit });
}

/**
 * Adds a value to a set metric. Value must be a string or integer.
 *
 * @experimental This API is experimental and might have breaking changes in the future.
 */
function set(aggregator, name, value, data) {
  addToMetricsAggregator(aggregator, SET_METRIC_TYPE, name, value, data);
}

/**
 * Adds a value to a gauge metric
 *
 * @experimental This API is experimental and might have breaking changes in the future.
 */
function gauge(aggregator, name, value, data) {
  addToMetricsAggregator(aggregator, GAUGE_METRIC_TYPE, name, ensureNumber(value), data);
}

const metrics = {
  increment,
  distribution,
  set,
  gauge,
  timing,
  /**
   * @ignore This is for internal use only.
   */
  getMetricsAggregatorForClient,
};

// Although this is typed to be a number, we try to handle strings as well here
function ensureNumber(number) {
  return typeof number === 'string' ? parseInt(number) : number;
}

export { metrics };
