/**
 * Request Context - AsyncLocalStorage for passing request-scoped data throughout rendering
 *
 * This is the unified context used everywhere:
 * - Middleware execution
 * - Route handlers and loaders
 * - Server components during rendering
 * - Error boundaries and streaming
 *
 * Available via getRequestContext() anywhere in the request lifecycle.
 */

import { AsyncLocalStorage } from "node:async_hooks";
import { parseCookiesFromHeader } from "./cookie-parse.js";
import type { CacheErrorCategory } from "../cache/cache-error.js";
import type { CookieOptions } from "../router/middleware.js";
import {
  KEEP_CACHE_HEADER,
  getRawCookieValue,
  mintStateValue,
  serializeStateCookie,
} from "../browser/cookie-name.js";
import type { LoaderDefinition, LoaderContext } from "../types.js";
import type { ScopedReverseFunction } from "../reverse.js";
import type {
  DefaultEnv,
  DefaultReverseRouteMap,
  DefaultRouteName,
} from "../types/global-namespace.js";
import type { Handle } from "../handle.js";
import {
  type ContextVar,
  contextGet,
  contextSet,
  isNonCacheable,
} from "../context-var.js";
import {
  createHandleStore,
  buildHandleSnapshot,
  type HandleStore,
  type HandleData,
} from "./handle-store.js";
import { isHandle } from "../handle.js";
import { withDefer } from "../defer.js";
import { type MetricsStore } from "./context.js";
import { observePhase, PHASES } from "../router/instrument.js";
import { getFetchableLoader } from "./fetchable-loader-store.js";
import type { SegmentCacheStore } from "../cache/types.js";
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
import type { TransitionWhenFn } from "../types/segments.js";
import type { ResolvedTracing } from "../router/tracing.js";
import type { RenderMode, RenderPhase } from "../router/timeout.js";
import {
  THEME_COOKIE,
  isValidTheme,
  warnInvalidTheme,
} from "../theme/constants.js";
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
import {
  assertCachedHeaderWriteAllowed,
  clearPprHeaderScope,
  isInsideCacheScope,
} from "./context.js";
import {
  createReverseFunction,
  stripInternalParams,
} from "../router/handler-context.js";
import {
  getGlobalRouteMap,
  isRouteRootScoped,
  getSearchSchema,
} from "../route-map-builder.js";
import { parseSearchParams } from "../search-params.js";
import { invariant } from "../errors.js";
import { isAutoGeneratedRouteName } from "../route-name.js";

export interface RenderForegroundCursor {
  mode: RenderMode;
  phase: RenderPhase;
  state: "paused" | "running";
  completed: number;
  total: number;
  pipelineStartedAt: number;
  phaseStartedAt?: number;
  routeKey?: string;
  actionId?: string;
}

/**
 * Unified request context available via getRequestContext()
 *
 * This is the same context passed to middleware and handlers.
 * Use this when you need access to request data outside of route handlers.
 */
export interface RequestContext<
  TEnv = DefaultEnv,
  TParams = Record<string, string>,
