import EventEmitter from 'events';

import { Tracer } from '@opentelemetry/api';

import { addTracingToMethods, traceFunction } from '../tracing/methodTracing.js';

type DefaultEventMap = [never];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type EventMap<T> = Record<keyof T, any[]> | DefaultEventMap;

type Key<K, T> = T extends DefaultEventMap ? string | symbol : K | keyof T;
type Listener<K, T, F> = T extends DefaultEventMap
  ? F
  : K extends keyof T
    ? T[K] extends unknown[]
      ? (...args: T[K]) => void
      : never
    : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Listener1<K, T> = Listener<K, T, (...args: any[]) => void>;

export class TracedEventEmitter<TEventMap extends EventMap<TEventMap>> extends EventEmitter<TEventMap> {
  private readonly tracer: Tracer;

  constructor(tracer: Tracer, options?: { captureRejections?: boolean }) {
    super(options);
    this.tracer = tracer;

    const methods = ['emit'];

    addTracingToMethods(this, methods, this.tracer);
  }

  public override on<K>(eventName: Key<K, TEventMap>, listener1: Listener1<K, TEventMap>): this {
    const tracer = this.tracer;
    const klassName = this.constructor.name;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const name = (listener1 as any).name;
    const listenerStr = name ? name : 'anonymous';
    const outputStr = `${klassName}.emit listener on ${String(eventName)}: ${listenerStr}`;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const wrappedListener = ((...args: any[]) => {
      traceFunction({
        name: outputStr,
        tracer: tracer,
        fn: async () => {
          // await the listener, but since we're not awaiting the wrapper, we've decoupled .emit()
          // returning from the amount of time that the listener takes, but this will let us get a
          // more accurate idea of how long callers take
          // eslint-disable-next-line @typescript-eslint/await-thenable
          await listener1(...args);
        }
      }).catch((err) => {
        throw err;
      });
    }) as typeof listener1;

    super.on(eventName, wrappedListener);

    return this;
  }
}
