import { IMPORT_CONCURRENCY } from '../config';

type Task<T = unknown> = {
  fn: () => Promise<T> | T;
  resolve: (value: T) => void;
  reject: (err: any) => void;
  promise?: Promise<T>;
};

export class ImportRunner {
  private concurrency: number;
  private queue: Task[] = [];
  private runningCount = 0;
  private completedCount = 0;
  private isRunning = false;
  private stopped = false;

  constructor(concurrency?: number) {
    this.concurrency = Math.max(1, concurrency ?? IMPORT_CONCURRENCY);
  }

  start() {
    if (this.isRunning) return;
    this.isRunning = true;
    this.stopped = false;
    this._processQueue();
  }

  async stopAndWait() {
    this.stop();

    // wait until any running tasks finish
    while (this.runningCount > 0) {
      await new Promise((res) => setImmediate(res));
    }
  }
  stop() {
    if (this.stopped) return;

    this.isRunning = false;
    this.stopped = true;

    // reject and count all pending tasks (schedule rejection to avoid unhandled rejections)
    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      this.completedCount++;
      // attach a noop catch so Node doesn't treat the rejection as unhandled
      if (task.promise) {
        task.promise.catch(() => {});
      }
      setImmediate(() => {
        try {
          task.reject(new Error('ImportRunner stopped'));
        } catch (e) {
          // noop
        }
      });
    }
  }

  enqueue<T = unknown>(fn: () => Promise<T> | T): Promise<T> {
    if (this.stopped) {
      return Promise.reject(new Error('ImportRunner stopped'));
    }

    let resolveFn!: (v: T) => void;
    let rejectFn!: (e: any) => void;
    const p = new Promise<T>((res, rej) => {
      resolveFn = res;
      rejectFn = rej;
    });

    this.queue.push({ fn, resolve: resolveFn, reject: rejectFn, promise: p });

    // start or continue processing immediately so the first task begins right away
    if (!this.isRunning) {
      this.start();
    } else {
      this._processQueue();
    }

    return p;
  }

  getStatus() {
    return {
      queued: this.queue.length,
      running: this.runningCount,
      completed: this.completedCount,
      concurrency: this.concurrency,
    };
  }

  private _processQueue() {
    if (!this.isRunning) return;

    while (this.runningCount < this.concurrency && this.queue.length > 0) {
      const task = this.queue.shift()!;

      const result = Promise.resolve().then(() => task.fn());

      this.runningCount++;

      result
        .then((res) => {
          this.runningCount--;
          this.completedCount++;
          try {
            task.resolve(res as any);
          } catch (e) {
            // noop
          }
          setImmediate(() => this._processQueue());
        })
        .catch((err) => {
          this.runningCount--;
          this.completedCount++;
          console.error(err);
          try {
            task.reject(err);
          } catch (e) {
            // noop
          }
          setImmediate(() => this._processQueue());
        });
    }
  }
}

export const runner = new ImportRunner();
runner.start();

export default runner;