> extends RequestScope<TEnv> {
  /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
  _variables: Record<string, any>;
  /** Get a variable set by middleware */
  get: {
    <T>(contextVar: ContextVar<T>): T | undefined;
    <K extends string>(key: K): any;
  };
  /** Set a variable (shared with middleware and handlers) */
  set: {
    <T>(
      contextVar: ContextVar<T>,
      value: T,
      options?: { cache?: boolean },
    ): void;
    <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
  };
  /**
   * Route params (populated after route matching)
   * Initially empty, then set to matched params
   */
  params: TParams;
  /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
  readonly res: Response;

  /**
   * True for build-time render/capture requests. Live requests use false.
   * Build-time shell capture sets this while replaying middleware so apps can
   * skip side-effectful runtime work.
   */
  readonly build: boolean;

  /**
   * Opt this request out of PPR shell serving/capture.
   * Runtime middleware can call this before the PPR commit point; handlers can
   * call it on a MISS to prevent the follow-up capture.
   *
   * Scope: the PPR SHELL axis only. It does NOT disable prerender B-segment
   * (Prerender/Static) serving, and it is inert in the prerender-collect /
   * static-render contexts (no live shell decision to influence there).
   */
  dynamic(): void;

  /** @internal Request-local PPR opt-out marker set by ctx.dynamic(). */
  _dynamic?: boolean;

  /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
  cookie(name: string): string | undefined;
  /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
  cookies(): Record<string, string>;
  /** @internal Set a cookie on the response. Use cookies().set() instead. */
  setCookie(name: string, value: string, options?: CookieOptions): void;
  /** @internal Delete a cookie. Use cookies().delete() instead. */
  deleteCookie(
    name: string,
    options?: Pick<CookieOptions, "domain" | "path">,
  ): void;
  /** Set a response header */
  header(name: string, value: string): void;
  /** Set the response status code */
  setStatus(status: number): void;
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
  _setStatus(status: number): void;
  /** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
  _rotateStateCookie(): void;
  /** @internal Set the keepClientCache() directive header on the response. */
  _setKeepCacheDirective(): void;

  /**
   * Access loader data or push handle data.
   *
   * For loaders: Returns a promise that resolves to the loader data.
   * Loaders are executed in parallel and memoized per request.
   *
   * For handles: Returns a push function to add data for this segment.
   * Handle data accumulates across all matched route segments.
   *
   * @example
   * ```typescript
   * // Loader usage
   * const cart = await ctx.use(CartLoader);
   *
   * // Handle usage
   * const push = ctx.use(Breadcrumbs);
   * push({ label: "Shop", href: "/shop" });
   * ```
   */
  use: {
    <T, TLoaderParams = any>(
      loader: LoaderDefinition<T, TLoaderParams>,
    ): Promise<T>;
    <TData, TAccumulated = TData[]>(
      handle: Handle<TData, TAccumulated>,
    ): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
  };

  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
  method: string;

  /** @internal Handle store for tracking handle data across segments */
  _handleStore: HandleStore;

  /**
   * @internal transition({ when }) predicates for segments matched this request,
   * keyed by segment id. Collected during resolution (the function is stripped
   * from the serialized segment config), then evaluated post-handler in
   * rsc-rendering — outside any cache scope — to drop the transition of any
   * segment whose predicate returns false.
   */
  _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;

  /** @internal PPR transition decisions evaluated before cache lookup/handlers. */
  _pprTransitionDecisions?: Map<string, boolean>;

  /**
   * @internal Post-match serve-source truth for the PPR replay reporter.
   * Stamped from the match context as `intercept`, then overwritten as
   * `prerender-store` if that lookup actually serves. The latter wins because
   * it identifies the response source. matchPartialWithPprReplay reads this
   * AFTER matching to reclassify its pre-match guess and suppress pointless
   * heal captures.
   */
  _pprReplayPostMatchReason?: "prerender-store" | "intercept";

  /** @internal Cache store for segment caching (optional, used by CacheScope) */
  _cacheStore?: SegmentCacheStore;

  /**
   * @internal Compiled `cache.searchParams` filter for default cache-key
   * generation (undefined = "all", the byte-stable unfiltered format). Set
   * once per request by handler.ts; every URL-keyed tier (segment, document,
   * response, shell, "use cache") reads it so the tiers cannot drift.
   */
  _searchParamsFilter?: import("../cache/search-params-filter.js").SearchParamsFilter;

  /**
   * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
   * capture task's derived request context (built by shell-capture.ts). This is
   * the switch every capture-specific behavior reads: loader masking
   * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
   * cookies()/headers() capture guard (cookie-store.ts
   * assertNotInsideShellCapture). The foreground render never sets it, so the
   * served response is byte-identical to axis 1. The capture descriptor itself
   * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
   * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
   * locally and passes it to scheduleShellCapture directly.
   */
  _shellCaptureRun?: boolean;

  /**
   * @internal Bake-lane loader containers collected DURING a shell capture:
   * segment-key -> the loader's (pre-wrap) result promise. Populated by
   * resolveLoaderData for loaders on entries with no renderable loading() (the
   * bake lane — they execute at capture instead of being masked; see
   * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
   * after the shell quiesces: settled containers are promise-elided,
   * Flight-serialized, and pinned into the snapshot's loader family; a
   * REJECTED container refuses the capture (error UI must never bake into the
   * shared shell). Own property of the capture's derived context only.
   */
  _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;

  /**
   * @internal Loader-family snapshot seed for a shell HIT's tail render:
   * segment-key -> the capture's elided container (already Flight-deserialized
   * by serveShellHit) plus its capture-computed hole bit. resolveLoaderData
   * overlays it onto the fresh run's container (recorded paths pinned,
   * hole-marker paths keep the fresh nested promises) so the payload's baked
   * bytes match the frozen prelude; a hole-free entry resolves pin-first
   * without gating on the fresh run. Own property of the HIT tail's derived
   * context only.
   */
  _shellLoaderSeed?: Map<
    string,
    import("../cache/shell-snapshot.js").ShellLoaderSeedEntry
  >;

  /**
   * @internal Shell fast-path marker: makes the next eligible match treat the
   * whole matched route as an implicit doc-level cache() boundary (see
   * resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
   * (a) the capture's derived context — with a record-only store so the
   * capture's cacheRoute write lands in the snapshot, never the real store —
   * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
   * replay, where `store` is a request-local segment overlay. Handler-live holes
   * and conditional transitions decline replay.
   * Routes with their own cache() config are never overridden — their scope's
   * store/key/ttl/swr/condition semantics stay authoritative. On the
   * navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
   * such a scope instead of being ignored: the seeded doc record supplies the match
   * only when the explicit tier misses (withCacheLookup). cache(false) and a
   * false condition() stay absolute opt-outs — replay bypasses before any
   * shell read (`cache-disabled`).
   */
  _shellImplicitCache?: {
    ttl?: number;
    swr?: number;
    store?: SegmentCacheStore;
    /**
     * @internal Override the implicit scope's default cache namespace. Shell
     * captures and navigation replay both consume the canonical document
     * segment record even when the triggering request is partial.
     */
    keyPrefix?: "doc";
    /** @internal Called only after the implicit cache hit decodes successfully. */
    onHit?: () => void;
    /**
     * @internal Called when the seeded document record fails server-side
     * deserialization. Partial replay uses it to replace the enclosing shell
     * snapshot after serving the fresh fallback.
     */
    onCorrupt?: () => void;
    /**
     * @internal The resolved key of the canonical document segment record.
     * Written during a CAPTURE render by CacheScope.cacheRoute when a
     * doc-namespaced scope records the matched segments; captureAndStoreShell
     * stamps it onto the ShellCacheEntry (`docKey`) so replay eligibility can
     * require the exact consumable record instead of "any segment record".
     */
    docKey?: string;
    /**
     * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
     * serve path. Its presence arms the explicit-scope composition in
     * withCacheLookup: a route-derived cache() scope stays authoritative, and
     * only when its lookup MISSes does the seeded doc record supply the match.
     * A capture render must never set this (its match must re-run handlers so
     * SWR recapture stays fresh), which is why the fallback keys off this
     * field and not off the marker itself. Fired when the route-derived
     * scope's own lookup supplied the match — the header then reports
     * `BYPASS; reason=explicit-cache-hit`, never a false replay HIT.
     */
    onExplicitHit?: () => void;
    /**
     * @internal Companion to onExplicitHit, same lifecycle: fired when the
     * route-derived scope's lookup REFUSED the read (`bypass` outcome — a
     * false condition() or no store). The gate only pre-decides the static
     * cache(false) case; a predicate refusal is known only at lookup time and
     * reports `BYPASS; reason=cache-disabled` post-match.
     */
    onExplicitBypass?: () => void;
  };

  /**
   * @internal Fragment-passthrough marker: cache/prerender-store hits during
   * THIS render emit stored segment fragments VERBATIM into the payload
   * (segment-codec fragmentSegments) instead of deserialize -> re-serialize
   * per request; the payload consumers (SSR resume, browser hydration, and
   * the client partial-navigation decode chokepoints) expand them
   * (segment-fragments.ts, issue #700). Partial requests arm only after the
   * browser advertises `X-Rango-Fragment-Passthrough: 1`; a decode failure
   * retries without it so CacheScope can evict the bad record. Two arming sites,
   * both scoped to a
   * render/match window: serveShellHit's derived tail context (document
   * HIT), and matchPartialWithPprReplay's mutate-restore around the partial
   * match (matchPartialForReplay). It must never be visible to a capture
   * render: the capture SSR-prerenders the payload AND serializes segments
   * into records (cacheRoute), and an envelope reaching serializeSegments
   * would store a double-encoded fragment — deriveShellCaptureContext resets
   * it as an own property for exactly that reason.
   */
  _shellFragmentPayload?: boolean;

  /**
   * @internal Handler-layer liveness observed DURING a shell capture, from
   * three sources: (a) the capture handle-store push wrapper (shell-capture.ts)
   * when a push made OUTSIDE a DSL loader scope carries a nested thenable
   * (masked to a never-filling hole); (b) still-pending top-level handler
   * pushes (liveness unknowable at the barrier); (c) a handler-invoked loader
   * executing during the capture (loader-resolution.ts — its consumption-lane
   * value would freeze on a handler-free HIT). captureAndStoreShell folds it
   * into ShellCacheEntry.handlerLiveHoles at the putShell barrier. Own
   * property of the capture's derived context only.
   */
  _shellCaptureHandleLiveness?: {
    holes: boolean;
    pendingPushes: number;
    handlerInvokedLoader: boolean;
  };

  /**
   * @internal Handle values pushed from a DSL loader scope DURING a shell
   * capture (identity set; populated by the capture push wrapper in
   * shell-capture.ts). cacheRoute threads it into captureHandles so those
   * values stay out of cache-write handle records — loaders re-run fresh on
   * every HIT, so replaying their captured (masked) values would duplicate
   * the fresh push and stall the Flight handle encode. Own property of the
   * capture's derived context only; render-time handle consumers are
   * unaffected (the exclusion applies only at the captureHandles call site).
   */
  _shellCaptureLoaderHandleValues?: WeakSet<object>;

  /**
   * @internal Set (to the offending fn name) by the cookies()/headers()
   * capture guard when it throws DURING a capture render. Load-bearing for the
   * bake lane: a guard throw inside an executing loader is swallowed by
   * wrapLoaderPromise into per-loader error UI, which would otherwise bake
   * silently into the shared shell — the capture checks this flag after the
   * render and refuses instead. Deterministic, so the capture does not retry.
   */
  _shellCaptureGuardTripped?: string;

  /**
   * @internal The loader $$id whose BODY was executing when the capture guard
   * tripped (read off the loader-body ALS scope at trip time), or undefined
   * when the read came from handler/render code. Only used to make the
   * once-per-key refusal warning name the real source — the old text
   * hardcoded "a bake-lane loader", which misattributed handler-land reads
   * and sent users debugging the wrong lane (issue #672, secondary).
   */
  _shellCaptureGuardTrippedLoaderId?: string;

  /**
   * @internal Handler-owned registry of explicit per-scope stores from
   * cache({ store }). Created once per createRSCHandler() and threaded into
   * every request context, so it accumulates every explicit store the handler
   * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
   * reach every store that may hold tagged entries. The app-level store is not
   * added here (it is always reachable via _cacheStore).
   */
  _explicitTaggedStores?: Set<SegmentCacheStore>;

  /**
   * @internal Union of every cache tag resolved while producing this request's
   * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
   * Populated at the tag-resolution sites via recordRequestTags(). Read by the
   * document cache middleware so a full-page entry is tagged with everything its
   * content used and can therefore be invalidated by updateTag()/revalidateTag().
   */
  _requestTags: Set<string>;

  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
  _cacheProfiles?: Record<
    string,
    import("../cache/profile-registry.js").CacheProfile
  >;

  /**
   * Register a callback to run when the response is created.
   * Callbacks are sync and receive the response. They can:
   * - Inspect response status/headers
   * - Return a modified response
   * - Schedule async work via waitUntil
   *
   * @example
   * ```typescript
   * ctx.onResponse((res) => {
   *   if (res.status === 200) {
   *     ctx.waitUntil(async () => await cacheIt());
   *   }
   *   return res;
   * });
   * ```
   */
  onResponse(callback: (response: Response) => Response): void;

  /** @internal Registered onResponse callbacks */
  _onResponseCallbacks: Array<(response: Response) => Response>;

  /**
   * @internal Promises of the background tasks scheduled via this context's
   * waitUntil (deferred cache writes, revalidations, consumer tasks). The PPR
   * shell capture drains this list BEFORE its match/render as an ORDERING EDGE:
   * every foreground deferred cache write is scheduled here before the capture
   * task is, so settling the list first guarantees the capture's cache reads
   * observe the foreground's generation instead of racing it (see
   * shell-capture.ts). Tasks whose scheduling fn carries
   * UNTRACKED_BACKGROUND_TASK are not tracked (the capture task itself —
   * tracking it would make that drain await its own promise).
   */
  _pendingBackgroundTasks?: Array<Promise<unknown>>;

  /**
   * Current theme setting (only available when theme is enabled in router config)
   *
   * Returns the theme value from the cookie, or the default theme if not set.
   * This is the user's preference ("light", "dark", or "system"), not the resolved value.
   *
   * @example
   * ```typescript
   * route("settings", (ctx) => {
   *   const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
   *   return <SettingsPage theme={currentTheme} />;
   * });
   * ```
   */
  theme?: Theme;

  /**
   * Set the theme (only available when theme is enabled in router config)
   *
   * Sets a cookie with the new theme value. The change takes effect on the next request.
   *
   * @example
   * ```typescript
   * route("settings", (ctx) => {
   *   if (ctx.method === "POST") {
   *     const formData = await ctx.request.formData();
   *     const newTheme = formData.get("theme") as Theme;
   *     ctx.setTheme(newTheme);
   *   }
   *   return <SettingsPage />;
   * });
   * ```
   */
  setTheme?: (theme: Theme) => void;

  /** @internal Theme configuration (null if theme not enabled) */
  _themeConfig?: ResolvedThemeConfig | null;

  /**
   * Attach location state entries to the current response.
   *
   * For partial (SPA) requests, the state is included in the RSC payload
   * metadata and merged into history.pushState on the client. For redirect
   * responses, the state travels through the redirect payload so the target
   * page can read it via useLocationState.
   *
   * Multiple calls accumulate entries.
   *
   * @example
   * ```typescript
   * ctx.setLocationState(Flash({ text: "Item saved!" }));
   * ```
   */
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;

  /** @internal Accumulated location state entries */
  _locationState?: LocationStateEntry[];

  /**
   * The matched route name, if the route has an explicit name.
   * Undefined before route matching or for unnamed routes.
   * Includes the namespace prefix from include() (e.g., "blog.post").
   */
  routeName?: DefaultRouteName;

  /**
   * Generate URLs from route names.
   * Uses the global route map. After route matching, scoped (`.name`) resolution
   * works within the matched include() scope.
   */
  reverse: ScopedReverseFunction<
    Record<string, string>,
    DefaultReverseRouteMap
  >;

  /** @internal Route name from route matching, used for scoped reverse resolution */
  _routeName?: string;

  /** @internal Previous route key (from the navigation source), used for revalidation */
  _prevRouteKey?: string;

  /**
   * @internal Navigation/action source data the transition({ when }) gate reads
   * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
   * come from the navigation snapshot (set at match time); action* are stashed
   * at the action-bearing gate call sites. All undefined when there is no source
   * (initial full load) or no action (plain navigation).
   */
  _gateCurrentUrl?: URL;
  _gateCurrentParams?: Record<string, string>;
  _gateActionId?: string;
  _gateActionUrl?: URL;
  _gateActionResult?: unknown;
  _gateFormData?: FormData;

  /**
   * @internal True while the post-action revalidation render is running (set by
   * revalidateAfterAction). The "use cache" runtime reads this to prefer
   * freshness over a fast stale response during an action: a stale entry
   * re-executes in the foreground (so the action response reflects the refreshed
   * value) with only the store write deferred, instead of serving stale and
   * revalidating in the background. A plain navigation (flag unset) keeps SWR.
   */
  _inActionRevalidation?: boolean;

  /**
   * @internal Render barrier for experimental `rendered()` API.
   * Resolves when all non-loader segments have settled and handle data
   * is available. Used by DSL loaders that call `ctx.rendered()`.
   */
  _renderBarrier: Promise<void>;

  /**
   * @internal Resolve the render barrier. Accepts resolved segments, filters
   * out loaders, and captures non-loader segment IDs as the handle ordering.
   * Called after segment resolution (fresh) or handle replay (cache/prerender).
   */
  _resolveRenderBarrier: (
    segments: Array<{ type: string; id: string }>,
  ) => void;

  /**
   * @internal Segment order at barrier resolution time, used by loader
   * ctx.use(handle) to collect handle data in correct order.
   */
  _renderBarrierSegmentOrder?: string[];

  /**
   * @internal Set to true when the matched entry tree contains any `loading()`
   * entries (streaming). On a streaming tree rendered() waits for the streaming
   * handlers to settle (via handleStore.settled) before resolving, and the
   * deadlock guard state is kept live until that wait completes.
   */
  _treeHasStreaming?: boolean;

  /**
   * @internal Loader IDs that have called rendered() and are waiting for the
   * barrier. Used to detect deadlocks when a handler tries to await the same
   * loader via ctx.use(Loader).
   */
  _renderBarrierWaiters?: Set<string>;

  /**
   * @internal Loader IDs that handlers have started awaiting via ctx.use().
   * Used for bidirectional deadlock detection: if a loader later calls
   * rendered() and a handler already awaits it, we can detect the deadlock.
   */
  _handlerLoaderDeps?: Set<string>;

  /**
   * @internal Cached HandleData snapshot built at barrier resolution time.
   * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
   */
  _renderBarrierHandleSnapshot?: HandleData;

  /**
   * @internal The deadlock guard window is closed (no further handler-awaits-
   * loader cycle is possible). For non-streaming trees this is set when the
   * barrier resolves. For streaming trees the window stays open until
   * handleStore.settled — rendered() keeps waiting past the barrier and a
   * loading() handler can still resume and await a still-waiting loader — so it
   * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
   * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
   * during the streaming settle wait.
   */
  _renderBarrierGuardClosed?: boolean;

  /** @internal Per-request error dedup set for onError reporting */
  _reportedErrors: WeakSet<object>;

  /**
   * @internal Report a non-fatal background error through the router's
   * onError callback. Wired by the RSC handler / router during request
   * creation. Cache-runtime and other subsystems call this to surface
   * errors without failing the response. `category` is surfaced to consumers as
   * `metadata.category` on the onError context (phase `cache`).
   */
  _reportBackgroundError?: (
    error: unknown,
    category: CacheErrorCategory,
  ) => void;

  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
  _debugPerformance?: boolean;

  /** @internal Request-scoped performance metrics store */
  _metricsStore?: MetricsStore;

  /**
   * @internal Foreground response-construction cursor. It reports only the
   * generator driver's current Flight/HTML/response operation; concurrent
   * loaders and post-commit stream work have separate lifetimes.
   */
  _renderForeground?: RenderForegroundCursor;

  /** @internal Keep the render cursor active for timeout support. */
  _renderDiagnosticsEnabled?: boolean;

  /**
   * @internal True request entry timestamp (performance.now() at handler entry).
   * Set once at request-context creation (rsc/handler.ts) so a metrics store
   * created MID-request — ctx.debugPerformance() or the getMetricsStore wrapper —
   * anchors its timeline to the real request start instead of the opt-in moment,
   * keeping phases that began before the opt-in at their true (non-negative) offset.
   */
  _handlerStart?: number;

  /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
  _tracing?: ResolvedTracing;

  /** @internal Router basename for this request (used by redirect()) */
  _basename?: string;

  /** @internal Owning router id; scopes the search-schema/root-scope registry
   *  lookups per router (route-map-builder.ts) */
  _routerId?: string;

  /**
   * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
   * to avoid a second resolveRoute call. Cleared on HMR invalidation.
   */
  _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;

  /**
   * @internal Coarse route-level cache signal for the X-Rango-Cache debug
   * header. Populated by match/matchPartial only when the debug cache signal
   * gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
   * the response-finalization path (createResponseWithMergedHeaders). Undefined
   * when the gate is off, so no header is emitted.
   */
  _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
}

