import { getDB } from '../db.js';
import { validateWebhookUrl } from '../security/webhook.js';
import { safeFetch, SsrfBlockedError } from '../security/safe-fetch.js';
import { encryptSecret, decryptSecret } from '../security/secret-storage.js';
const RETRY_DELAYS_MS = [0, 30_000, 120_000, 600_000];
const DELIVERY_TIMEOUT_MS = 10_000;
/** Sign a webhook payload using HMAC-SHA256. */
export async function signPayload(payload, secret) {
    const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
    const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
    return Array.from(new Uint8Array(signature))
        .map((b) => b.toString(16).padStart(2, '0'))
        .join('');
}
/** Verify an incoming webhook signature. */
export async function verifySignature(payload, signature, secret) {
    const expected = await signPayload(payload, secret);
    if (expected.length !== signature.length)
        return false;
    let mismatch = 0;
    for (let i = 0; i < expected.length; i++) {
        mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
    }
    return mismatch === 0;
}
// ---------------------------------------------------------------------------
// Endpoint CRUD (DB-backed)
// ---------------------------------------------------------------------------
/** Create a new webhook endpoint. Validates URL against SSRF rules. */
export async function createEndpoint(input) {
    const urlCheck = validateWebhookUrl(input.url);
    if (!urlCheck.valid) {
        throw new Error(`Invalid webhook URL: ${urlCheck.error}`);
    }
    const db = getDB();
    return db.webhookEndpoint.create({
        data: {
            url: input.url,
            events: input.events,
            secret: await encryptSecret(input.secret),
            name: input.name ?? null,
            active: input.active ?? true,
        },
    });
}
/** List all webhook endpoints. */
export async function listEndpoints() {
    const db = getDB();
    return db.webhookEndpoint.findMany({ orderBy: { createdAt: 'desc' } });
}
/** Update a webhook endpoint. Validates URL against SSRF rules if URL is changed. */
export async function updateEndpoint(id, data) {
    if (data.url !== undefined) {
        const urlCheck = validateWebhookUrl(data.url);
        if (!urlCheck.valid) {
            throw new Error(`Invalid webhook URL: ${urlCheck.error}`);
        }
    }
    const db = getDB();
    const encryptedSecret = data.secret !== undefined ? await encryptSecret(data.secret) : undefined;
    return db.webhookEndpoint.update({
        where: { id },
        data: {
            ...(data.url !== undefined ? { url: data.url } : {}),
            ...(data.events !== undefined ? { events: data.events } : {}),
            ...(encryptedSecret !== undefined ? { secret: encryptedSecret } : {}),
            ...(data.name !== undefined ? { name: data.name } : {}),
            ...(data.active !== undefined ? { active: data.active } : {}),
        },
    });
}
/** Delete a webhook endpoint and all its delivery logs. */
export async function deleteEndpoint(id) {
    const db = getDB();
    return db.webhookEndpoint.delete({ where: { id } });
}
/** Get recent deliveries for an endpoint. */
export async function getDeliveries(endpointId, limit = 50) {
    const db = getDB();
    return db.webhookDeliveryLog.findMany({
        where: { endpointId },
        orderBy: { createdAt: 'desc' },
        take: limit,
    });
}
// ---------------------------------------------------------------------------
// Event dispatch (DB-persisted with async delivery)
// ---------------------------------------------------------------------------
/**
 * Dispatch a webhook event: finds matching endpoints, creates delivery records,
 * and attempts immediate delivery. Failed deliveries are queued for retry.
 */
export async function dispatch(event, data) {
    const db = getDB();
    const endpoints = await db.webhookEndpoint.findMany({
        where: { active: true },
    });
    const matching = endpoints.filter((ep) => {
        const events = ep.events;
        return events.includes(event);
    });
    if (matching.length === 0) {
        return { queued: 0, delivered: 0, failed: 0 };
    }
    const payload = {
        event,
        timestamp: new Date().toISOString(),
        data,
    };
    let delivered = 0;
    let failed = 0;
    for (const ep of matching) {
        const delivery = await db.webhookDeliveryLog.create({
            data: {
                endpointId: ep.id,
                event,
                payload,
                status: 'pending',
                attempts: 0,
                maxAttempts: 3,
            },
        });
        let signingSecret;
        try {
            signingSecret = await decryptSecret(ep.secret);
        }
        catch (err) {
            console.error(`[actuate][webhook] Failed to decrypt secret for endpoint ${ep.id}:`, err instanceof Error ? err.message : err);
            failed++;
            continue;
        }
        const result = await attemptDelivery(delivery.id, ep.url, signingSecret, payload);
        if (result) {
            delivered++;
        }
        else {
            failed++;
        }
    }
    return { queued: matching.length, delivered, failed };
}
/**
 * Attempt a single delivery. Updates the delivery record with result.
 * On failure, schedules a retry if attempts remain.
 */
