import EventEmitter from 'events';

import type { LiteralUnion } from 'type-fest';

import type { EventType } from '../store/models';

interface MyEventType extends EventEmitter {
  on(eventName: LiteralUnion<EventType, string | symbol>, listener: (...args: any[]) => void): this;
  emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
const _events = new EventEmitter();

// Wrap on/once so async listeners auto-register their Promises in CF Workers.
// EventEmitter.emit() is synchronous and discards async listener return values.
// In CF Workers, untracked Promises are abandoned when the request/consumer ends.
// This wrapper ensures async listener work completes via flushPendingJobs().
// In Blocklet Server (__cfPendingJobs__ doesn't exist), this is a transparent pass-through.
function wrapAsyncListener(listener: (...args: any[]) => any): (...args: any[]) => any {
  return function (...args: any[]) {
    const result = listener(...args);
    if (result && typeof result.then === 'function') {
      // Context-aware: HTTP uses waitUntil (non-blocking), queue/cron uses pendingJobs (blocking)
      const isHttp = (globalThis as any).__cfHttpContext__;
      if (isHttp) {
        const waitUntil = (globalThis as any).__cfWaitUntil__;
        if (typeof waitUntil === 'function') {
          waitUntil(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
        }
      } else {
        const pending = (globalThis as any).__cfPendingJobs__;
        if (Array.isArray(pending)) {
          pending.push(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
        }
      }
    }
    return result;
  };
}

// eslint-disable-next-line @typescript-eslint/naming-convention
const _origOn = _events.on.bind(_events);
// eslint-disable-next-line @typescript-eslint/naming-convention
const _origOnce = _events.once.bind(_events);
_events.on = function (eventName: string | symbol, listener: (...args: any[]) => void) {
  return _origOn(eventName, wrapAsyncListener(listener));
};
_events.once = function (eventName: string | symbol, listener: (...args: any[]) => void) {
  return _origOnce(eventName, wrapAsyncListener(listener));
};

export const events = _events as MyEventType;

export const emitAsync = (event: string, ...args: any[]) => {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      cleanup();
      reject(new Error(`Event ${event} timed out after 10000ms`));
    }, 10000);

    const cleanup = () => {
      clearTimeout(timeout);
      events.removeListener(`${event}.done`, handleDone);
      events.removeListener(`${event}.error`, handleError);
    };

    const handleDone = (...results: any[]) => {
      cleanup();
      resolve(results.length > 1 ? results : results[0]);
    };

    const handleError = (error: any) => {
      cleanup();
      reject(error);
    };
    events.once(`${event}.done`, handleDone);
    events.once(`${event}.error`, handleError);
    events.emit(event, ...args);
  });
};