/**
 * Public view of RequestContext, without internal methods and fields.
 *
 * This is the type exported to library consumers. Internal code should
 * use the full RequestContext interface directly.
 */
export type PublicRequestContext<
  TEnv = DefaultEnv,
  TParams = Record<string, string>,
> = Omit<
  RequestContext<TEnv, TParams>,
  | "cookie"
  | "cookies"
  | "setCookie"
  | "deleteCookie"
  | "_handleStore"
  | "_transitionWhen"
  | "_pprTransitionDecisions"
  | "_pprReplayPostMatchReason"
  | "_cacheStore"
  | "_searchParamsFilter"
  | "_shellCaptureRun"
  | "_shellImplicitCache"
  | "_shellLoaderSeed"
  | "_shellCaptureLoaderRecords"
  | "_shellCaptureHandleLiveness"
  | "_shellCaptureLoaderHandleValues"
  | "_shellFragmentPayload"
  | "_shellCaptureGuardTripped"
  | "_shellCaptureGuardTrippedLoaderId"
  | "_explicitTaggedStores"
  | "_requestTags"
  | "_cacheProfiles"
  | "_onResponseCallbacks"
  | "_pendingBackgroundTasks"
  | "_themeConfig"
  | "_locationState"
  | "_routeName"
  | "_prevRouteKey"
  | "_gateCurrentUrl"
  | "_gateCurrentParams"
  | "_gateActionId"
  | "_gateActionUrl"
  | "_gateActionResult"
  | "_gateFormData"
  | "_inActionRevalidation"
  | "_reportedErrors"
  | "_renderBarrier"
  | "_resolveRenderBarrier"
  | "_renderBarrierSegmentOrder"
  | "_treeHasStreaming"
  | "_renderBarrierWaiters"
  | "_handlerLoaderDeps"
  | "_renderBarrierHandleSnapshot"
  | "_renderBarrierGuardClosed"
  | "_reportBackgroundError"
  | "_debugPerformance"
  | "_metricsStore"
  | "_renderForeground"
  | "_renderDiagnosticsEnabled"
  | "_handlerStart"
  | "_tracing"
  | "_basename"
  | "_routerId"
  | "_setStatus"
  | "_rotateStateCookie"
  | "_setKeepCacheDirective"
  | "_variables"
  | "_classifiedRoute"
  | "_cacheSignal"
  | "_dynamic"
  | "res"
