/**
 * src/delivery/delivery.ts
 * Delivery Layer — Route messages to multiple channels
 */
import { createChildLogger } from '../utils/logger.js';
import { whatsappClient } from '../channels/whatsapp/client.js';
const log = createChildLogger('delivery');
/**
 * Deliver a message to a channel
 */
export async function deliverMessage(options) {
    const startTime = Date.now();
    log.info({
        tenantId: options.tenantId,
        to: options.to,
        channel: options.channel,
        messageLength: options.message.length,
    }, 'Delivering message');
    // Outbound circuit-breaker — caps customer-facing sends per window so a
    // runaway loop or over-eager outreach batch can't blast the WhatsApp number
    // into a ban / PDPL violation. Owner-facing notifications bypass it.
    const audience = options.metadata?.audience;
    const bypassRL = audience === 'owner' || options.metadata?.bypassRateLimit === true;
    if (!bypassRL) {
        const { checkAndRecordSend } = await import('./rate-limit.js');
        const rl = checkAndRecordSend(options.tenantId, options.channel);
        if (!rl.allowed) {
            log.warn({ tenantId: options.tenantId, channel: options.channel, reason: rl.reason }, 'delivery blocked by rate limit');
            return {
                success: false,
                channel: options.channel,
                error: `rate_limited: ${rl.reason}`,
                deliveredAt: new Date().toISOString(),
            };
        }
    }
    try {
        let messageId;
        switch (options.channel) {
            case 'whatsapp':
                messageId = await deliverToWhatsApp(options);
                break;
            case 'telegram':
                messageId = await deliverToTelegram(options);
                break;
            case 'webhook':
                messageId = await deliverToWebhook(options);
                break;
            case 'web':
                // For web widget, just log (real implementation would push via WebSocket)
                log.info({ tenantId: options.tenantId, message: options.message }, 'Web delivery (simulated)');
                messageId = `web-${Date.now()}`;
                break;
            case 'slack':
            case 'discord':
            case 'email':
                // Placeholder for future channels
                log.warn({ channel: options.channel }, 'Channel not yet implemented');
                return {
                    success: false,
                    channel: options.channel,
                    error: 'Channel not yet implemented',
                    deliveredAt: new Date().toISOString(),
                };
            default:
                throw new Error(`Unknown channel: ${options.channel}`);
        }
        // After the text lands, attach any files the task produced. Failures
        // here are logged but never fail the whole delivery (the text already
        // went out). WhatsApp/Telegram only; other channels ignore attachments.
        if (options.attachments?.length && (options.channel === 'whatsapp' || options.channel === 'telegram')) {
            for (const att of options.attachments) {
                try {
                    await deliverAttachment(options.channel, options.tenantId, options.to, att);
                    log.info({ tenantId: options.tenantId, channel: options.channel, file: att.path }, 'attachment delivered');
                }
                catch (aerr) {
                    log.warn({ tenantId: options.tenantId, file: att.path, err: aerr instanceof Error ? aerr.message : String(aerr) }, 'attachment delivery failed (text still sent)');
                }
            }
        }
        return {
            success: true,
            channel: options.channel,
            messageId,
            deliveredAt: new Date().toISOString(),
        };
    }
    catch (err) {
        const error = err instanceof Error ? err.message : String(err);
        log.error({
            tenantId: options.tenantId,
            channel: options.channel,
            error,
            duration: Date.now() - startTime,
        }, 'Delivery failed — enqueueing for retry');
        // Spool the failed delivery so the retry worker picks it up. Without
        // this, an outbound message lost to a transient WhatsApp socket
        // disconnect or webhook blip is gone forever — for B2B that means a
        // missed customer reply, missed dunning, missed escalation. The queue
        // gives us 5 retries with exponential backoff, then escalates.
        void enqueueFailedDelivery(options, error).catch(qerr => {
            log.error({ qerr: qerr.message }, 'failed to enqueue retry — message is truly lost');
        });
        return {
            success: false,
            channel: options.channel,
            error,
            deliveredAt: new Date().toISOString(),
        };
    }
}
async function enqueueFailedDelivery(options, error) {
    const { randomUUID } = await import('node:crypto');
    const { supabase } = await import('../utils/supabase.js');
    const now = new Date();
    const nextAttempt = new Date(now.getTime() + 30_000); // first retry in 30s
    await supabase.from('delivery_queue').insert({
        id: randomUUID(),
        tenant_id: options.tenantId,
        channel: options.channel,
        recipient: options.to,
        payload: JSON.stringify({ message: options.message, metadata: options.metadata ?? {} }),
        retry_count: 0,
        max_retries: 5,
        last_error: error.slice(0, 500),
        status: 'pending',
        next_attempt_at: nextAttempt.toISOString(),
        created_at: now.toISOString(),
        updated_at: now.toISOString(),
    });
}
/**
 * Deliver to WhatsApp
 */
