import { logger } from '../utils/logger.js';
import { randomInt, randomUUID } from 'node:crypto';
import { GoodVibesSdkError, RETRYABLE_STATUS_CODES } from '@pellux/goodvibes-errors';
import { summarizeError } from '../utils/error-display.js';
// ---------------------------------------------------------------------------
// Failure classification
// ---------------------------------------------------------------------------
/** HTTP status codes that indicate a retryable transient failure. */
const DELIVERY_RETRYABLE_STATUSES = new Set(RETRYABLE_STATUS_CODES);
/**
 * Classify a delivery error as retryable or terminal.
 *
 * Rules (in order):
 * 1. Network errors with no HTTP status → retryable (timeout, ECONNREFUSED, etc.)
 * 2. HTTP 4xx (except 408/429) → terminal (auth failure, bad request, not found)
 * 3. HTTP 429 / 5xx → retryable
 * 4. Unknown → retryable (prefer retry over silent drop)
 */
export function classifyDeliveryError(error) {
    if (error instanceof DeliveryError) {
        return error.failureClass;
    }
    if (error instanceof Error) {
        const msg = error.message.toLowerCase();
        // HTTP status in message (e.g. "HTTP 400: bad request")
        const match = /http\s+(\d{3})/.exec(msg);
        if (match) {
            const status = parseInt(match[1], 10);
            return DELIVERY_RETRYABLE_STATUSES.has(status) ? 'retryable' : 'terminal';
        }
        // Network-level errors: timeout, connection refused, DNS failure
        if (msg.includes('timeout') ||
            msg.includes('aborted') ||
            msg.includes('econnrefused') ||
            msg.includes('enotfound') ||
            msg.includes('network')) {
            return 'retryable';
        }
        // TypeError for invalid URL or fetch misconfiguration
        if (error instanceof TypeError) {
            return 'terminal';
        }
    }
    // Default: retryable; delivery failures remain observable.
    return 'retryable';
}
// ---------------------------------------------------------------------------
// DeliveryError — typed error with explicit classification
// ---------------------------------------------------------------------------
/** Typed delivery error that carries an explicit failure classification. */
export class DeliveryError extends GoodVibesSdkError {
    failureClass;
    statusCode;
    constructor(message, failureClass, statusCode) {
        super(message, { code: 'DELIVERY_ERROR', category: 'internal', source: 'runtime', recoverable: false });
        this.failureClass = failureClass;
        this.statusCode = statusCode;
        this.name = 'DeliveryError';
    }
}
const DEFAULT_CONFIG = {
    maxRetries: 3,
    initialDelayMs: 1_000,
    maxDelayMs: 30_000,
    maxDlqSize: 500,
    sloEnforced: false,
};
// ---------------------------------------------------------------------------
// DeliveryQueue
// ---------------------------------------------------------------------------
/**
 * Delivery queue with retry/backoff and dead-letter storage.
 *
 * Wrap any integration send operation with `enqueue()`. The queue:
 *  1. Attempts delivery immediately.
 *  2. On retryable failure: schedules retry with exponential backoff + jitter.
 *  3. On terminal failure or exhausted retries: moves entry to DLQ.
 *  4. Emits `delivery:dead_letter` events to registered listeners.
 *
 * Dead-letter entries can be replayed via `replay()` or cleared with `clearDlq()`.
 *
 * Enable SLO enforcement via the `integration-delivery-slo` feature flag to
 * surface dead-letter failures as error-level log entries and expose them in
 * integration diagnostics.
 */
