import { defer } from "@virtualstate/promise";
import { isLike, isPromise, isSignalled, ok } from "../is.js";
import { fromDurableRequest, fromRequestResponse, setDurableRequestForEvent } from "../data/index.js";
import { dispatcher } from "../events/schedule/schedule.js";
import { v4 } from "uuid";
import { caches } from "./cache.js";
import { dispatchEvent } from "../events/schedule/event.js";
import { getConfig } from "../config/index.js";
import { getServiceWorkerModuleExports } from "../worker/service-worker/worker-exports.js";
export function isDurableFetchEventCache(value) {
    return !!(isLike(value) &&
        typeof value.name === "string");
}
function isRespondWith(event) {
    return (isLike(event) &&
        typeof event.respondWith === "function" &&
        !!event.handled);
}
function isWaitUntil(event) {
    return (isLike(event) &&
        typeof event.waitUntil === "function");
}
export function createRespondWith(event) {
    if (isRespondWith(event)) {
        return event;
    }
    const { promise, resolve, reject } = defer();
    function respondWith(response) {
        if (isPromise(response)) {
            return response.then(resolve, reject);
        }
        return resolve(response);
    }
    return {
        promise,
        handled: promise.then(() => undefined),
        respondWith
    };
}
export function createWaitUntil(event) {
    if (isWaitUntil(event)) {
        return event;
    }
    let promises = [];
    function waitUntil(promise) {
        promises.push(promise.catch(error => void error));
    }
    async function wait() {
        if (!promises.length) {
            return;
        }
        const current = promises;
        promises = [];
        await Promise.all(current);
        return wait();
    }
    return {
        wait,
        waitUntil
    };
}
function createSignal(event) {
    if (isSignalled(event)) {
        return event;
    }
    const controller = new AbortController();
    return {
        signal: controller.signal,
        controller
    };
}
export function isDurableFetchEventData(event) {
    return !!(isLike(event) &&
        event.type === "fetch" &&
        event.request &&
        event.request.url);
}
async function onFetchResponse(event, request, response) {
    let durableEventDispatch;
    if (event.dispatch) {
        let durableEventDispatchData;
        if (typeof event.dispatch === "string") {
            durableEventDispatchData = {
                type: event.dispatch
            };
        }
        else {
            durableEventDispatchData = event.dispatch;
        }
        durableEventDispatch = {
            durableEventId: v4(),
            ...durableEventDispatchData
        };
    }
    const isRetain = durableEventDispatch || (event.durableEventId && event.retain !== false);
    let body;
    const givenCache = typeof event.cache === "string" ? { name: event.cache } : isDurableFetchEventCache(event.cache) ? event.cache : undefined;
    const cache = givenCache ?? (isRetain ? { name: "fetch" } : undefined);
    // cache name has some special cases:
    // type RequestCache = "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
    if (cache && cache.name !== "no-store" && cache.name !== "only-if-cached") {
        const { name, always } = cache;
        const isForceCache = name === "force-cache";
        if (response.ok || (always || isForceCache)) {
            let cacheName = name;
            if (isForceCache || name === "reload") {
                cacheName = "default";
            }
            const store = await caches.open(cacheName);
            await store.put(request, response);
            body = {
                type: "cache",
                value: name,
                url: request.url
            };
        }
    }
    let durableRequest;
    if (isRetain) {
        const durableRequestData = await fromRequestResponse(request, response, {
            body
        });
        durableRequest = await setDurableRequestForEvent(durableRequestData, durableEventDispatch || event);
        if (durableEventDispatch) {
            const { response, ...request } = durableRequest;
            await dispatchEvent({
                ...durableEventDispatch,
                request,
                response
            });
        }
    }
    const { response: givenFns } = getConfig();
    const responseFns = Array.isArray(givenFns) ? givenFns : (givenFns ? [givenFns] : []);
    if (responseFns.length) {
        await Promise.all(responseFns.map(async (fn) => fn(response.clone(), request, durableRequest)));
    }
}
export const removeFetchDispatcherFunction = dispatcher("fetch", async (event, dispatch) => {
    const { signal, controller } = createSignal(event);
    ok(isDurableFetchEventData(event));
    const { promise, handled, respondWith } = createRespondWith(event);
    const { wait, waitUntil } = createWaitUntil(event);
    let request;
    try {
        request = await fromDurableRequest(event.request);
    }
    catch (error) {
        throw new Error("Could not create request from event");
    }
    try {
        const requestEvent = {
            ...event,
            signal,
            request,
            handled,
            respondWith,
            waitUntil
        };
        const entrypointArguments = event.entrypointArguments;
        async function dispatchServiceWorkerFnRequest(fn, fnError = "Expected entrypoint to be a function") {
            ok(typeof fn === "function", fnError);
            let returned;
            if (entrypointArguments) {
                const requestArguments = entrypointArguments.map(key => key === "$event" ? requestEvent : requestEvent[key]);
                returned = fn(...requestArguments);
            }
            else {
                ok(fn, fnError);
                returned = fn(request, requestEvent);
            }
            if (isLike(returned) && typeof returned === "object" && "then" in returned) {
                waitUntil(returned);
                returned = await returned;
            }
            if (returned instanceof Response) {
                respondWith(returned);
            }
        }
        const serviceWorker = getServiceWorkerModuleExports();
        if (event.entrypoint) {
            const entrypoint = serviceWorker[event.entrypoint];
            if (!entrypoint) {
                const names = Object.keys(serviceWorker);
                throw new Error(`Unknown entrypoint ${event.entrypoint}, expected one of ${names.join(", ")}`);
            }
            await dispatchServiceWorkerFnRequest(entrypoint);
        }
        else if (typeof serviceWorker.fetch === "function") {
            await dispatchServiceWorkerFnRequest(serviceWorker.fetch);
        }
        else if (typeof serviceWorker.default === "function") {
            await dispatchServiceWorkerFnRequest(serviceWorker.default);
        }
        else if (isLike(serviceWorker.default) && typeof serviceWorker.default.fetch === "function") {
            await dispatchServiceWorkerFnRequest(serviceWorker.default.fetch);
        }
        else {
            await dispatch(requestEvent);
        }
        // We may not get a response as it is being handled elsewhere
        if (promise) {
            const response = await promise;
            await onFetchResponse(event, request, response);
        }
        else {
            await handled;
        }
        if (!signal.aborted) {
            await wait?.();
        }
    }
    catch (error) {
        if (!signal.aborted) {
            controller?.abort(error);
        }
        await onFetchResponse(event, request, Response.error());
    }
    finally {
        if (!signal.aborted) {
            await wait?.();
            controller?.abort();
        }
    }
});
//# sourceMappingURL=dispatch.js.map