async function deliverToWhatsApp(options) {
    // (Round-6 fix) Was returning a "fallback id" on error → outer
    // deliverMessage saw success=true even though no message was sent.
    // Now throws so the outer try/catch spools to the retry queue.
    //
    // (v2.5.2) Route through Baileys when no valid Cloud-API token is
    // configured. Real-world test caught delivery hammering the
    // demo_phone_id endpoint with `Bearer ` + empty token, getting
    // 401 OAuthException repeatedly. Now: prefer Baileys session,
    // fall back to Cloud API only when Cloud env is set.
    const cloudReady = Boolean(process.env.WHATSAPP_ACCESS_TOKEN &&
        process.env.WHATSAPP_PHONE_NUMBER_ID &&
        process.env.WHATSAPP_PHONE_NUMBER_ID !== 'demo_phone_id');
    // Try Baileys first if a tenant session is connected for this tenantId
    if (options.tenantId) {
        try {
            const { isWhatsAppConnected, sendBaileysMessage } = await import('../channels/whatsapp/baileys-adapter.js');
            if (isWhatsAppConnected?.(options.tenantId)) {
                const id = await sendBaileysMessage(options.tenantId, options.to, options.message);
                if (id)
                    return id;
            }
        }
        catch (err) {
            log.debug({ err: err.message }, 'baileys send not available — trying Cloud API');
        }
    }
    if (!cloudReady) {
        throw new Error('WhatsApp delivery unavailable: no Baileys session connected AND WHATSAPP_ACCESS_TOKEN/PHONE_NUMBER_ID not set');
    }
    try {
        const result = await whatsappClient.sendText(options.to, options.message);
        const id = result?.messages?.[0]?.id;
        if (!id)
            throw new Error('WhatsApp send returned no message id (likely client not ready)');
        return id;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        log.warn({ error: msg }, 'WhatsApp delivery failed — propagating to retry queue');
        throw new Error(`WhatsApp delivery failed: ${msg}`);
    }
}
/**
 * Deliver to Telegram. `options.to` is the Telegram chat_id captured from the
 * originating conversation. Throws on failure so the outer try/catch spools the
 * message to the retry queue — same durability guarantee as WhatsApp, so a
 * background task's result is never silently lost for a Telegram customer.
 */
async function deliverToTelegram(options) {
    if (!options.tenantId)
        throw new Error('Telegram delivery requires tenantId');
    if (!options.to)
        throw new Error('Telegram delivery requires a chat_id (options.to)');
    const { sendTelegramMessage } = await import('../channels/telegram/adapter.js');
    await sendTelegramMessage(options.tenantId, options.to, options.message);
    return `telegram-${Date.now()}`;
}
/**
 * Deliver to webhook
 */
async function deliverToWebhook(options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000);
    try {
        const response = await fetch(options.to, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                tenant_id: options.tenantId,
                message: options.message,
                metadata: options.metadata,
                delivered_at: new Date().toISOString(),
            }),
            signal: controller.signal,
        });
        clearTimeout(timeoutId);
        if (!response.ok) {
            throw new Error(`Webhook returned ${response.status}: ${await response.text()}`);
        }
        return `webhook-${Date.now()}`;
    }
    catch (err) {
        clearTimeout(timeoutId);
        throw err;
    }
}
/**
 * Push a message to a customer on THEIR originating channel (WhatsApp/Telegram).
 *
 * This is what makes background work — subagents and scheduled tasks — actually
 * "come back" to the user instead of only landing in the web dashboard. The
 * customer row already records the channel + address (phone for WhatsApp,
 * tg-<id> / metadata.telegram_user_id for Telegram), so we resolve it here and
 * deliver. Web/system customers return null (the dashboard surfaces those via
 * polling — no external push needed). Failures spool to the retry queue inside
 * deliverMessage, so a result is never silently lost.
 *
 * opts.skipChannel: if the resolved channel equals this, skip (used to avoid a
 * double message when a legacy announce path already delivered to that channel).
 */
