/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Counter, Histogram, Meter, metrics } from '@opentelemetry/api';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { PathMetadata } from 'genkit/tracing';

export const METER_NAME = 'genkit';
export const METRIC_NAME_PREFIX = 'genkit';
const METRIC_DIMENSION_MAX_CHARS = 32;

export function internalMetricNamespaceWrap(...namespaces) {
  return [METRIC_NAME_PREFIX, ...namespaces].join('/');
}

type MetricCreateFn<T> = (meter: Meter) => T;

/**
 * Wrapper for OpenTelemetry metrics.
 *
 * The OpenTelemetry {MeterProvider} can only be accessed through the metrics
 * API after the NodeSDK library has been initialized. To prevent race
 * conditions we defer the instantiation of the metric to when it is first
 * ticked.
 */
class Metric<T> {
  readonly createFn: MetricCreateFn<T>;
  readonly meterName: string;
  metric?: T;

  constructor(createFn: MetricCreateFn<T>, meterName: string = METER_NAME) {
    this.meterName = meterName;
    this.createFn = createFn;
  }

  get(): T {
    if (!this.metric) {
      this.metric = this.createFn(
        metrics.getMeterProvider().getMeter(this.meterName)
      );
    }

    return this.metric;
  }
}

/**
 * Wrapper for an OpenTelemetry Counter.
 *
 * By using this wrapper, we defer initialization of the counter until it is
 * need, which ensures that the OpenTelemetry SDK has been initialized before
 * the metric has been defined.
 */
export class MetricCounter extends Metric<Counter> {
  constructor(name: string, options: any) {
    super((meter) => meter.createCounter(name, options));
  }

  add(val?: number, opts?: any) {
    if (val) {
      truncateDimensions(opts);
      this.get().add(val, opts);
    }
  }
}

/**
 * Wrapper for an OpenTelemetry Histogram.
 *
 * By using this wrapper, we defer initialization of the counter until it is
 * need, which ensures that the OpenTelemetry SDK has been initialized before
 * the metric has been defined.
 */
export class MetricHistogram extends Metric<Histogram> {
  constructor(name: string, options: any) {
    super((meter) => meter.createHistogram(name, options));
  }

  record(val?: number, opts?: any) {
    if (val) {
      truncateDimensions(opts);
      this.get().record(val, opts);
    }
  }
}

/**
 * Truncates all of the metric dimensions to avoid long, high-cardinality
 * dimensions being added to metrics.
 */
function truncateDimensions(opts?: any) {
  if (opts) {
    Object.keys(opts).forEach((k) => {
      // We don't want to truncate paths. They are known to be long but with
      // relatively low cardinality, and are useful for downstream monitoring.
      if (!k.startsWith('path') && typeof opts[k] == 'string') {
        opts[k] = opts[k].substring(0, METRIC_DIMENSION_MAX_CHARS);
      }
    });
  }
}

export interface Telemetry {
  tick(
    span: ReadableSpan,
    paths: Set<PathMetadata>,
    logIO: boolean,
    projectId?: string
  ): void;
}