>;

/**
 * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
 * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
 * the capture's pre-render write barrier settles that list, so tracking the
 * capture itself would make the drain wait on its own (still-running) promise.
 * @internal
 */
export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
  "rango.untrackedBackgroundTask",
);

// AsyncLocalStorage instance for request context
const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();

/**
 * Run a function within a request context
 * Used by the RSC handler to provide context to server actions
 */
export function runWithRequestContext<TEnv, T>(
  context: RequestContext<TEnv>,
  fn: () => T,
): T {
  return requestContextStorage.run(context, fn);
}

/**
 * Get the current request context
 * Throws if called outside of a request context
 */
export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
  const ctx = requestContextStorage.getStore() as
    | RequestContext<TEnv>
    | undefined;
  invariant(
    ctx,
    "getRequestContext() called outside of a request context. " +
      "This function must be called from within a route handler, loader, middleware, " +
      "server action, or server component.",
  );
  return ctx;
}

/**
 * @internal Get the request context without throwing — for internal code that
 * may run outside a request context (cache stores, optional handle lookups, etc.)
 */
export function _getRequestContext<TEnv = DefaultEnv>():
  | RequestContext<TEnv>
  | undefined {
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
}

/**
 * Update params on the current request context
 * Called after route matching to populate route params and route name
 */