async function attemptDelivery(deliveryId, url, secret, payload) {
    const db = getDB();
    const body = JSON.stringify(payload);
    try {
        const signature = await signPayload(body, secret);
        const response = await safeFetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Actuate-Signature': signature,
                'X-Actuate-Event': payload.event,
                'X-Actuate-Delivery': deliveryId,
                'User-Agent': 'ActuateCMS-Webhook/1.0',
            },
            body,
            timeoutMs: DELIVERY_TIMEOUT_MS,
        });
        const responseBody = await response.text().catch(() => '');
        if (response.ok) {
            await db.webhookDeliveryLog.update({
                where: { id: deliveryId },
                data: {
                    status: 'success',
                    attempts: { increment: 1 },
                    lastAttemptAt: new Date(),
                    responseStatus: response.status,
                    responseBody: responseBody.substring(0, 1000),
                    nextRetryAt: null,
                },
            });
            return true;
        }
        return handleFailure(deliveryId, response.status, responseBody);
    }
    catch (err) {
        if (err instanceof SsrfBlockedError) {
            return handleFailure(deliveryId, 0, err.message);
        }
        console.error(`[actuate][webhook] Delivery ${deliveryId} failed:`, err?.message ?? 'Connection failed');
        return handleFailure(deliveryId, 0, err?.message ?? 'Connection failed');
    }
}
async function handleFailure(deliveryId, responseStatus, responseBody) {
    const db = getDB();
    const delivery = await db.webhookDeliveryLog.findUnique({
        where: { id: deliveryId },
    });
    const attempt = (delivery?.attempts ?? 0) + 1;
    const maxAttempts = delivery?.maxAttempts ?? 3;
    if (attempt >= maxAttempts) {
        console.error(`[actuate][webhook] Delivery ${deliveryId} permanently failed after ${attempt} attempts (status: ${responseStatus})`);
        await db.webhookDeliveryLog.update({
            where: { id: deliveryId },
            data: {
                status: 'failed',
                attempts: attempt,
                lastAttemptAt: new Date(),
                responseStatus,
                responseBody: responseBody.substring(0, 1000),
                nextRetryAt: null,
            },
        });
    }
    else {
        const delay = RETRY_DELAYS_MS[attempt] ?? 600_000;
        await db.webhookDeliveryLog.update({
            where: { id: deliveryId },
            data: {
                status: 'retrying',
                attempts: attempt,
                lastAttemptAt: new Date(),
                responseStatus,
                responseBody: responseBody.substring(0, 1000),
                nextRetryAt: new Date(Date.now() + delay),
            },
        });
    }
    return false;
}
// ---------------------------------------------------------------------------
// Retry processor (called from cron or scheduled task)
// ---------------------------------------------------------------------------
/**
 * Process pending retries. Call this from a cron job (e.g. every 1-2 minutes).
 * Finds deliveries with status='retrying' and nextRetryAt <= now.
 */
export async function processRetries() {
    const db = getDB();
    const pending = await db.webhookDeliveryLog.findMany({
        where: {
            status: 'retrying',
            nextRetryAt: { lte: new Date() },
        },
        include: { endpoint: true },
        take: 50,
    });
    let succeeded = 0;
    let failed = 0;
    for (const delivery of pending) {
        const payload = delivery.payload;
        const result = await attemptDelivery(delivery.id, delivery.endpoint.url, delivery.endpoint.secret, payload);
        if (result) {
            succeeded++;
        }
        else {
            failed++;
        }
    }
    return { retried: pending.length, succeeded, failed };
}
//# sourceMappingURL=index.js.map