// engine/outbound-webhook-dispatcher.ts — Pure HTTP delivery engine with HMAC + retry (SPEC-281)
// This module has NO storage imports — dependency injection via callbacks.
import { createHmac } from 'node:crypto';
// ---------------------------------------------------------------------------
// HMAC signature
// ---------------------------------------------------------------------------
function signPayload(secret, body) {
    return `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;
}
// ---------------------------------------------------------------------------
// Single HTTP attempt
// ---------------------------------------------------------------------------
async function attemptDelivery(url, body, signature) {
    const start = Date.now();
    const controller = new AbortController();
    const timeout = setTimeout(() => {
        controller.abort();
    }, 10_000);
    try {
        const res = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Planu-Signature': signature,
                'User-Agent': 'Planu-Webhook/1.0',
            },
            body,
            signal: controller.signal,
        });
        clearTimeout(timeout);
        return { statusCode: res.status, latencyMs: Date.now() - start, success: res.ok };
    }
    catch (err) {
        clearTimeout(timeout);
        const message = err instanceof Error ? err.message : String(err);
        return { statusCode: 0, latencyMs: Date.now() - start, success: false, error: message };
    }
}
// ---------------------------------------------------------------------------
// Retry logic: 3 attempts with exponential backoff (1s, 5s, 30s)
// ---------------------------------------------------------------------------
const RETRY_DELAYS_MS = [1_000, 5_000, 30_000];
async function deliverWithRetry(webhook, body, payload, onDelivery) {
    let lastResult = null;
    const signature = signPayload(webhook.secret, body);
    for (let attempt = 0; attempt < 3; attempt++) {
        if (attempt > 0) {
            const delay = RETRY_DELAYS_MS[attempt - 1] ?? 1_000;
            await new Promise((resolve) => setTimeout(resolve, delay));
        }
        lastResult = await attemptDelivery(webhook.url, body, signature);
        if (lastResult.success) {
            break;
        }
    }
    if (lastResult !== null) {
        const deliveryRecord = {
            webhookId: webhook.id,
            deliveredAt: new Date().toISOString(),
            event: payload.event,
            statusCode: lastResult.statusCode,
            latencyMs: lastResult.latencyMs,
            success: lastResult.success,
            ...(lastResult.error !== undefined ? { error: lastResult.error } : {}),
        };
        await onDelivery(deliveryRecord).catch(() => {
            /* best-effort — never throw */
        });
    }
}
// ---------------------------------------------------------------------------
// Core dispatch (pure — no storage deps)
// ---------------------------------------------------------------------------
export async function dispatchToWebhooks(webhooks, payload, onDelivery) {
    if (webhooks.length === 0) {
        return;
    }
    const body = JSON.stringify(payload);
    await Promise.allSettled(webhooks.map((webhook) => deliverWithRetry(webhook, body, payload, onDelivery)));
}
// ---------------------------------------------------------------------------
// Helper to build spec lifecycle payloads
// ---------------------------------------------------------------------------
export function buildSpecLifecyclePayload(event, specId, specTitle, status, projectPath) {
    return {
        event,
        specId,
        specTitle,
        status,
        timestamp: new Date().toISOString(),
        projectPath,
    };
}
//# sourceMappingURL=outbound-webhook-dispatcher.js.map