export function setRequestContextParams(
  params: Record<string, string>,
  routeName?: string,
  routeMap?: Record<string, string>,
): void {
  const ctx = requestContextStorage.getStore();
  if (ctx) {
    ctx.params = params;
    if (routeName !== undefined) {
      ctx._routeName = routeName;
      ctx.routeName = (
        routeName && !isAutoGeneratedRouteName(routeName)
          ? routeName
          : undefined
      ) as DefaultRouteName | undefined;
    }
    // Update reverse with scoped resolution now that route is known. Production
    // omits routeMap and uses the global map (routes are registered globally);
    // the testing primitives (renderToFlightString/renderServerTree) pass a
    // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
    // registered last.
    ctx.reverse = createReverseFunction(
      routeMap ?? getGlobalRouteMap(),
      routeName,
      params,
      routeName ? isRouteRootScoped(routeName, ctx._routerId) : undefined,
    );
  }
}

/**
 * Store the previous route key on the request context.
 * Called during partial-match context creation to make the navigation source
 * route key available for revalidation and intercept evaluation.
 * @internal
 */
export function setRequestContextPrevRouteKey(
  prevRouteKey: string | undefined,
  currentUrl?: URL,
  currentParams?: Record<string, string>,
): void {
  const ctx = requestContextStorage.getStore();
  if (!ctx) return;
  if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
  // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
  // effectiveFromMatch.params from the navigation snapshot). Same write point as
  // _prevRouteKey, which doubles as fromRouteName.
  if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
  if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
}

/**
 * Get accumulated location state entries from the current request context.
 * Returns undefined if no state has been set.
 *
 * @internal Used by the RSC handler to include state in payload metadata.
 */
export function getLocationState(): LocationStateEntry[] | undefined {
  const ctx = getRequestContext();
  return ctx?._locationState;
}

export type { ExecutionContext };

/**
 * Options for creating a request context
 */
export interface CreateRequestContextOptions<TEnv> {
  env: TEnv;
  request: Request;
  url: URL;
  variables: Record<string, any>;
  /** Optional initial response stub headers/status to seed effective cookie reads */
  initialResponse?: Response;
  /** Optional cache store for segment caching (used by CacheScope) */
  cacheStore?: SegmentCacheStore;
  /**
   * Compiled `cache.searchParams` filter from the resolved handler cache
   * config (handler.ts). Stored as _searchParamsFilter.
   */
  searchParamsFilter?: import("../cache/search-params-filter.js").SearchParamsFilter;
  /**
   * Handler-owned registry of explicit per-scope stores for cross-store tag
   * invalidation. Created once per handler, reused across requests.
   */
  explicitTaggedStores?: Set<SegmentCacheStore>;
  /** Optional cache profiles for "use cache" resolution (per-router) */
  cacheProfiles?: Record<
    string,
    import("../cache/profile-registry.js").CacheProfile
  >;
  /** Optional Cloudflare execution context for waitUntil support */
  executionContext?: ExecutionContext;
  /** Build-time render/capture request marker. Defaults to false. */
  build?: boolean;
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
  themeConfig?: ResolvedThemeConfig | null;
  /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
  stateCookieName?: string;
  /** Build version, used as the prefix of a server-rotated rango state value. */
  version?: string;
}

/**
 * Create a full request context with all methods implemented
 *
 * This is used by the RSC handler to create the unified context that's:
 * - Available via getRequestContext() throughout the request
 * - Passed to middleware as ctx
 * - Passed to handlers as ctx
 */
