import {
  Counter,
  createNoopMeter,
  Gauge,
  Histogram,
  Meter,
  MetricOptions,
  metrics,
  ObservableCounter,
  ObservableGauge,
  ObservableUpDownCounter,
  UpDownCounter,
  Attributes,
} from "@opentelemetry/api";

type MetricTypeMap<TAttributes extends Attributes> = {
  counter: Counter<TAttributes>;
  histogram: Histogram<TAttributes>;
  gauge: Gauge<TAttributes>;
  updowncounter: UpDownCounter<TAttributes>;
  "async-counter": ObservableCounter<TAttributes>;
  "async-updowncounter": ObservableUpDownCounter<TAttributes>;
  "async-gauge": ObservableGauge<TAttributes>;
};

export type MetricType = keyof MetricTypeMap<Attributes>;

const MetricsFactoryMap: Record<
  MetricType,
  (
    meter: Meter,
  ) => (
    name: string,
    options?: MetricOptions,
  ) => MetricTypeMap<Attributes>[MetricType]
> = {
  gauge: (meter: Meter) => meter.createGauge,
  histogram: (meter: Meter) => meter.createHistogram,
  counter: (meter: Meter) => meter.createCounter,
  updowncounter: (meter: Meter) => meter.createUpDownCounter,
  "async-counter": (meter: Meter) => meter.createObservableCounter,
  "async-updowncounter": (meter: Meter) => meter.createObservableUpDownCounter,
  "async-gauge": (meter: Meter) => meter.createObservableGauge,
} as const;

export interface MetricsParams {
  meterName: string;
  metricName: string;
  options?: MetricOptions;
}

function getMeter({ meterName }: MetricsParams) {
  if (!meterName) {
    console.error("Invalid metric name!");
    return createNoopMeter();
  }

  return metrics.getMeter(`custom_metric.${meterName}`);
}

export function getMetric<
  T extends MetricType,
  TAttributes extends Attributes = Attributes,
>(type: T, p: MetricsParams): MetricTypeMap<TAttributes>[T] {
  const meter = getMeter(p);

  if (!MetricsFactoryMap[type]) {
    throw new Error(`Unsupported metric type: ${type}`);
  }

  return MetricsFactoryMap[type](meter).bind(meter)(
    p.metricName,
    p.options,
  ) as MetricTypeMap<TAttributes>[T];
}
