import { DEFAULT_QUEUE_CONFIG } from './types.js';
import { summarizeError } from '../../../utils/error-display.js';
/**
 * Computes the delay in milliseconds for a given retry attempt using
 * exponential backoff with jitter (±10%).
 */
function computeDelay(attempt, config) {
    const base = config.baseDelayMs * Math.pow(config.backoffFactor, attempt);
    const capped = Math.min(base, config.maxDelayMs);
    // Add ±10% jitter to avoid thundering-herd
    const jitter = capped * 0.1 * (Math.random() * 2 - 1);
    return Math.max(0, Math.round(capped + jitter));
}
/** Awaitable sleep. */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Bounded in-memory export queue with retry and overflow protection.
 *
 * @example
 * ```ts
 * const queue = new ExportQueue(
 *   async (batch) => {
 *     await fetch('http://localhost:4318/v1/traces', {
 *       method: 'POST',
 *       body: JSON.stringify(batch),
 *     });
 *   },
 *   { maxSize: 64, drainTimeoutMs: 3000, retry: { maxRetries: 2, baseDelayMs: 50, backoffFactor: 2, maxDelayMs: 1000 } },
 * );
 *
 * queue.enqueue(spans);
 * await queue.drain();
 * ```
 */
export class ExportQueue {
    _config;
    _exportFn;
    _onResult;
    /** Ring buffer holding pending batches. */
    _ring;
    _head = 0;
    _tail = 0;
    _size = 0;
    /** True while a drain loop is running. */
    _draining = false;
    /** True after shutdown() is called — no new enqueues accepted. */
    _shutdown = false;
    /**
     * @param exportFn - The actual export implementation. Must not throw.
     * @param config - Queue configuration. Merged with defaults.
     * @param onResult - Optional callback invoked after each export attempt.
     */
    constructor(exportFn, config, onResult) {
        this._config = {
            ...DEFAULT_QUEUE_CONFIG,
            ...config,
            retry: { ...DEFAULT_QUEUE_CONFIG.retry, ...config?.retry },
        };
        this._exportFn = exportFn;
        this._onResult = onResult;
        this._ring = new Array(this._config.maxSize);
    }
    /** Number of batches currently in the queue. */
    get size() {
        return this._size;
    }
    /** Whether the queue is currently draining. */
    get draining() {
        return this._draining;
    }
    /**
     * Enqueue a batch of spans for export.
     *
     * - If the queue is full, the oldest entry is dropped to make room.
     * - Starts the drain loop if not already running.
     * - No-op after shutdown().
     */
    enqueue(batch) {
        if (this._shutdown)
            return;
        if (batch.length === 0)
            return;
        if (this._size >= this._config.maxSize) {
            // Drop oldest — advance head
            const dropped = this._ring[this._head];
            this._head = (this._head + 1) % this._config.maxSize;
            this._size--;
            if (dropped) {
                this._emitResult({
                    code: 'dropped',
                    spanCount: dropped.batch.length,
                    attempts: 0,
                    completedAt: Date.now(),
                });
            }
            console.warn(`[ExportQueue] Queue overflow — dropped oldest batch (size=${this._config.maxSize})`);
        }
        this._ring[this._tail] = { batch, enqueuedAt: Date.now() };
        this._tail = (this._tail + 1) % this._config.maxSize;
        this._size++;
        this._startDrainLoop();
    }
    /**
     * Drain all queued batches best-effort, honouring drainTimeoutMs.
     * Waits until the queue is empty or the timeout elapses.
     */
    async drain() {
        if (this._size === 0)
            return;
        const deadline = Date.now() + this._config.drainTimeoutMs;
        while (this._size > 0 && Date.now() < deadline) {
            await sleep(10);
        }
    }
    /**
     * Stop accepting new entries and drain remaining batches.
     * Returns after drain completes or drainTimeoutMs elapses.
     */
    async shutdown() {
        this._shutdown = true;
        await this.drain();
    }
    // ── Private drain loop ───────────────────────────────────────────────────
    /** Starts the async drain loop if not already running. */
    _startDrainLoop() {
        if (this._draining)
            return;
        this._draining = true;
        // Fire-and-forget — errors are swallowed inside _processNext
        void this._drainLoop();
    }
    /** Continuously drains entries until the queue is empty. */
    async _drainLoop() {
        try {
            while (this._size > 0) {
                const entry = this._dequeue();
                if (entry === undefined)
                    break;
                await this._processEntry(entry);
            }
        }
        finally {
            this._draining = false;
            // If more entries arrived during the final iteration, restart
            if (this._size > 0) {
                this._startDrainLoop();
            }
        }
    }
    /** Dequeues and returns the oldest entry, or undefined if empty. */
    _dequeue() {
        if (this._size === 0)
            return undefined;
        const entry = this._ring[this._head];
        this._ring[this._head] = undefined;
        this._head = (this._head + 1) % this._config.maxSize;
        this._size--;
        return entry;
    }
    /**
     * Attempts to export a single batch with retry/backoff.
     * Never throws.
     */
    async _processEntry(entry) {
        const { maxRetries } = this._config.retry;
        let lastError;
        let attempts = 0;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                await this._exportFn(entry.batch);
                this._emitResult({
                    code: 'success',
                    spanCount: entry.batch.length,
                    attempts,
                    completedAt: Date.now(),
                });
                return;
            }
            catch (err) {
                attempts++;
                lastError = summarizeError(err);
                if (attempt < maxRetries) {
                    const delay = computeDelay(attempt, this._config.retry);
                    console.warn(`[ExportQueue] Export attempt ${attempt + 1} failed — retrying in ${delay}ms: ${lastError}`);
                    await sleep(delay);
                }
            }
        }
        // All retries exhausted
        console.error(`[ExportQueue] Export failed after ${attempts} attempt(s) — dropping batch of ${entry.batch.length} span(s): ${lastError}`);
        this._emitResult({
            code: 'failure',
            spanCount: entry.batch.length,
            attempts,
            error: lastError,
            completedAt: Date.now(),
        });
    }
    /** Safely invokes the result callback without throwing. */
    _emitResult(result) {
        if (this._onResult === undefined)
            return;
        try {
            this._onResult(result);
        }
        catch {
            // Result callback must not propagate
        }
    }
}