export class DeliveryQueue {
    _config;
    _dlq = [];
    _pending = new Map();
    _timers = new Map();
    _listeners = new Set();
    // Metrics counters
    _totalAttempts = 0;
    _delivered = 0;
    _retrying = 0;
    _deadLettered = 0;
    constructor(config = {}) {
        const { featureFlags, ...queueConfig } = config;
        this._config = {
            ...DEFAULT_CONFIG,
            ...queueConfig,
            sloEnforced: queueConfig.sloEnforced
                ?? featureFlags?.isEnabled('integration-delivery-slo')
                ?? DEFAULT_CONFIG.sloEnforced,
        };
    }
    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /**
     * Enqueue a delivery attempt.
     *
     * @param channel - Integration channel identifier (e.g. "slack").
     * @param event   - Event name for tracing.
     * @param payload - Message text to deliver.
     * @param deliver - Async function that performs the actual delivery.
     * @returns The delivery outcome for the immediate attempt.
     */
    async enqueue(channel, event, payload, deliver) {
        const id = `${channel}:${event}:${Date.now()}:${randomUUID().slice(0, 8)}`;
        const entry = {
            id,
            channel,
            event,
            payload,
            createdAt: Date.now(),
            attempts: 0,
            nextAttemptAt: 0,
            deliver,
        };
        this._pending.set(id, entry);
        return this._attempt(entry);
    }
    /**
     * Replay all dead-letter entries.
     *
     * Each entry is re-enqueued with a fresh retry budget. The DLQ is cleared
     * on a per-entry basis as each replayed entry resolves.
     *
     * @param deliver - Optional delivery function override. When omitted,
     *   the original delivery function is not available (DLQ is persistent
     *   storage), so a no-op is used and the caller must provide one.
     *
     * @returns Array of per-entry replay results.
     */
    async replay(deliver) {
        const entries = [...this._dlq];
        const results = [];
        for (const dlqEntry of entries) {
            // Remove from DLQ before replaying
            const idx = this._dlq.findIndex((e) => e.id === dlqEntry.id);
            if (idx !== -1)
                this._dlq.splice(idx, 1);
            this._deadLettered = Math.max(0, this._deadLettered - 1);
            const outcome = await this.enqueue(dlqEntry.channel, dlqEntry.event, dlqEntry.payload, () => deliver(dlqEntry));
            results.push({ id: dlqEntry.id, outcome });
        }
        return results;
    }
    /**
     * Register a listener invoked whenever an entry moves to the DLQ.
     * Returns an unsubscribe function.
     */
    onDeadLetter(listener) {
        this._listeners.add(listener);
        return () => this._listeners.delete(listener);
    }
    /** Get current dead-letter queue contents (snapshot). */
    getDlq() {
        return [...this._dlq];
    }
    /** Clear all dead-letter entries. */
    clearDlq() {
        const count = this._dlq.length;
        this._dlq.length = 0;
        return count;
    }
    /** Whether SLO enforcement is active for this queue. */
    get sloEnforced() { return this._config.sloEnforced; }
    /** Current delivery metrics snapshot. */
    getMetrics() {
        return {
            totalAttempts: this._totalAttempts,
            delivered: this._delivered,
            retrying: this._retrying,
            deadLettered: this._deadLettered,
            dlqSize: this._dlq.length,
        };
    }
    /**
     * Cancel all pending retry timers and clear internal state.
     * Call on shutdown to prevent timer leaks.
     */
    dispose() {
        for (const timer of this._timers.values()) {
            clearTimeout(timer);
        }
        this._timers.clear();
        this._pending.clear();
    }
    async _attempt(entry) {
        entry.attempts += 1;
        this._totalAttempts += 1;
        try {
            await entry.deliver();
            this._delivered += 1;
            if (entry.attempts > 1)
                this._retrying = Math.max(0, this._retrying - 1);
            this._pending.delete(entry.id);
            logger.debug('DeliveryQueue: delivered', {
                channel: entry.channel,
                event: entry.event,
                attempts: entry.attempts,
            });
            return 'delivered';
        }
        catch (err) {
            const errorMsg = summarizeError(err);
            const failureClass = classifyDeliveryError(err);
            if (failureClass === 'terminal' || entry.attempts > this._config.maxRetries) {
                return this._moveToDlq(entry, errorMsg, failureClass);
            }
            // Schedule retry
            const delayMs = this._computeDelay(entry.attempts);
            entry.nextAttemptAt = Date.now() + delayMs;
            this._retrying += 1;
            logger.warn('DeliveryQueue: retrying', {
                channel: entry.channel,
                event: entry.event,
                attempt: entry.attempts,
                maxRetries: this._config.maxRetries,
                delayMs,
                error: errorMsg,
                failureClass,
            });
            const timer = setTimeout(() => {
                this._timers.delete(entry.id);
                this._retrying = Math.max(0, this._retrying - 1);
                void this._attempt(entry).catch((error) => {
                    logger.warn('DeliveryQueue: retry attempt failed outside delivery classifier', {
                        channel: entry.channel,
                        event: entry.event,
                        error: summarizeError(error),
                    });
                });
            }, delayMs);
            timer.unref?.();
            this._timers.set(entry.id, timer);
            return 'retrying';
        }
    }
    _moveToDlq(entry, finalError, failureClass) {
        const dlqEntry = {
            id: entry.id,
            channel: entry.channel,
            event: entry.event,
            payload: entry.payload,
            createdAt: entry.createdAt,
            deadAt: Date.now(),
            attempts: entry.attempts,
            finalError,
            failureClass,
        };
        // Bounded DLQ: evict oldest entry when limit exceeded
        if (this._dlq.length >= this._config.maxDlqSize) {
            this._dlq.shift();
            this._deadLettered = Math.max(0, this._deadLettered - 1);
        }
        this._dlq.push(dlqEntry);
        this._deadLettered += 1;
        this._pending.delete(entry.id);
        if (this._config.sloEnforced) {
            logger.error('DeliveryQueue: dead-lettered (SLO violated)', {
                id: dlqEntry.id,
                channel: dlqEntry.channel,
                event: dlqEntry.event,
                attempts: dlqEntry.attempts,
                finalError: dlqEntry.finalError,
                failureClass: dlqEntry.failureClass,
            });
        }
        else {
            logger.warn('DeliveryQueue: dead-lettered', {
                id: dlqEntry.id,
                channel: dlqEntry.channel,
                event: dlqEntry.event,
                attempts: dlqEntry.attempts,
                finalError: dlqEntry.finalError,
                failureClass: dlqEntry.failureClass,
            });
        }
        for (const listener of this._listeners) {
            try {
                listener(dlqEntry);
            }
            catch (err) {
                logger.warn('[delivery] listener error:', {
                    error: summarizeError(err),
                    entryId: dlqEntry.id,
                });
            }
        }
        return 'dead_letter';
    }
    _computeDelay(attempt) {
        const exponential = this._config.initialDelayMs * Math.pow(2, attempt - 1);
        const jitter = randomInt(0, Math.max(1, Math.floor(this._config.initialDelayMs * 0.5) + 1));
        return Math.min(exponential + jitter, this._config.maxDelayMs);
    }
}
/**
 * Produce a diagnostics snapshot for a channel's DeliveryQueue.
 */
export function snapshotQueueStatus(channel, queue, sloEnforced) {
    const dlq = queue.getDlq();
    return {
        channel,
        metrics: queue.getMetrics(),
        dlqEntries: [...dlq].reverse().slice(0, 50),
        sloEnforced,
        capturedAt: Date.now(),
    };
}
