const DEFAULT_OPTIONS = {
    minIntervalMs: 250,
    editMinIntervalMs: 1_500,
    typingMinIntervalMs: 4_500,
    maxRetries: 5,
};
export class SlackRateLimiter {
    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) {
        if (method.startsWith("edit"))
            return this.options.editMinIntervalMs;
        if (method.startsWith("typing"))
            return this.options.typingMinIntervalMs;
        return this.options.minIntervalMs;
    }
}
export const slackRateLimiter = new SlackRateLimiter();
export function getSlackRateLimitMetrics() {
    return slackRateLimiter.getMetrics();
}
function getRetryAfterSeconds(error) {
    if (!error || typeof error !== "object") {
        return undefined;
    }
    const candidate = error;
    if (typeof candidate.retryAfter === "number") {
        return candidate.retryAfter;
    }
    if (typeof candidate.data?.retryAfter === "number") {
        return candidate.data.retryAfter;
    }
    const header = candidate.headers?.["retry-after"];
    const raw = Array.isArray(header) ? header[0] : header;
    const parsed = Number(raw);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
