/**
 * Defer work past the HTTP response.
 *
 * Use for bookkeeping that doesn't need to complete before the client
 * gets bytes — writes that record state, maintenance queries, cache
 * refreshes. `after()` hands the promise to the host's lifetime
 * extender when one is available (Cloudflare's `waitUntil` under
 * workerd), or fires-and-forgets on Node (the process lives for the
 * next request anyway).
 *
 * Host binding is resolved lazily via a dynamic import of the
 * `virtual:emdash/wait-until` virtual module. Lazy — rather than a
 * static top-level import — so tools that walk the dist in a plain
 * Node loader (`astro check`, Vitest, etc.) don't trip over the
 * `virtual:` scheme: they'd only fail if they actually called
 * `after()`, which they don't during type-checking.
 */

export type WaitUntilFn = (promise: Promise<unknown>) => void;

// Resolves to the host's waitUntil if the adapter provided one, or
// null otherwise. Kicked off once at module load; subsequent `after()`
// calls see the cached result without re-importing.
const waitUntilReady: Promise<WaitUntilFn | null> = (async () => {
	try {
		// @ts-ignore - virtual module, generated by the Astro integration
		const mod = (await import("virtual:emdash/wait-until")) as {
			waitUntil?: WaitUntilFn;
		};
		return mod.waitUntil ?? null;
	} catch {
		// No virtual module available (Node-side tooling, tests without the
		// integration in scope). Fire-and-forget is the safe fallback.
		return null;
	}
})();
// Surface rejections without making the module-load fail.
waitUntilReady.catch(() => {});

/**
 * Schedule `fn` to run without blocking the response.
 *
 * Errors are caught and logged — a deferred task should never surface
 * as an unhandled rejection because the response is long gone. Callers
 * that care about errors should handle them inside `fn`.
 */
export function after(fn: () => void | Promise<void>): void {
	const promise = Promise.resolve()
		.then(fn)
		.catch((error) => {
			console.error("[emdash] deferred task failed:", error);
		});

	// Defer the lifetime-extender handoff to the microtask that resolves
	// waitUntilReady. On workerd this is effectively instant (the virtual
	// module is already loaded in the bundle); on Node the promise
	// resolves to null, so this is just one extra microtask and no-op.
	void waitUntilReady.then((waitUntil) => {
		if (waitUntil) waitUntil(promise);
		return null;
	});
}