export function createRequestContext<TEnv>(
  options: CreateRequestContextOptions<TEnv>,
): RequestContext<TEnv> {
  const {
    env,
    request,
    url,
    variables,
    initialResponse,
    cacheStore,
    searchParamsFilter,
    explicitTaggedStores,
    cacheProfiles,
    executionContext,
    build = false,
    themeConfig,
    stateCookieName,
    version: stateVersion,
  } = options;
  const cookieHeader = request.headers.get("Cookie");
  let rangoStateRotated = false;
  let parsedCookies: Record<string, string> | null = null;

  let stubResponse = initialResponse
    ? new Response(null, {
        status: initialResponse.status,
        statusText: initialResponse.statusText,
        headers: new Headers(initialResponse.headers),
      })
    : new Response(null, { status: 200 });

  // The #713 choke point: `rawStubHeaders` is the stub's REAL Headers; the
  // proxy below is the only view reachable from consumer code, and its
  // mutating methods consult the cached-scope guard first. shadowStubHeaders
  // shadows `headers` on the stub Response INSTANCE (a real Response, not a
  // facade — safe to hand to the platform), so every surface that ends in a
  // stub-header mutation — ctx.header/setCookie/deleteCookie, ctx.setTheme,
  // the handler ctx.headers proxy, and raw `ctx.res.headers.set(...)` — is
  // guarded at the mutation itself, not per enumerated wrapper. Guard-exempt
  // internal writers (_rotateStateCookie, _setKeepCacheDirective — serve
  // machinery, documented callable from loaders/during capture) write to
  // rawStubHeaders directly. The serve/commit path's stub reads and its
  // Set-Cookie drain (rsc/helpers.ts) run outside any latched funnel scope,
  // where assertCachedHeaderWriteAllowed is a no-op.
  let rawStubHeaders = stubResponse.headers;
  const guardedStubHeaders: Headers = new Proxy(new Headers(), {
    get(_target, prop) {
      const raw = rawStubHeaders;
      const value = Reflect.get(raw, prop) as unknown;
      if (typeof value !== "function") return value;
      if (prop === "set" || prop === "append" || prop === "delete") {
        return (...args: unknown[]) => {
          assertCachedHeaderWriteAllowed("response headers", prop);
          return (value as (...a: unknown[]) => unknown).apply(raw, args);
        };
      }
      return (value as (...a: unknown[]) => unknown).bind(raw);
    },
  });
  const shadowStubHeaders = (res: Response): void => {
    Object.defineProperty(res, "headers", {
      value: guardedStubHeaders,
      enumerable: true,
      configurable: true,
    });
  };
  shadowStubHeaders(stubResponse);
  // Rebuild the stub with a new status, re-shadowing the fresh instance
  // (the Response constructor copies rawStubHeaders into new raw Headers).
  const replaceStubStatus = (status: number): void => {
    stubResponse = new Response(null, { status, headers: rawStubHeaders });
    rawStubHeaders = stubResponse.headers;
    shadowStubHeaders(stubResponse);
  };

  const handleStore = createHandleStore();
  const loaderPromises = new Map<string, Promise<any>>();

  const getParsedCookies = (): Record<string, string> => {
    if (!parsedCookies) {
      parsedCookies = parseCookiesFromHeader(cookieHeader);
    }
    return parsedCookies;
  };

  let responseCookieCache: Map<string, string | null> | null = null;
  const getResponseCookies = (): Map<string, string | null> => {
    if (!responseCookieCache) {
      responseCookieCache = parseResponseCookies(stubResponse);
    }
    return responseCookieCache;
  };
  const invalidateResponseCookieCache = () => {
    responseCookieCache = null;
  };

  // Guard for the two response-write surfaces that are NOT Headers mutations
  // (setStatus rebuilds the stub Response, onResponse registers a callback) —
  // these can't ride the guarded-headers choke point above, so they stay
  // enumerated. Same unified #713 guard, same message family.
  function assertResponseWriteAllowed(methodName: string): void {
    assertCachedHeaderWriteAllowed("ctx", methodName);
  }

  // Response stub Set-Cookie wins, then original header (source of truth for mutations).
  const effectiveCookie = (name: string): string | undefined => {
    const mutations = getResponseCookies();
    if (mutations.has(name)) {
      const v = mutations.get(name);
      return v === null ? undefined : v;
    }
    return getParsedCookies()[name];
  };

  const getTheme = (): Theme | undefined => {
    if (!themeConfig) return undefined;

    const stored = effectiveCookie(themeConfig.storageKey);
    if (stored) {
      if (stored === "system" && themeConfig.enableSystem) {
        return "system";
      }
      if (themeConfig.themes.includes(stored)) {
        return stored as Theme;
      }
    }
    return themeConfig.defaultTheme;
  };

  const setTheme = (theme: Theme): void => {
    if (!themeConfig) return;

    // Shared guard (isValidTheme): reject any value not in the configured theme
    // set, AND reject "system" when system detection is off — a cookie of
    // theme=system with enableSystem:false would re-apply a bogus class="system"
    // on the next SSR.
    if (!isValidTheme(theme, themeConfig)) {
      warnInvalidTheme(theme, themeConfig);
      return;
    }

    stubResponse.headers.append(
      "Set-Cookie",
      serializeCookieValue(themeConfig.storageKey, theme, {
        path: THEME_COOKIE.path,
        maxAge: THEME_COOKIE.maxAge,
        sameSite: THEME_COOKIE.sameSite,
      }),
    );
    invalidateResponseCookieCache();
  };

  const cleanUrl = stripInternalParams(url);

  const ctx: RequestContext<TEnv> = {
    env,
    request,
    url: cleanUrl,
    originalUrl: new URL(request.url),
    pathname: url.pathname,
    searchParams: cleanUrl.searchParams,
    _variables: variables,
    build,
    dynamic(): void {
      ctx._dynamic = true;
      // A dynamic() render is always live (never a shell HIT), so its handler
      // header writes are deterministic — clear the ppr latch to re-permit them
      // (#735). No-op from middleware (outside the funnel scope) or on non-ppr
      // routes; cache() latches are left alone.
      clearPprHeaderScope();
    },
    _dynamic: false,
    get: ((keyOrVar: any) => {
      if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
        throw new Error(
          `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
            `The variable was created with { cache: false } or set with { cache: false }, ` +
            `and its value would be stale on cache hit. Move the read outside the cached scope.`,
        );
      }
      return contextGet(variables, keyOrVar);
    }) as RequestContext<TEnv>["get"],
    set: ((keyOrVar: any, value: any, options?: any) => {
      assertNotInsideCacheExec(ctx, "set");
      contextSet(variables, keyOrVar, value, options);
    }) as RequestContext<TEnv>["set"],
    params: {} as Record<string, string>,

    get res(): Response {
      return stubResponse;
    },
    set res(_: Response) {
      throw new Error(
        "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
      );
    },

    cookie(name: string): string | undefined {
      return effectiveCookie(name);
    },

    cookies(): Record<string, string> {
      const parsed = getParsedCookies();
      const mutations = getResponseCookies();
      if (mutations.size === 0) return { ...parsed };
      // Build result without delete (avoids V8 dictionary-mode de-opt)
      const deleted = new Set<string>();
      for (const [k, v] of mutations) {
        if (v === null) deleted.add(k);
      }
      const result: Record<string, string> = {};
      for (const key of Object.keys(parsed)) {
        if (!deleted.has(key)) result[key] = parsed[key];
      }
      for (const [k, v] of mutations) {
        if (v !== null) result[k] = v;
      }
      return result;
    },

    setCookie(name: string, value: string, options?: CookieOptions): void {
      assertNotInsideCacheExec(ctx, "setCookie");
      stubResponse.headers.append(
        "Set-Cookie",
        serializeCookieValue(name, value, options),
      );
      invalidateResponseCookieCache();
    },

    deleteCookie(
      name: string,
      options?: Pick<CookieOptions, "domain" | "path">,
    ): void {
      assertNotInsideCacheExec(ctx, "deleteCookie");
      stubResponse.headers.append(
        "Set-Cookie",
        serializeCookieValue(name, "", { ...options, maxAge: 0 }),
      );
      invalidateResponseCookieCache();
    },

    header(name: string, value: string): void {
      assertNotInsideCacheExec(ctx, "header");
      stubResponse.headers.set(name, value);
    },

    // Rotate the rango state cookie for the responding client (the server seat
    // of invalidateClientCache). Writes ONE Set-Cookie per request with the
    // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
    // serializer), not the URL-encoded form serializeCookieValue would produce.
    // The timestamp is strictly greater than the client's current one (inbound
    // X-Rango-State), so a same-millisecond server rotation still differs from
    // the client value and the divergence observer fires.
    _rotateStateCookie(): void {
      if (rangoStateRotated) return;
      rangoStateRotated = true;
      if (!stateCookieName) return;
      // The client's current value, for the monotonic guard: prefer the
      // X-Rango-State header (router navigation/prefetch fetches send it), but
      // fall back to the request's rango state cookie — action POSTs / plain
      // app fetch()s carry no router header yet DO send the cookie. Without the
      // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
      // leaving the divergence observer silent. `|| null` so an empty header
      // ('' from proxy normalization) falls through instead of short-circuiting.
      // getRawCookieValue reads the cookie undecoded (the wire value
      // decodeStateValue decodes exactly once) AND is the same parser the client
      // mirror uses, so both seats read the same jar entry.
      const prevRaw =
        (request.headers.get("x-rango-state") || null) ??
        getRawCookieValue(cookieHeader, stateCookieName);
      const value = mintStateValue(stateVersion ?? "0", prevRaw);
      // rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
      // is documented callable from loaders and during shell capture.
      rawStubHeaders.append(
        "Set-Cookie",
        serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
      );
      invalidateResponseCookieCache();
    },

    // Set the keepClientCache() directive header. The action bridge reads it on
    // the response and suppresses its automatic invalidation. `.set` makes this
    // idempotent (one header regardless of call count). rawStubHeaders:
    // guard-exempt internal writer.
    _setKeepCacheDirective(): void {
      rawStubHeaders.set(KEEP_CACHE_HEADER, "1");
    },

    setStatus(status: number): void {
      assertNotInsideCacheExec(ctx, "setStatus");
      assertResponseWriteAllowed("setStatus");
      replaceStubStatus(status);
    },

    _setStatus(status: number): void {
      replaceStubStatus(status);
    },

    use: null as any,

    method: request.method,

    _handleStore: handleStore,
    _transitionWhen: [],
    _cacheStore: cacheStore,
    _searchParamsFilter: searchParamsFilter,
    _explicitTaggedStores: explicitTaggedStores,
    _requestTags: new Set<string>(),
    _cacheProfiles: cacheProfiles,

    waitUntil(fn: () => Promise<void>): void {
      if (ctx.build) return;
      // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
      // non-async callback becomes a rejected promise handed to the host's
      // waitUntil (logged as a background failure), instead of escaping into
      // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
      const task = Promise.resolve().then(fn);
      // Track the task promise so the PPR shell capture can settle the
      // foreground's deferred cache writes before its own match/render (the
      // ordering edge; see _pendingBackgroundTasks). The capture task itself
      // opts out via the marker — the drain must never await its own promise.
      if (
        !(fn as { [UNTRACKED_BACKGROUND_TASK]?: boolean })[
          UNTRACKED_BACKGROUND_TASK
        ]
      ) {
        ctx._pendingBackgroundTasks?.push(task);
      }
      if (executionContext?.waitUntil) {
        executionContext.waitUntil(task);
      } else {
        // Node/dev fallback: fire-and-forget with error logging (the same
        // policy fireAndForgetWaitUntil applies).
        task.catch((err) =>
          console.error("[waitUntil] Background task failed:", err),
        );
      }
    },

    executionContext,

    _onResponseCallbacks: [],
    _pendingBackgroundTasks: [],

    onResponse(callback: (response: Response) => Response): void {
      assertNotInsideCacheExec(ctx, "onResponse");
      assertResponseWriteAllowed("onResponse");
      this._onResponseCallbacks.push(callback);
    },

    get theme() {
      return themeConfig ? getTheme() : undefined;
    },
    setTheme: themeConfig
      ? (theme: Theme) => {
          assertNotInsideCacheExec(ctx, "setTheme");
          setTheme(theme);
        }
      : undefined,
    _themeConfig: themeConfig,

    setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
      assertNotInsideCacheExec(ctx, "setLocationState");
      const arr = Array.isArray(entries) ? entries : [entries];
      this._locationState = this._locationState
        ? [...this._locationState, ...arr]
        : arr;
    },
    _locationState: undefined,

    _reportedErrors: new WeakSet<object>(),
    _metricsStore: undefined,
    _renderForeground: undefined,
    _renderDiagnosticsEnabled: undefined,
    _handlerStart: undefined,

    _renderBarrier: null as any,
    _resolveRenderBarrier: null as any,
    _renderBarrierSegmentOrder: undefined,

    reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
  };

  wireRenderBarrier(ctx, handleStore);

  ctx.use = createUseFunction({
    handleStore,
    loaderPromises,
    getContext: () => ctx,
  });

  (ctx as any)[NOCACHE_SYMBOL] = true;
  return ctx;
}

/**
 * Wire a fresh render barrier onto `ctx`, closure-bound to THIS ctx and THIS
 * handle store. Called by createRequestContext for every fresh context, and by
 * deriveShellCaptureContext (rsc/shell-capture.ts) for the PPR capture's
 * derived context.
 *
 * The derived-context call is load-bearing (issue #684, plan 009): the capture
 * context is `Object.create(reqCtx)`, so without its own wiring every
 * `_renderBarrier*` read fell through the prototype to the FOREGROUND
 * request's barrier — whose getter and resolver are closure-bound to the
 * foreground ctx and its handle store, and whose resolver no-ops once
 * resolved. A bake-lane loader's `await ctx.rendered()` during capture then
 * resolved instantly against the foreground's barrier and `ctx.use(handle)`
 * read the foreground's handle snapshot; the capture's fresh `_handleStore`
 * was invisible, so foreground per-request handle data could bake into the
 * shared shell.
 */
export function wireRenderBarrier(
  ctx: RequestContext<any, any>,
  handleStore: HandleStore,
): void {
  // Reset the whole barrier family as OWN properties. No-op for a fresh
  // context; for the derived capture context this shadows the foreground's
  // resolved state so the capture runs its own barrier lifecycle. In
  // particular _treeHasStreaming must be recomputed for the CAPTURE's tree
  // (cache-lookup/segment-resolution only set it when undefined): an
  // inherited `true` made a capture-lane rendered() seal the capture's fresh
  // store at loader start and pair it with the foreground's segment order.
  ctx._renderBarrierSegmentOrder = undefined;
  ctx._renderBarrierWaiters = undefined;
  ctx._renderBarrierHandleSnapshot = undefined;
  ctx._renderBarrierGuardClosed = undefined;
  ctx._handlerLoaderDeps = undefined;
  ctx._treeHasStreaming = undefined;

  // Lazy allocation: only create the Promise when a loader calls rendered().
  let barrierResolved = false;
  let resolveBarrier: (() => void) | undefined;
  ctx._resolveRenderBarrier = (
    segments: Array<{ type: string; id: string }>,
  ) => {
    if (barrierResolved) return;
    barrierResolved = true;
    const segOrder = segments
      .filter((s) => s.type !== "loader")
      .map((s) => s.id);
    ctx._renderBarrierSegmentOrder = segOrder;

    const closeGuard = () => {
      ctx._renderBarrierWaiters = undefined;
      ctx._handlerLoaderDeps = undefined;
      ctx._renderBarrierGuardClosed = true;
    };

    if (ctx._treeHasStreaming) {
      handleStore.settled.then(closeGuard);
    } else {
      ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
        handleStore,
        segOrder,
      );
      closeGuard();
    }
    if (resolveBarrier) resolveBarrier();
  };
  // defineProperty, not assignment: on a derived context the prototype's
  // _renderBarrier may already be a non-writable data property (the getter
  // pins it after first access), which would reject a plain assignment.
  Object.defineProperty(ctx, "_renderBarrier", {
    get() {
      const p = barrierResolved
        ? Promise.resolve()
        : new Promise<void>((resolve) => {
            resolveBarrier = resolve;
          });
      Object.defineProperty(ctx, "_renderBarrier", {
        value: p,
        writable: false,
        configurable: false,
      });
      return p;
    },
    configurable: true,
  });
}

// Capture the Max-Age value so it can be parsed numerically. A leading zero
// (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
// to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
// zero-prefixed values like 05 / 010 as deletions.
const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;

function isCookieDeletion(header: string): boolean {
  const m = MAX_AGE_RE.exec(header);
  if (!m) return false;
  return Number(m[1]) <= 0;
}

function parseResponseCookies(response: Response): Map<string, string | null> {
  const result = new Map<string, string | null>();
  const setCookies = response.headers.getSetCookie();

  for (const header of setCookies) {
    const semiIdx = header.indexOf(";");
    const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
    const eqIdx = pair.indexOf("=");
    if (eqIdx === -1) continue;

    let name: string;
    let value: string;
    try {
      name = decodeURIComponent(pair.substring(0, eqIdx).trim());
      value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
    } catch {
      continue;
    }

    const isDeleted = isCookieDeletion(header);
    result.set(name, isDeleted ? null : value);
  }

  return result;
}

// Re-exported for unit tests and the existing import path. The implementation
// lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
// dispatcher) can share it without pulling this module's request-context graph.
export { parseCookiesFromHeader };

/** Reject CR/LF/;/,/whitespace/control so raw attribute interpolation cannot inject. */
function assertSafeCookieAttribute(
  value: string,
  label: "domain" | "path",
): void {
  for (let i = 0; i < value.length; i++) {
    const code = value.charCodeAt(i);
    if (code <= 0x20 || code === 0x7f || value[i] === ";" || value[i] === ",") {
      throw new Error(
        label === "domain" ? "invalid cookie domain" : "invalid cookie path",
      );
    }
  }
}

export function serializeCookieValue(
  name: string,
  value: string,
  options: CookieOptions = {},
): string {
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;

  if (options.domain) {
    assertSafeCookieAttribute(options.domain, "domain");
    cookie += `; Domain=${options.domain}`;
  }
  if (options.path) {
    if (!options.path.startsWith("/")) {
      throw new Error("invalid cookie path");
    }
    assertSafeCookieAttribute(options.path, "path");
    cookie += `; Path=${options.path}`;
  }
  if (options.maxAge !== undefined) {
    // Safe integer: Number.isInteger(1e21) is true but stringifies as "1e+21",
    // which is invalid Max-Age wire syntax. Number.isSafeInteger rejects that.
    if (
      typeof options.maxAge !== "number" ||
      !Number.isSafeInteger(options.maxAge)
    ) {
      throw new Error("invalid cookie maxAge");
    }
    cookie += `; Max-Age=${options.maxAge}`;
  }
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
  if (options.httpOnly) cookie += "; HttpOnly";
  if (options.secure) cookie += "; Secure";
  if (options.sameSite) {
    if (
      options.sameSite !== "strict" &&
      options.sameSite !== "lax" &&
      options.sameSite !== "none"
    ) {
      throw new Error("invalid cookie sameSite");
    }
    cookie += `; SameSite=${options.sameSite}`;
  }

  return cookie;
}

/**
 * Options for creating the use() function
 */
export interface CreateUseFunctionOptions<TEnv> {
  handleStore: HandleStore;
  loaderPromises: Map<string, Promise<any>>;
  getContext: () => RequestContext<TEnv>;
}

export function createUseFunction<TEnv>(
  options: CreateUseFunctionOptions<TEnv>,
): RequestContext["use"] {
  const { handleStore, loaderPromises, getContext } = options;

  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
    if (isHandle(item)) {
      const handle = item;
      const ctx = getContext();
      const segmentId = (ctx as any)._currentSegmentId;

      if (!segmentId) {
        throw new Error(
          `Handle "${handle.$$id}" used outside of handler context. ` +
            `Handles must be used within route/layout handlers.`,
        );
      }

      return withDefer(
        (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
          const valueOrPromise =
            typeof dataOrFn === "function"
              ? (dataOrFn as () => Promise<unknown>)()
              : dataOrFn;

          handleStore.push(handle.$$id, segmentId, valueOrPromise);
        },
      );
    }

    const loader = item as LoaderDefinition<any, any>;

    if (loaderPromises.has(loader.$$id)) {
      return loaderPromises.get(loader.$$id);
    }

    let loaderFn = loader.fn;
    if (!loaderFn) {
      const fetchable = getFetchableLoader(loader.$$id);
      if (fetchable) {
        loaderFn = fetchable.fn;
      }
    }

    if (!loaderFn) {
      throw new Error(
        `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`,
      );
    }

    const ctx = getContext();

    // Build the typed ctx.search the same way the render path
    // (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
    // parse the route's search schema over the cleaned searchParams. The base
    // RequestContext carries no `search` field, so reading `(ctx as any).search`
    // here always yielded {} — dropping typed search for action/dispatch loaders.
    const searchSchema = ctx._routeName
      ? getSearchSchema(ctx._routeName, ctx._routerId)
      : undefined;
    const loaderSearch = searchSchema
      ? parseSearchParams(ctx.searchParams, searchSchema)
      : {};

    const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
      params: ctx.params,
      routeParams: (ctx.params ?? {}) as Record<string, string>,
      request: ctx.request,
      searchParams: ctx.searchParams,
      search: loaderSearch,
      pathname: ctx.pathname,
      url: ctx.url,
      originalUrl: ctx.originalUrl,
      env: ctx.env as any,
      waitUntil: ctx.waitUntil.bind(ctx),
      executionContext: ctx.executionContext,
      get: ctx.get as any,
      use: (<TDep, TDepParams = any>(
        dep: LoaderDefinition<TDep, TDepParams>,
      ): Promise<TDep> => {
        return ctx.use(dep);
      }) as LoaderContext["use"],
      method: "GET",
      body: undefined,
      reverse: createReverseFunction(
        getGlobalRouteMap(),
        ctx._routeName,
        ctx.params as Record<string, string>,
        ctx._routeName
          ? isRouteRootScoped(ctx._routeName, ctx._routerId)
          : undefined,
      ),
      rendered: () => {
        throw new Error(
          `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
            `It cannot be used from request-context loaders or server actions.`,
        );
      },
    };

    // Meter through the same unified phase API as the loader-resolution funnel
    // (observePhase), so a loader resolved via this base request-context ctx.use
    // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
    // drift between the two ctx.use implementations.
    const promise = observePhase(PHASES.loader(loader.$$id), () =>
      Promise.resolve(loaderFn(loaderCtx)),
    );

    loaderPromises.set(loader.$$id, promise);

    return promise;
  }) as RequestContext["use"];
}
