import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { deliverMessage } from './delivery.js';
const log = createChildLogger('delivery:retry-worker');
let workerInterval = null;
const POLL_INTERVAL_MS = 60_000; // every minute
export function startDeliveryRetryWorker() {
    if (workerInterval)
        return;
    log.info('🚚 starting delivery retry worker');
    // First pass immediately so messages spooled during downtime get drained
    void drain().catch(err => log.warn({ err: err.message }, 'initial drain failed'));
    workerInterval = setInterval(() => {
        void drain().catch(err => log.warn({ err: err.message }, 'drain cycle failed'));
    }, POLL_INTERVAL_MS);
}
export function stopDeliveryRetryWorker() {
    if (workerInterval) {
        clearInterval(workerInterval);
        workerInterval = null;
        log.info('🚚 delivery retry worker stopped');
    }
}
async function drain() {
    const now = new Date().toISOString();
    const { data, error } = await supabase
        .from('delivery_queue')
        .select('*')
        .eq('status', 'pending')
        .lte('next_attempt_at', now)
        .order('next_attempt_at', { ascending: true })
        .limit(20);
    if (error) {
        log.warn({ err: error.message }, 'queue read failed');
        return;
    }
    const rows = (data ?? []);
    if (rows.length === 0)
        return;
    log.info({ count: rows.length }, 'draining delivery queue');
    for (const row of rows) {
        await processRow(row);
    }
}
async function processRow(row) {
    const payload = (() => {
        try {
            return JSON.parse(row.payload);
        }
        catch {
            return null;
        }
    })();
    if (!payload) {
        await markFailed(row.id, 'unparseable payload');
        return;
    }
    // Mark sending so concurrent workers don't double-send (best-effort, no row locks in SQLite shim)
    await supabase
        .from('delivery_queue')
        .update({ status: 'sending', updated_at: new Date().toISOString() })
        .eq('id', row.id);
    const options = {
        tenantId: row.tenant_id,
        to: row.recipient,
        message: payload.message,
        channel: row.channel,
        metadata: payload.metadata,
    };
    // Bypass the spool-on-fail path inside deliverMessage by checking explicitly
    // here — we don't want infinite re-enqueue loops on the same row.
    const result = await deliverMessage(options).catch(err => ({
        success: false,
        error: err.message,
        channel: row.channel,
        deliveredAt: new Date().toISOString(),
    }));
    if (result.success) {
        await supabase
            .from('delivery_queue')
            .update({
            status: 'delivered',
            delivered_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
        })
            .eq('id', row.id);
        return;
    }
    const newCount = row.retry_count + 1;
    if (newCount >= row.max_retries) {
        await markFailed(row.id, result.error ?? 'unknown', newCount);
        await escalateFailedDelivery(row, payload.message, result.error ?? 'unknown');
        return;
    }
    // Exponential backoff: 30s, 2m, 8m, 30m, 2h
    const backoffMs = 30_000 * Math.pow(4, newCount);
    const nextAttempt = new Date(Date.now() + backoffMs);
    await supabase
        .from('delivery_queue')
        .update({
        status: 'pending',
        retry_count: newCount,
        next_attempt_at: nextAttempt.toISOString(),
        last_error: (result.error ?? '').slice(0, 500),
        updated_at: new Date().toISOString(),
    })
        .eq('id', row.id);
    log.info({ id: row.id, retry_count: newCount, next: nextAttempt.toISOString() }, 'retry scheduled');
}
async function markFailed(id, error, retryCount) {
    await supabase
        .from('delivery_queue')
        .update({
        status: 'failed',
        last_error: error.slice(0, 500),
        retry_count: retryCount,
        updated_at: new Date().toISOString(),
    })
        .eq('id', id);
}
async function escalateFailedDelivery(row, messagePreview, error) {
    try {
        const { escalate } = await import('../autonomy/escalation.js');
        await escalate({
            tenantId: row.tenant_id,
            title: `Delivery failed: ${row.channel} → ${row.recipient}`,
            context: `After ${row.retry_count + 1} retries, this message could not be delivered. Last error: ${error.slice(0, 200)}.`,
            recommendation: `Reach out to ${row.recipient} via an alternative channel and resend manually. Then disable the failing channel until investigated.`,
            counterargument: `The recipient may have blocked the channel intentionally — manual outreach could be unwelcome.`,
            alternatives: [
                'Resend manually now',
                'Wait 24h and retry once',
                'Drop and notify the customer-success team',
            ],
            riskLevel: 'high',
        });
    }
    catch (err) {
        log.warn({ err: err.message }, 'failed to escalate dead-lettered delivery');
    }
    // Also journal it
    try {
        const { record } = await import('../autonomy/journal.js');
        await record({
            tenantId: row.tenant_id,
            kind: 'delivery_failed',
            action: `${row.channel} → ${row.recipient}`,
            target: row.recipient,
            outcome: 'failure',
            reason: `Dead-lettered after ${row.retry_count + 1} retries: ${error.slice(0, 200)}`,
            metadata: { messagePreview: messagePreview.slice(0, 100), queueId: row.id },
        });
    }
    catch { /* ignore */ }
}
//# sourceMappingURL=retry-worker.js.map