import type {
  DiagLogger,
  MeterProvider,
  Tracer,
  TracerProvider,
} from "@opentelemetry/api";
import { diag, metrics, trace } from "@opentelemetry/api";
import type {
  Instrumentation,
  InstrumentationConfig,
} from "@opentelemetry/instrumentation";

import type {
  Transport as CoreTransport,
  Instrumenter,
} from "@cerbos/core/~internal";
import { addInstrumenter, removeInstrumenter } from "@cerbos/core/~internal";

import { Instruments } from "./instruments.js";
import { name, version } from "./metadata.js";
import { Transport } from "./transport.js";

/**
 * Configuration for OpenTelemetry instrumentation of Cerbos clients.
 *
 * @remarks
 * See {@link https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_instrumentation.InstrumentationConfig.html | `InstrumentationConfig` documentation from OpenTelemetry}.
 */
export type CerbosInstrumentationConfig = InstrumentationConfig;

/**
 * OpenTelemetry instrumentation for Cerbos clients.
 *
 * @example
 * ```typescript
 * import { CerbosInstrumentation } from "@cerbos/opentelemetry";
 * import { registerInstrumentations } from "@opentelemetry/instrumentation";
 *
 * registerInstrumentations({
 *   instrumentations: [...yourOtherInstrumentations, new CerbosInstrumentation()],
 * });
 * ```
 */
export class CerbosInstrumentation implements Instrumentation {
  /**
   * Name of the instrumentation.
   */
  public readonly instrumentationName = name;

  /**
   * Version of the instrumentation.
   */
  public readonly instrumentationVersion = version;

  /** @internal */
  public ["~tracer"]: Tracer;

  private readonly diag: DiagLogger;
  private readonly instrumenter: Instrumenter;
  private config: CerbosInstrumentationConfig;
  private instruments: Instruments | undefined;

  /**
   * Create OpenTelemetry instrumentation for Cerbos clients.
   */
  public constructor(config: CerbosInstrumentationConfig = {}) {
    this.diag = diag.createComponentLogger({
      namespace: name,
    });

    this.config = { enabled: true, ...config };
    this["~tracer"] = trace.getTracer(name, version);

    this.instrumenter = (transport): CoreTransport =>
      new Transport(this, transport);

    if (this.config.enabled) {
      this.enable();
    }
  }

  /**
   * Gets the instrumentation configuration.
   */
  public getConfig(): CerbosInstrumentationConfig {
    return this.config;
  }

  /**
   * Sets the instrumentation configuration.
   *
   * @remarks
   * Changing `enabled` via this method has no effect.
   * Use the {@link CerbosInstrumentation.disable} and {@link CerbosInstrumentation.enable} methods instead.
   */
  public setConfig(config: CerbosInstrumentationConfig): void {
    this.config = config;
  }

  /**
   * Override the meter provider, which otherwise defaults to the global meter provider.
   */
  public setMeterProvider(meterProvider: MeterProvider): void {
    this.instruments = new Instruments(meterProvider);
  }

  /**
   * Override the tracer provider, which otherwise defaults to the global tracer provider.
   */
  public setTracerProvider(tracerProvider: TracerProvider): void {
    this["~tracer"] = tracerProvider.getTracer(name, version);
  }

  /**
   * Enables the instrumentation.
   */
  public enable(): void {
    this.diag.debug("Enabling Cerbos client instrumentation");
    addInstrumenter(this.instrumenter);
  }

  /**
   * Disables the instrumentation.
   */
  public disable(): void {
    this.diag.debug("Disabling Cerbos client instrumentation");
    removeInstrumenter(this.instrumenter);
  }

  /** @internal */
  public get ["~instruments"](): Instruments {
    return (this.instruments ??= new Instruments(metrics));
  }
}
