// shortcut: 프로세스 안 대기열(동시 1개, 재시작 시 폴더 스캔으로 복구), 여러 서버 인스턴스가 필요해지면 DB 상태 행 대기열로
export interface QueueItem {
  jobId: string;
  fileId: string;
}

export class ExtractQueue {
  private readonly waiting: QueueItem[] = [];
  private running: QueueItem | null = null;
  private idle: Promise<void> = Promise.resolve();

  constructor(private readonly handler: (item: QueueItem) => Promise<void>) {}

  get length(): number {
    return this.waiting.length;
  }

  enqueue(item: QueueItem): void {
    if (this.has(item)) return;
    this.waiting.push(item);
    this.pump();
  }

  //* 대기 중이면 1부터의 순번, 실행 중이거나 없으면 null
  position(item: QueueItem): number | null {
    const i = this.waiting.findIndex((w) => same(w, item));
    return i < 0 ? null : i + 1;
  }

  remove(jobId: string, fileId?: string): void {
    for (let i = this.waiting.length - 1; i >= 0; i--) {
      const w = this.waiting[i];
      if (w.jobId === jobId && (!fileId || w.fileId === fileId))
        this.waiting.splice(i, 1);
    }
  }

  //* 시험용 — 대기열이 비고 실행 중인 일도 끝날 때까지 기다린다
  async drain(): Promise<void> {
    while (this.running || this.waiting.length) await this.idle;
  }

  //* 실행 중인 같은 파일은 중복으로 보지 않는다 — 실행이 끝나기 전에 들어온 다음 시도(비밀번호 재입력 등)를 잃지 않게.
  //* 다음 시도가 아니면 처리기가 파일 상태(QUEUED 아님)를 보고 그냥 돌아간다
  private has(item: QueueItem): boolean {
    return this.waiting.some((w) => same(w, item));
  }

  private pump(): void {
    if (this.running || !this.waiting.length) return;
    const item = this.waiting.shift() as QueueItem;
    this.running = item;
    this.idle = this.handler(item)
      .catch(() => undefined) //* 처리기는 스스로 오류를 기록한다 — 대기열은 멈추지 않는다
      .then(() => {
        this.running = null;
        this.pump();
      });
  }
}

const same = (a: QueueItem, b: QueueItem) =>
  a.jobId === b.jobId && a.fileId === b.fileId;
