const DEFAULT_OPTIONS = {
    minIntervalMs: 80,
    editMinIntervalMs: 1200,
    maxRetries: 5,
};
export class TelegramRateLimiter {
    options;
    buckets = new Map();
    queued = 0;
    running = 0;
    completed = 0;
    failed = 0;
    retries = 0;
    rateLimitHits = 0;
    lastRateLimitAt;
    lastRetryAfterSeconds;
    constructor(options = DEFAULT_OPTIONS) {
        this.options = options;
    }
    configure(options) {
        this.options = { ...this.options, ...options };
    }
    async run(bucket, method, operation) {
        const normalizedBucket = `${method}:${bucket}`;
        const state = this.getBucket(normalizedBucket);
        this.queued += 1;
        let releasePrevious;
        const previous = state.chain;
        state.chain = new Promise((resolve) => {
            releasePrevious = resolve;
        });
        await previous.catch(() => { });
        this.queued = Math.max(0, this.queued - 1);
        this.running += 1;
        try {
            await this.waitForBucket(state, method);
            const result = await this.runWithRetries(operation);
            this.completed += 1;
            state.lastRunAtMs = Date.now();
            state.queuedUntilMs = Math.max(state.queuedUntilMs, state.lastRunAtMs + this.intervalForMethod(method));
            return result;
        }
        catch (error) {
            this.failed += 1;
            throw error;
        }
        finally {
            this.running = Math.max(0, this.running - 1);
            releasePrevious();
        }
    }
    getMetrics() {
        return {
            queued: this.queued,
            running: this.running,
            completed: this.completed,
            failed: this.failed,
            retries: this.retries,
            rateLimitHits: this.rateLimitHits,
            lastRateLimitAt: this.lastRateLimitAt,
            lastRetryAfterSeconds: this.lastRetryAfterSeconds,
            buckets: [...this.buckets.entries()]
                .filter(([, state]) => state.queuedUntilMs > Date.now() || state.lastRunAtMs > 0)
                .slice(0, 12)
                .map(([bucket, state]) => ({
                bucket,
                queuedUntilMs: state.queuedUntilMs,
                lastRunAtMs: state.lastRunAtMs,
            })),
        };
    }
    getBucket(bucket) {
        let state = this.buckets.get(bucket);
        if (!state) {
            state = {
                chain: Promise.resolve(),
                queuedUntilMs: 0,
                lastRunAtMs: 0,
            };
            this.buckets.set(bucket, state);
        }
        return state;
    }
    async waitForBucket(state, method) {
        const interval = this.intervalForMethod(method);
        const now = Date.now();
        const earliest = Math.max(state.queuedUntilMs, state.lastRunAtMs + interval);
        if (earliest > now) {
            await delay(earliest - now);
        }
    }
    async runWithRetries(operation) {
        let attempt = 0;
        while (true) {
            try {
                return await operation();
            }
            catch (error) {
                const retryAfterSeconds = getRetryAfterSeconds(error);
                if (retryAfterSeconds === undefined || attempt >= this.options.maxRetries) {
                    throw error;
                }
                attempt += 1;
                this.retries += 1;
                this.rateLimitHits += 1;
                this.lastRateLimitAt = new Date().toISOString();
                this.lastRetryAfterSeconds = retryAfterSeconds;
                await delay((retryAfterSeconds * 1000) + 250);
            }
        }
    }
    intervalForMethod(method) {
        return method.startsWith("edit") ? this.options.editMinIntervalMs : this.options.minIntervalMs;
    }
}
export const telegramRateLimiter = new TelegramRateLimiter();
export function getTelegramRateLimitMetrics() {
    return telegramRateLimiter.getMetrics();
}
function getRetryAfterSeconds(error) {
    const candidate = error;
    const direct = candidate.parameters?.retry_after ?? candidate.error?.parameters?.retry_after;
    if (typeof direct === "number" && Number.isFinite(direct)) {
        return Math.max(1, direct);
    }
    const text = `${typeof candidate.description === "string" ? candidate.description : ""} ${typeof candidate.message === "string" ? candidate.message : ""}`;
    const match = text.match(/retry after\s+(\d+)/i);
    if (!match) {
        return undefined;
    }
    const parsed = Number(match[1]);
    return Number.isFinite(parsed) ? Math.max(1, parsed) : undefined;
}
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
}
