export function createGoodVibesCloudflareWorker(options = {}) {
    return {
        async fetch(request, env, _ctx) {
            const url = new URL(request.url);
            if (url.pathname === '/health' || url.pathname === '/batch/health') {
                return json({ ok: true, service: 'goodvibes-cloudflare-worker' });
            }
            const authError = requireWorkerAuth(request, env, options);
            if (authError)
                return authError;
            if (url.pathname === '/batch/tick/enqueue' && request.method === 'POST') {
                const queue = env.GOODVIBES_BATCH_QUEUE;
                if (!queue)
                    return json({ error: 'GOODVIBES_BATCH_QUEUE is not bound', code: 'QUEUE_NOT_CONFIGURED' }, 503);
                const body = await optionalJson(request);
                if (body instanceof Response)
                    return body;
                await queue.send({
                    type: 'batch.tick',
                    force: toRecord(body)['force'] === true,
                    enqueuedAt: Date.now(),
                });
                return json({ queued: true }, 202);
            }
            const daemonPath = toDaemonBatchPath(url.pathname);
            if (!daemonPath)
                return json({ error: 'Not found', code: 'NOT_FOUND' }, 404);
            if (url.pathname === '/batch/jobs/enqueue' && request.method === 'POST') {
                if (!options.queueJobPayloads && env.GOODVIBES_QUEUE_JOB_PAYLOADS !== 'true') {
                    return json({
                        error: 'Queueing full batch job payloads is disabled. Post /batch/jobs to proxy directly to the daemon, or enable queueJobPayloads explicitly.',
                        code: 'QUEUE_PAYLOADS_DISABLED',
                    }, 409);
                }
                const queue = env.GOODVIBES_BATCH_QUEUE;
                if (!queue)
                    return json({ error: 'GOODVIBES_BATCH_QUEUE is not bound', code: 'QUEUE_NOT_CONFIGURED' }, 503);
                const body = await optionalJson(request);
                if (body instanceof Response)
                    return body;
                await queue.send({
                    type: 'batch.job.create',
                    body: toRecord(body),
                    enqueuedAt: Date.now(),
                });
                return json({ queued: true }, 202);
            }
            return proxyDaemonBatch(request, env, options, daemonPath);
        },
        async queue(batch, env, _ctx) {
            for (const message of batch.messages) {
                try {
                    const response = await handleQueuePayload(message.body, env, options);
                    if (response.ok) {
                        message.ack?.();
                    }
                    else {
                        message.retry?.();
                    }
                }
                catch (error) {
                    // Warn rather than re-throw: a thrown error here would surface as an
                    // unhandled rejection. Calling retry() signals Cloudflare to re-deliver.
                    console.warn('GoodVibes worker queue message failed', { error });
                    message.retry?.();
                }
            }
        },
        async scheduled(_event, env) {
            await proxyDaemonJson(env, options, '/api/batch/tick', {
                method: 'POST',
                body: JSON.stringify({ force: false }),
            });
        },
    };
}
async function handleQueuePayload(payload, env, options) {
    if (payload.type === 'batch.tick') {
        return proxyDaemonJson(env, options, '/api/batch/tick', {
            method: 'POST',
            body: JSON.stringify({ force: payload.force === true }),
        });
    }
    return proxyDaemonJson(env, options, '/api/batch/jobs', {
        method: 'POST',
        body: JSON.stringify({
            ...payload.body,
            source: {
                kind: 'cloudflare-queue',
                id: typeof payload.body['id'] === 'string' ? payload.body['id'] : undefined,
            },
        }),
    });
}
async function proxyDaemonBatch(request, env, options, daemonPath) {
    // The caller's Authorization header is intentionally dropped here: it carries
    // the worker-to-worker auth token (GOODVIBES_WORKER_TOKEN), NOT the daemon
    // operator token. proxyDaemonJson always attaches the correct operator token
    // from GOODVIBES_OPERATOR_TOKEN / options.authToken before forwarding.
    if (request.method !== 'GET' && request.method !== 'HEAD') {
        const contentType = request.headers.get('content-type') ?? '';
        if (contentType && !contentType.toLowerCase().includes('application/json')) {
            return json({ error: 'Only application/json batch payloads are supported', code: 'UNSUPPORTED_MEDIA_TYPE' }, 415);
        }
    }
    const body = request.method === 'GET' || request.method === 'HEAD'
        ? undefined
        : await readTextBodyWithinLimit(request, options.maxRequestBodyBytes);
    if (body instanceof Response)
        return body;
    return proxyDaemonJson(env, options, daemonPath, {
        method: request.method,
        body,
        search: new URL(request.url).search,
    });
}
async function proxyDaemonJson(env, options, path, init) {
    const baseUrl = resolveDaemonUrl(env, options);
    if (!baseUrl) {
        return json({ error: 'GOODVIBES_DAEMON_URL is not configured', code: 'DAEMON_URL_REQUIRED' }, 503);
    }
    const headers = new Headers();
    if (init.body !== undefined)
        headers.set('Content-Type', 'application/json');
    const token = options.authToken ?? env.GOODVIBES_OPERATOR_TOKEN ?? '';
    if (token)
        headers.set('Authorization', `Bearer ${token}`);
    return fetch(`${baseUrl}${path}${init.search ?? ''}`, {
        method: init.method,
        headers,
        body: init.body ?? null,
    });
}
function toDaemonBatchPath(pathname) {
    if (pathname.startsWith('/api/batch'))
        return pathname;
    if (pathname.startsWith('/batch/'))
        return `/api${pathname}`;
    if (pathname === '/batch')
        return '/api/batch';
    return null;
}
/** Constant-time string comparison to prevent timing attacks on the worker auth token. */
function constantTimeEqual(a, b) {
    if (a.length !== b.length)
        return false;
    let result = 0;
    for (let i = 0; i < a.length; i++) {
        result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }
    return result === 0;
}
function requireWorkerAuth(request, env, options) {
    const expected = options.workerAuthToken ?? env.GOODVIBES_WORKER_TOKEN ?? '';
    // allowUnauthenticated bypasses token checks for the entire worker — use only
    // when the worker sits behind a trusted network perimeter (e.g. private VPC).
    if (!expected && options.allowUnauthenticated === true)
        return null;
    if (!expected) {
        return json({
            error: 'GOODVIBES_WORKER_TOKEN is required for non-health endpoints',
            code: 'WORKER_AUTH_TOKEN_REQUIRED',
        }, 503);
    }
    const auth = request.headers.get('authorization') ?? '';
    if (constantTimeEqual(auth, `Bearer ${expected}`))
        return null;
    return json({ error: 'Worker authorization required', code: 'WORKER_AUTH_REQUIRED' }, 401);
}
function resolveDaemonUrl(env, options) {
    const raw = options.daemonUrl ?? env.GOODVIBES_DAEMON_URL ?? '';
    return raw.replace(/\/+$/, '');
}
async function optionalJson(request) {
    const text = await readTextBodyWithinLimit(request);
    if (text instanceof Response)
        return text;
    if (!text.trim())
        return {};
    try {
        return JSON.parse(text);
    }
    catch {
        return json({ error: 'Request body contains invalid JSON', code: 'INVALID_JSON' }, 400);
    }
}
async function readTextBodyWithinLimit(request, 
// Default cap: 1 MB. Override via GoodVibesCloudflareWorkerOptions.maxRequestBodyBytes.
// Cloudflare Workers enforce a separate hard limit on request body size that
// is independent of this value; this cap is applied before the body is decoded
// to prevent excessive memory use.
maxBytes = 1_000_000) {
    const contentLength = Number.parseInt(request.headers.get('content-length') ?? '0', 10);
    if (Number.isFinite(contentLength) && contentLength > maxBytes) {
        return json({ error: 'Payload too large', code: 'PAYLOAD_TOO_LARGE' }, 413);
    }
    if (!request.body)
        return '';
    const reader = request.body.getReader();
    const decoder = new TextDecoder();
    let total = 0;
    let body = '';
    try {
        for (;;) {
            const { done, value } = await reader.read();
            if (done)
                break;
            total += value.byteLength;
            if (total > maxBytes) {
                try {
                    await reader.cancel('Payload too large');
                }
                catch (error) {
                    console.warn('GoodVibes Worker request body cancel failed after size limit', error);
                }
                return json({ error: 'Payload too large', code: 'PAYLOAD_TOO_LARGE' }, 413);
            }
            body += decoder.decode(value, { stream: true });
        }
        body += decoder.decode();
        return body;
    }
    finally {
        reader.releaseLock();
    }
}
// CORS is intentionally absent from this helper: the worker is designed for
// server-to-server calls (from your backend to the daemon proxy). If you need
// browser CORS support, add an `Access-Control-Allow-Origin` header via
// middleware or a Cloudflare Transform Rule at your infrastructure layer.
function json(body, status = 200) {
    return new Response(JSON.stringify(body), {
        status,
        headers: { 'Content-Type': 'application/json' },
    });
}
function toRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value)
        ? value
        : {};
}
