import { ACTION, RSC, RSC_CONTENT_TYPE_HEADER } from "../../client/components/app-router-headers";
import { isNotFoundError } from "../../client/components/not-found";
import { getURLFromRedirectError, isRedirectError } from "../../client/components/redirect";
import RenderResult from "../render-result";
import { FlightRenderResult } from "./flight-render-result";
import { filterReqHeaders, actionsForbiddenHeaders } from "../lib/server-ipc/utils";
import { appendMutableCookies, getModifiedCookieValues } from "../web/spec-extension/adapters/request-cookies";
import { NEXT_CACHE_REVALIDATED_TAGS_HEADER, NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER } from "../../lib/constants";
function nodeToWebReadableStream(nodeReadable) {
    if (process.env.NEXT_RUNTIME !== "edge") {
        const { Readable  } = require("stream");
        if ("toWeb" in Readable && typeof Readable.toWeb === "function") {
            return Readable.toWeb(nodeReadable);
        }
        return new ReadableStream({
            start (controller) {
                nodeReadable.on("data", (chunk)=>{
                    controller.enqueue(chunk);
                });
                nodeReadable.on("end", ()=>{
                    controller.close();
                });
                nodeReadable.on("error", (error)=>{
                    controller.error(error);
                });
            }
        });
    } else {
        throw new Error("Invalid runtime");
    }
}
function formDataFromSearchQueryString(query) {
    const searchParams = new URLSearchParams(query);
    const formData = new FormData();
    for (const [key, value] of searchParams){
        formData.append(key, value);
    }
    return formData;
}
function nodeHeadersToRecord(headers) {
    const record = {};
    for (const [key, value] of Object.entries(headers)){
        if (value !== undefined) {
            record[key] = Array.isArray(value) ? value.join(", ") : `${value}`;
        }
    }
    return record;
}
function getForwardedHeaders(req, res) {
    // Get request headers and cookies
    const requestHeaders = req.headers;
    const requestCookies = requestHeaders["cookie"] ?? "";
    // Get response headers and Set-Cookie header
    const responseHeaders = res.getHeaders();
    const rawSetCookies = responseHeaders["set-cookie"];
    const setCookies = (Array.isArray(rawSetCookies) ? rawSetCookies : [
        rawSetCookies
    ]).map((setCookie)=>{
        // remove the suffixes like 'HttpOnly' and 'SameSite'
        const [cookie] = `${setCookie}`.split(";");
        return cookie;
    });
    // Merge request and response headers
    const mergedHeaders = filterReqHeaders({
        ...nodeHeadersToRecord(requestHeaders),
        ...nodeHeadersToRecord(responseHeaders)
    }, actionsForbiddenHeaders);
    // Merge cookies
    const mergedCookies = requestCookies.split("; ").concat(setCookies).join("; ");
    // Update the 'cookie' header with the merged cookies
    mergedHeaders["cookie"] = mergedCookies;
    // Remove headers that should not be forwarded
    delete mergedHeaders["transfer-encoding"];
    return new Headers(mergedHeaders);
}
async function addRevalidationHeader(res, { staticGenerationStore , requestStore  }) {
    var _staticGenerationStore_revalidatedTags;
    await Promise.all(staticGenerationStore.pendingRevalidates || []);
    // If a tag was revalidated, the client router needs to invalidate all the
    // client router cache as they may be stale. And if a path was revalidated, the
    // client needs to invalidate all subtrees below that path.
    // To keep the header size small, we use a tuple of
    // [[revalidatedPaths], isTagRevalidated ? 1 : 0, isCookieRevalidated ? 1 : 0]
    // instead of a JSON object.
    // TODO-APP: Currently the prefetch cache doesn't have subtree information,
    // so we need to invalidate the entire cache if a path was revalidated.
    // TODO-APP: Currently paths are treated as tags, so the second element of the tuple
    // is always empty.
    const isTagRevalidated = ((_staticGenerationStore_revalidatedTags = staticGenerationStore.revalidatedTags) == null ? void 0 : _staticGenerationStore_revalidatedTags.length) ? 1 : 0;
    const isCookieRevalidated = getModifiedCookieValues(requestStore.mutableCookies).length ? 1 : 0;
    res.setHeader("x-action-revalidated", JSON.stringify([
        [],
        isTagRevalidated,
        isCookieRevalidated
    ]));
}
async function createRedirectRenderResult(req, res, redirectUrl, staticGenerationStore) {
    res.setHeader("x-action-redirect", redirectUrl);
    // if we're redirecting to a relative path, we'll try to stream the response
    if (redirectUrl.startsWith("/")) {
        var _staticGenerationStore_incrementalCache;
        const forwardedHeaders = getForwardedHeaders(req, res);
        forwardedHeaders.set(RSC, "1");
        const host = req.headers["host"];
        const proto = ((_staticGenerationStore_incrementalCache = staticGenerationStore.incrementalCache) == null ? void 0 : _staticGenerationStore_incrementalCache.requestProtocol) || "https";
        const fetchUrl = new URL(`${proto}://${host}${redirectUrl}`);
        if (staticGenerationStore.revalidatedTags) {
            var _staticGenerationStore_incrementalCache1, _staticGenerationStore_incrementalCache_prerenderManifest, _staticGenerationStore_incrementalCache_prerenderManifest_preview;
            forwardedHeaders.set(NEXT_CACHE_REVALIDATED_TAGS_HEADER, staticGenerationStore.revalidatedTags.join(","));
            forwardedHeaders.set(NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, ((_staticGenerationStore_incrementalCache1 = staticGenerationStore.incrementalCache) == null ? void 0 : (_staticGenerationStore_incrementalCache_prerenderManifest = _staticGenerationStore_incrementalCache1.prerenderManifest) == null ? void 0 : (_staticGenerationStore_incrementalCache_prerenderManifest_preview = _staticGenerationStore_incrementalCache_prerenderManifest.preview) == null ? void 0 : _staticGenerationStore_incrementalCache_prerenderManifest_preview.previewModeId) || "");
        }
        // Ensures that when the path was revalidated we don't return a partial response on redirects
        // if (staticGenerationStore.pathWasRevalidated) {
        forwardedHeaders.delete("next-router-state-tree");
        // }
        try {
            const headResponse = await fetch(fetchUrl, {
                method: "HEAD",
                headers: forwardedHeaders,
                next: {
                    // @ts-ignore
                    internal: 1
                }
            });
            if (headResponse.headers.get("content-type") === RSC_CONTENT_TYPE_HEADER) {
                const response = await fetch(fetchUrl, {
                    method: "GET",
                    headers: forwardedHeaders,
                    next: {
                        // @ts-ignore
                        internal: 1
                    }
                });
                // copy the headers from the redirect response to the response we're sending
                for (const [key, value] of response.headers){
                    if (!actionsForbiddenHeaders.includes(key)) {
                        res.setHeader(key, value);
                    }
                }
                return new FlightRenderResult(response.body);
            }
        } catch (err) {
            // we couldn't stream the redirect response, so we'll just do a normal redirect
            console.error(`failed to get redirect response`, err);
        }
    }
    return new RenderResult(JSON.stringify({}));
}
export async function handleAction({ req , res , ComponentMod , page , serverActionsManifest , generateFlight , staticGenerationStore , requestStore , serverActionsBodySizeLimit  }) {
    let actionId = req.headers[ACTION.toLowerCase()];
    const contentType = req.headers["content-type"];
    const isURLEncodedAction = req.method === "POST" && contentType === "application/x-www-form-urlencoded";
    const isMultipartAction = req.method === "POST" && (contentType == null ? void 0 : contentType.startsWith("multipart/form-data"));
    const isFetchAction = actionId !== undefined && typeof actionId === "string" && req.method === "POST";
    if (isFetchAction || isURLEncodedAction || isMultipartAction) {
        // ensure we avoid caching server actions unexpectedly
        res.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
        let bound = [];
        const workerName = "app" + page;
        const serverModuleMap = new Proxy({}, {
            get: (_, id)=>{
                return {
                    id: serverActionsManifest[process.env.NEXT_RUNTIME === "edge" ? "edge" : "node"][id].workers[workerName],
                    name: id,
                    chunks: []
                };
            }
        });
        const { actionAsyncStorage  } = ComponentMod;
        let actionResult;
        try {
            await actionAsyncStorage.run({
                isAction: true
            }, async ()=>{
                if (process.env.NEXT_RUNTIME === "edge") {
                    // Use react-server-dom-webpack/server.edge
                    const { decodeReply , decodeAction  } = ComponentMod;
                    const webRequest = req;
                    if (!webRequest.body) {
                        throw new Error("invariant: Missing request body.");
                    }
                    if (isMultipartAction) {
                        // TODO-APP: Add streaming support
                        const formData = await webRequest.request.formData();
                        if (isFetchAction) {
                            bound = await decodeReply(formData, serverModuleMap);
                        } else {
                            const action = await decodeAction(formData, serverModuleMap);
                            await action();
                            // Skip the fetch path
                            return;
                        }
                    } else {
                        let actionData = "";
                        const reader = webRequest.body.getReader();
                        while(true){
                            const { done , value  } = await reader.read();
                            if (done) {
                                break;
                            }
                            actionData += new TextDecoder().decode(value);
                        }
                        if (isURLEncodedAction) {
                            const formData = formDataFromSearchQueryString(actionData);
                            bound = await decodeReply(formData, serverModuleMap);
                        } else {
                            bound = await decodeReply(actionData, serverModuleMap);
                        }
                    }
                } else {
                    // Use react-server-dom-webpack/server.node which supports streaming
                    const { decodeReply , decodeReplyFromBusboy , decodeAction  } = require(`react-server-dom-webpack/server.node`);
                    if (isMultipartAction) {
                        if (isFetchAction) {
                            const busboy = require("busboy");
                            const bb = busboy({
                                headers: req.headers
                            });
                            req.pipe(bb);
                            bound = await decodeReplyFromBusboy(bb, serverModuleMap);
                        } else {
                            // React doesn't yet publish a busboy version of decodeAction
                            // so we polyfill the parsing of FormData.
                            const UndiciRequest = require("next/dist/compiled/undici").Request;
                            const fakeRequest = new UndiciRequest("http://localhost", {
                                method: "POST",
                                headers: {
                                    "Content-Type": req.headers["content-type"]
                                },
                                body: nodeToWebReadableStream(req),
                                duplex: "half"
                            });
                            const formData = await fakeRequest.formData();
                            const action = await decodeAction(formData, serverModuleMap);
                            await action();
                            // Skip the fetch path
                            return;
                        }
                    } else {
                        const { parseBody  } = require("../api-utils/node");
                        let actionData;
                        try {
                            actionData = await parseBody(req, serverActionsBodySizeLimit ?? "1mb") || "";
                        } catch (e) {
                            if (e && e.statusCode === 413) {
                                // Exceeded the size limit
                                e.message = e.message + "\nTo configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation";
                            }
                            throw e;
                        }
                        if (isURLEncodedAction) {
                            const formData = formDataFromSearchQueryString(actionData);
                            bound = await decodeReply(formData, serverModuleMap);
                        } else {
                            bound = await decodeReply(actionData, serverModuleMap);
                        }
                    }
                }
                // actions.js
                // app/page.js
                //   action worker1
                //     appRender1
                // app/foo/page.js
                //   action worker2
                //     appRender
                // / -> fire action -> POST / -> appRender1 -> modId for the action file
                // /foo -> fire action -> POST /foo -> appRender2 -> modId for the action file
                const actionModId = serverActionsManifest[process.env.NEXT_RUNTIME === "edge" ? "edge" : "node"][actionId].workers[workerName];
                const actionHandler = ComponentMod.__next_app__.require(actionModId)[actionId];
                const returnVal = await actionHandler.apply(null, bound);
                // For form actions, we need to continue rendering the page.
                if (isFetchAction) {
                    await addRevalidationHeader(res, {
                        staticGenerationStore,
                        requestStore
                    });
                    actionResult = await generateFlight({
                        actionResult: Promise.resolve(returnVal),
                        // if the page was not revalidated, we can skip the rendering the flight tree
                        skipFlight: !staticGenerationStore.pathWasRevalidated
                    });
                }
            });
            return actionResult;
        } catch (err) {
            if (isRedirectError(err)) {
                const redirectUrl = getURLFromRedirectError(err);
                // if it's a fetch action, we don't want to mess with the status code
                // and we'll handle it on the client router
                await addRevalidationHeader(res, {
                    staticGenerationStore,
                    requestStore
                });
                if (isFetchAction) {
                    return createRedirectRenderResult(req, res, redirectUrl, staticGenerationStore);
                }
                if (err.mutableCookies) {
                    const headers = new Headers();
                    // If there were mutable cookies set, we need to set them on the
                    // response.
                    if (appendMutableCookies(headers, err.mutableCookies)) {
                        res.setHeader("set-cookie", Array.from(headers.values()));
                    }
                }
                res.setHeader("Location", redirectUrl);
                res.statusCode = 303;
                return new RenderResult("");
            } else if (isNotFoundError(err)) {
                res.statusCode = 404;
                await addRevalidationHeader(res, {
                    staticGenerationStore,
                    requestStore
                });
                if (isFetchAction) {
                    const promise = Promise.reject(err);
                    try {
                        await promise;
                    } catch  {}
                    return generateFlight({
                        skipFlight: false,
                        actionResult: promise,
                        asNotFound: true
                    });
                }
                return "not-found";
            }
            if (isFetchAction) {
                res.statusCode = 500;
                await Promise.all(staticGenerationStore.pendingRevalidates || []);
                const promise = Promise.reject(err);
                try {
                    await promise;
                } catch  {}
                return generateFlight({
                    actionResult: promise,
                    // if the page was not revalidated, we can skip the rendering the flight tree
                    skipFlight: !staticGenerationStore.pathWasRevalidated
                });
            }
            throw err;
        }
    }
}

//# sourceMappingURL=action-handler.js.map