// fastq shim for CF Workers
// Executes jobs serially with proper callback handling.
//
// Key design decisions:
// 1. The original queue's worker is `async (data, cb) => { ... }` — it's both
//    async (returns a promise) AND manually calls cb(). We must handle BOTH signals.
// 2. If cb is called, that's the primary completion signal (with err/result).
// 3. If the async worker's promise resolves WITHOUT cb being called (e.g.,
//    ensureUniqueExecution early-return when job is already running), we must
//    still resolve to avoid hanging the processQueue loop.
// 4. If the promise rejects without cb being called, forward the error to cb.

type Task = { data: any; cb?: Function };

export default function fastq(_context: any, worker: Function, _concurrency: number) {
  const pending: Task[] = [];
  let running = false;
  let drainFn: (() => void) | null = null;

  async function processQueue() {
    if (running) return;
    running = true;

    while (pending.length > 0) {
      const task = pending.shift()!;
      try {
        await new Promise<void>((resolve) => {
          let cbCalled = false;

          // Wrap the callback so we track whether it was called
          const wrappedCb = (err: any, res: any) => {
            if (cbCalled) return; // guard against double-call
            cbCalled = true;
            task.cb?.(err, res);
            resolve();
          };

          const result = worker(task.data, wrappedCb);

          // Handle async workers that may resolve/reject without calling cb
          if (result && typeof result.then === 'function') {
            result.then(
              () => {
                // Worker's promise resolved. If cb wasn't called, the worker
                // completed without signaling (e.g., duplicate job early-return).
                // Resolve the processQueue loop to avoid hanging.
                if (!cbCalled) {
                  console.warn('[fastq-shim] worker resolved without calling cb — resolving with no-op');
                  cbCalled = true;
                  resolve();
                }
              },
              (err: any) => {
                // Worker's promise rejected without calling cb
                if (!cbCalled) {
                  console.error('[fastq-shim] worker rejected without calling cb:', err);
                  cbCalled = true;
                  task.cb?.(err);
                  resolve();
                }
              }
            );
          }
        });
      } catch (err) {
        console.error('[fastq-shim] unexpected error in processQueue:', err);
        task.cb?.(err);
      }
    }

    running = false;
    if (pending.length === 0 && drainFn) {
      drainFn();
    }
  }

  const q: any = {
    push(data: any, cb?: Function) {
      pending.push({ data, cb });
      // Use queueMicrotask to allow the push caller to set up event listeners
      // before the job starts executing
      queueMicrotask(() => processQueue());
    },
    unshift(data: any, cb?: Function) {
      pending.unshift({ data, cb });
      queueMicrotask(() => processQueue());
    },
    pause() {},
    resume() {},
    idle() { return pending.length === 0 && !running; },
    length() { return pending.length; },
    kill() { pending.length = 0; },
    killAndDrain() {
      pending.length = 0;
      if (drainFn) drainFn();
    },
    get drain() { return drainFn; },
    set drain(fn: any) { drainFn = fn; },
    empty: null as any,
    saturated: null as any,
    error: null as any,
  };

  return q;
}
