/*
 This file is part of GNU Taler
 (C) 2025 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

 SPDX-License-Identifier: AGPL3.0-or-later
*/

import { CancellationToken } from "./CancellationToken.js";
import { Logger } from "./logging.js";
import { openPromise } from "./promises.js";

const logger = new Logger("longpoll-queue.ts");

const PERMITS: number = 20;
type LongpollRunFn<T> = (timeoutMs: number) => Promise<T>;

export class LongpollQueue {
  private idCounter: number = 0;
  private queue: (() => void)[] = [];
  private permits: number = PERMITS

  constructor() { }

  async run<T>(
    url: URL,
    cancellationToken: CancellationToken,
    f: LongpollRunFn<T>,
  ): Promise<T> {
    const hostname = url.hostname;
    const rid = this.idCounter++;

    const triggerNextLongpoll = () => {
      logger.trace(`cleaning up after long-poll ${rid} to ${hostname}`);
      // Run pending task
      const next = this.queue.shift();
      if (next != null) {
        next();
      } else {
        // Else release permit
        this.permits++;
      }
    };

    const doRunLongpoll: () => Promise<T> = async () => {
      const numWaiting = this.queue.length;
      const numConcurrent = PERMITS - this.permits;
      logger.info(
        `running long-poll ${rid} to ${hostname} with ${numWaiting} waiting and ${numConcurrent} running`
      );
      try {
        const timeoutMs = Math.round(Math.max(10000, 30000 / (numWaiting + 1)));
        return await f(timeoutMs);
      } finally {
        triggerNextLongpoll();
      }
    };

    if (this.permits > 0) {
      this.permits--;
      return doRunLongpoll();
    } else {
      logger.info(`long-poll ${rid} to ${hostname} queued`);
      const promcap = openPromise<void>();
      this.queue.push(promcap.resolve);
      try {
        await cancellationToken.racePromise(promcap.promise);
      } finally {
        logger.info(
          `long-poll ${rid} to ${hostname} cancelled while queued`,
        );
        triggerNextLongpoll();
      }
      return doRunLongpoll();
    }
  }
}