export async function deliverToCustomerChannel(tenantId, customerId, message, opts = {}) {
    if (!tenantId || !customerId || !message)
        return null;
    const { supabase } = await import('../utils/supabase.js');
    const { data: cust } = await supabase
        .from('customers')
        .select('phone, metadata')
        .eq('id', customerId)
        .maybeSingle();
    if (!cust)
        return null;
    let meta = cust.metadata;
    if (typeof meta === 'string') {
        try {
            meta = JSON.parse(meta);
        }
        catch {
            meta = {};
        }
    }
    meta = meta || {};
    const phone = String(cust.phone || '');
    let channel = meta.channel;
    let to = null;
    if (channel === 'telegram' || phone.startsWith('tg-')) {
        channel = 'telegram';
        to = meta.telegram_user_id != null ? String(meta.telegram_user_id) : phone.replace(/^tg-/, '');
    }
    else if (channel === 'whatsapp' || /^\+?\d{6,}$/.test(phone)) {
        channel = 'whatsapp';
        to = phone;
    }
    else {
        // web / system / unknown → dashboard handles surfacing; nothing to push.
        return null;
    }
    if (!to || (opts.skipChannel && channel === opts.skipChannel))
        return null;
    return deliverMessage({
        tenantId,
        to,
        channel,
        message,
        // Auto-attach any files the background task produced (subagent output,
        // scheduled-job artifacts) so "send what it creates" works on any channel.
        attachments: opts.attachments,
        // Owner-facing notification: bypass the customer-outreach rate limiter
        // (these are the owner's own results coming back, not cold outreach).
        metadata: { audience: 'owner', bypassRateLimit: true },
    });
}
// Infer a mime type from a file extension when a tool didn't supply one.
function mimeFromPath(p) {
    const ext = (p.split('.').pop() || '').toLowerCase();
    const map = {
        pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
        webp: 'image/webp', gif: 'image/gif', mp4: 'video/mp4', mp3: 'audio/mpeg',
        wav: 'audio/wav', csv: 'text/csv', txt: 'text/plain', md: 'text/markdown',
        json: 'application/json', html: 'text/html', htm: 'text/html', zip: 'application/zip',
        xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        xls: 'application/vnd.ms-excel', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    };
    return map[ext] || 'application/octet-stream';
}
// Send one already-created file to a customer on a given channel. Best-effort:
// resolves ~, infers mime, dispatches to the channel's file-send helper.
async function deliverAttachment(channel, tenantId, to, att) {
    const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
    const path = att.path?.startsWith('~/') && home ? att.path.replace(/^~/, home) : att.path;
    if (!path)
        return;
    const mime = att.mimeType || mimeFromPath(path);
    if (channel === 'telegram') {
        const { sendTelegramFile } = await import('../channels/telegram/adapter.js');
        await sendTelegramFile(tenantId, to, path, mime);
    }
    else if (channel === 'whatsapp') {
        const { sendBaileysFile } = await import('../channels/whatsapp/baileys-adapter.js');
        await sendBaileysFile(tenantId, to, path, mime);
    }
}
/**
 * Broadcast to multiple channels
 */
export async function broadcastMessage(tenantId, message, channels, recipient) {
    const results = await Promise.all(channels.map(channel => deliverMessage({
        tenantId,
        to: recipient || '',
        message,
        channel: channel,
    })));
    const successCount = results.filter(r => r.success).length;
    log.info({ tenantId, channels, successCount }, 'Broadcast completed');
    return results;
}
/**
 * Announce to tenant's primary channel
 */
export async function announceToTenant(tenant, message, channel) {
    const targetChannel = channel || 'whatsapp';
    return deliverMessage({
        tenantId: tenant.id,
        to: tenant.whatsapp_phone,
        message,
        channel: targetChannel,
    });
}
//# sourceMappingURL=delivery.js.map