/**
 * Default Cloudflare Worker entry point for vinext App Router.
 *
 * Use this directly in wrangler.jsonc:
 *   "main": "vinext/server/app-router-entry"
 *
 * Or import and delegate to it from a custom worker:
 *   import handler from "vinext/server/app-router-entry";
 *   return handler.fetch(request, env, ctx);
 *
 * This file runs in the RSC environment. Configure the Cloudflare plugin with:
 *   cloudflare({ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] } })
 */

import "./server-globals.js";
// Virtual module generated by vinext at build time. The default export is
// the request handler; `__assetPrefix` and `__basePath` are named exports
// inlined from next.config (both as string literals) so this worker can
// recognise asset-shaped requests without invoking the handler. Types
// live in `virtual-vinext-rsc-entry.d.ts`; the generator is in
// `entries/app-rsc-entry.ts`.
import rscHandler, {
  __assetPrefix as __rscAssetPrefix,
  __basePath as __rscBasePath,
} from "virtual:vinext-rsc-entry";
import { runWithExecutionContext, type ExecutionContextLike } from "vinext/shims/request-context";
// @ts-expect-error -- virtual module resolved by vinext at build time
import { registerConfiguredCacheAdapters } from "virtual:vinext-cache-adapters";
import { resolveStaticAssetSignal } from "./worker-utils.js";
import {
  cloneRequestWithHeaders,
  filterInternalHeaders,
  isOpenRedirectShaped,
} from "./request-pipeline.js";
import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER } from "./headers.js";
import {
  readTrustedPrerenderRouteParams,
  serializePrerenderRouteParamsHeader,
} from "./prerender-route-params.js";
import {
  badRequestResponse,
  notFoundResponse,
  notFoundStaticAssetResponse,
} from "./http-error-responses.js";
import { assetPrefixPathname, isNextStaticPath } from "../utils/asset-prefix.js";

// Precompute the path components used for `_next/static/*` 404 short-circuit
// detection. Both `__basePath` and `__assetPrefix` are inlined as
// compile-time constants in the generated RSC entry — see entries/app-rsc-entry.ts.
const __workerBasePath: string = typeof __rscBasePath === "string" ? __rscBasePath : "";
const __workerAssetPathPrefix: string = assetPrefixPathname(
  typeof __rscAssetPrefix === "string" ? __rscAssetPrefix : "",
);

type WorkerAssetEnv = {
  ASSETS?: {
    fetch(request: Request): Promise<Response> | Response;
  };
};

export default {
  async fetch(
    request: Request,
    env?: WorkerAssetEnv,
    ctx?: ExecutionContextLike,
  ): Promise<Response> {
    return handleRequest(request, env, ctx);
  },
};

async function handleRequest(
  request: Request,
  env: WorkerAssetEnv | undefined,
  ctx: ExecutionContextLike | undefined,
): Promise<Response> {
  // Register config-driven cache adapters before any rendering touches the cache.
  registerConfiguredCacheAdapters(env as Record<string, unknown> | undefined);

  const url = new URL(request.url);

  // Block protocol-relative URL open redirects (//evil.com/, /\evil.com/,
  // /%5Cevil.com/, /%2F/evil.com/). Check BEFORE decode so both literal and
  // percent-encoded variants are caught — encoded forms survive segment-wise
  // decoding and would otherwise reach trailing-slash redirect emitters.
  if (isOpenRedirectShaped(url.pathname)) {
    return notFoundResponse();
  }

  // Validate that percent-encoding is well-formed. The RSC handler performs
  // the actual decode + normalize; we only check here to return a clean 400
  // instead of letting a malformed sequence crash downstream.
  try {
    decodeURIComponent(url.pathname);
  } catch {
    // Malformed percent-encoding (e.g. /%E0%A4%A) — return 400 instead of throwing.
    return badRequestResponse();
  }

  // Invalid `_next/static/*` paths short-circuit with a plain-text 404
  // instead of falling through to the RSC handler (which would render the
  // full HTML 404 page). Valid assets are served by Cloudflare's ASSETS
  // binding BEFORE the worker is invoked; only misses reach this code.
  // Matches Next.js: packages/next/src/server/lib/router-server.ts.
  if (isNextStaticPath(url.pathname, __workerBasePath, __workerAssetPathPrefix)) {
    return notFoundStaticAssetResponse();
  }

  // Strip internal headers from inbound requests before any handler or
  // middleware sees them. Must happen before the RSC handler runs.
  // Builds a new Headers — Request.headers is immutable in Workers.
  {
    const prerenderRouteParamsPayload = readTrustedPrerenderRouteParams(request);
    const filteredHeaders = filterInternalHeaders(request.headers);
    const prerenderRouteParamsHeader = serializePrerenderRouteParamsHeader(
      prerenderRouteParamsPayload,
    );
    if (prerenderRouteParamsHeader !== null) {
      filteredHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
    }
    request = cloneRequestWithHeaders(request, filteredHeaders);
  }

  // Do NOT decode/normalize the pathname here. The RSC handler
  // (virtual:vinext-rsc-entry) is the single point of decoding — it calls
  // decodeURIComponent + normalizePath on the incoming URL. Decoding here
  // AND in the handler would double-decode, causing inconsistent path
  // matching between middleware and routing.

  // Delegate to RSC handler (which decodes + normalizes the pathname itself),
  // wrapping in the ExecutionContext ALS scope so downstream code can reach
  // ctx.waitUntil() without having ctx threaded through every call site.
  const handleFn = () => rscHandler(request, ctx);
  const result = await (ctx ? runWithExecutionContext(ctx, handleFn) : handleFn());

  if (result instanceof Response) {
    if (env?.ASSETS) {
      const assetFetcher = env.ASSETS;
      const assetResponse = await resolveStaticAssetSignal(result, {
        fetchAsset: (path) =>
          Promise.resolve(assetFetcher.fetch(new Request(new URL(path, request.url)))),
      });
      if (assetResponse) return assetResponse;
    }
    return result;
  }

  if (result === null || result === undefined) {
    return notFoundResponse();
  }

  return new Response(String(result), { status: 200 });
}
