import Logger from '@joplin/utils/Logger';
import time from '@joplin/lib/time';

const logger = Logger.create('BackOffHandler');

// This handler performs two checks:
//
// 1. If the plugin makes many API calls one after the other, a delay is going
//    to be applied before responding. The delay is set using backOffIntervals_.
//    When a plugin needs to be throttled that way a warning is displayed so
//    that the author gets an opportunity to fix it.
//
// 2. If the plugin makes many simultaneous calls, the handler throws an
//    exception to stop the plugin. In that case the plugin will be broken, but
//    most plugins will not get this error anyway because call are usually made
//    in sequence. It might reveal a bug though - for example if the plugin
//    makes a call every 1 second, but does not wait for the response (or assume
//    the response will come in less than one second). In that case, the back
//    off intervals combined with the incorrect code will make the plugin fail.

export default class BackOffHandler {

	// The current logic is:
	//
	// - Up to 1000 calls per 10 seconds without restrictions
	// - For calls 1000 to 2000, a 100 ms wait time is applied
	// - Over 2000 calls, a 200 ms wait time is applied
	// - After 10 seconds without making any call, the limits are reset (back to
	//   0 second between calls).
	//
	// If more than 5000 simultaneous calls are being throttled, it's a bug in
	// the plugin (not waiting for API responses), so we stop responding and
	// throw an error.

	private backOffIntervals_ =
		Array(1000).fill(0).concat(
			Array(1000).fill(100)).concat(
			[200]);

	private lastRequestTime_ = 0;
	private pluginId_: string;
	private resetBackOffInterval_ = 10 * 1000;
	private backOffIndex_ = 0;
	private waitCount_ = 0;
	private maxWaitCount_ = 5000;

	public constructor(pluginId: string) {
		this.pluginId_ = pluginId;
	}

	private backOffInterval() {
		const now = Date.now();

		if (now - this.lastRequestTime_ > this.resetBackOffInterval_) {
			this.backOffIndex_ = 0;
		} else {
			this.backOffIndex_++;
		}

		this.lastRequestTime_ = now;
		const effectiveIndex = this.backOffIndex_ >= this.backOffIntervals_.length ? this.backOffIntervals_.length - 1 : this.backOffIndex_;
		return this.backOffIntervals_[effectiveIndex];
	}

	public async wait(path: string, args: unknown) {
		const interval = this.backOffInterval();
		if (!interval) return;

		this.waitCount_++;

		logger.warn(`Plugin ${this.pluginId_}: Applying a backoff of ${interval} milliseconds due to frequent plugin API calls. Consider reducing the number of calls, caching the data, or requesting more data per call. API call was: `, path, args, `[Wait count: ${this.waitCount_}]`);

		if (this.waitCount_ > this.maxWaitCount_) throw new Error(`Plugin ${this.pluginId_}: More than ${this.maxWaitCount_} API calls are waiting - aborting. Please consider queuing the API calls in your plugins to reduce the load on the application.`);

		await time.msleep(interval);

		this.waitCount_--;
	}

}
