import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  type HttpContractConfig,
  methodSupportsRequestBody,
} from "../contracts/index.js";
import {
  assertValidContractLifecycle,
  getContractOperationId,
} from "../contracts/lifecycle.js";
import {
  comparePathParamsToTemplate,
  formatPathParamsMismatch,
  getObjectSchemaShape,
} from "../contracts/schema-shape.js";
import { createErrorResponseBody } from "../errors/index.js";
import { runWithMemoScope } from "../memo/index.js";
import type { AnyPorts } from "../ports/index.js";
import { isUnboundPort } from "../ports/index.js";
import type {
  InferProviderPorts,
  ProviderSetupResult,
  ServiceProvider,
} from "../providers/index.js";
import type {
  ServerContextConfig,
  ServiceContextInputArgs,
} from "./context.js";
import { createContextFinalizer, resolveServerContext } from "./context.js";
import type { ContractLike, ResolveContract } from "./contract-like.js";
import { resolveContract } from "./contract-like.js";
import type {
  Handler,
  HttpRequestLike,
  HttpResponse,
  ResolvedRoute,
  RouteHook,
  ServerCaughtErrorHook,
  ServerHook,
  ServerUnhandledErrorMapper,
} from "./http.js";
import type { ServerInstrumentationOptions } from "./instrumentation.js";
import { createServerInstrumentation } from "./instrumentation.js";
import { loadProviderConfig } from "./providers/index.js";
import type { ActiveRequestContext } from "./request-context.js";
import {
  enterActiveRequestContext,
  readContextActor,
  readContextTenant,
  runWithActiveRequestContext,
} from "./request-context.js";
import {
  buildHandler,
  createMemoScopeRecorder,
  createRequestExecutor,
} from "./request-executor.js";
import type { RequestBodyOptions } from "./request-preparation.js";
import { errorResponse } from "./response-finalization.js";
import {
  contractsFromRoutes,
  createRoutes,
  defineRoutes,
  type HandlerRouteDef,
  type RouteDef,
  type RouteDefinitionBuilder,
  type RouteGroup,
  type RouteGroupBuilder,
  type Routes,
} from "./route-definitions.js";
import {
  type CompiledPath,
  compareRouteSpecificity,
  compilePath,
} from "./route-matching.js";
import type { RuntimeIntegrityCheck } from "./runtime-integrity.js";
import { runRuntimeIntegrityCheck } from "./runtime-integrity.js";
import {
  resolveTrustedRequest,
  type TrustedProxyConfig,
  type TrustedRequestInfo,
} from "./trusted-proxy.js";
import { assertValidTrustedProxyConfig } from "./trusted-proxy-internal.js";
import {
  createUseCaseRouteHandler,
  isUseCaseRouteDef,
} from "./use-case-route.js";

export type {
  HandlerRouteDef,
  RequestBodyOptions,
  RouteDef,
  RouteDefinitionBuilder,
  RouteGroup,
  RouteGroupBuilder,
  Routes,
};
export { contractsFromRoutes, createRoutes, defineRoutes };

/**
 * Loosely typed provider list element.
 *
 * Required-port, app-context, and service-input generics are erased here so
 * providers created with the typed `createProvider<Requires, Context,
 * ServiceInput>()` form stay assignable to server provider lists.
 */
type AnyServiceProvider = ServiceProvider<
  unknown,
  // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
  StandardSchemaV1<any, any>,
  AnyPorts,
  // biome-ignore lint/suspicious/noExplicitAny: provider context types are erased at this level
  any,
  // biome-ignore lint/suspicious/noExplicitAny: provider service-input types are erased at this level
  any
>;

function copyTrustedRequestInfo(
  requestInfo: TrustedRequestInfo,
): TrustedRequestInfo {
  return {
    ...requestInfo,
    url: new URL(requestInfo.url),
  };
}

/**
 * Options for creating a Beignet server instance.
 */
export type CreateServerOptions<
  Ctx,
  Ports extends AnyPorts,
  ServiceInput = void,
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
  Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
  Providers extends readonly AnyServiceProvider[] = readonly [],
> = {
  /**
   * App-owned ports available to context creation, hooks, and handlers.
   */
  ports: Ports;
  /**
   * Providers installed during server startup.
   *
   * Provider ports are merged into `ports` before request handling and are
   * stopped in reverse setup order when `server.stop()` runs.
   */
  providers?: Providers;

  /**
   * Runtime env used by providers. Defaults to `process.env`.
   */
  providerEnv?: Record<string, string | undefined>;
  /**
   * Provider config overrides keyed by provider name.
   */
  providerConfig?: Record<string, unknown>;

  /**
   * Context blueprint for request and service contexts.
   *
   * Gate-less contexts may pass a plain request factory. Contexts with a
   * `gate` property must use the blueprint form
   * `{ gate: (ports) => ports.gate, request, service }` so the server owns
   * gate attachment and identity changes can never go stale.
   *
   * The `ports` argument includes app ports plus ports provided during server
   * startup.
   */
  context: ServerContextConfig<
    Ctx,
    Ports & InferProviderPorts<Providers>,
    ServiceInput
  >;
  /**
   * Explicit policy for trusting proxy- or edge-provided request metadata.
   *
   * Forwarding headers are ignored by default. Configure this only when every
   * request reaches the app through a trusted platform or reverse proxy that
   * strips or normalizes those headers.
   */
  trustedProxy?: TrustedProxyConfig;
  /**
   * Server hooks that wrap every registered route.
   */
  hooks?: ServerHook<Ctx, Ports & InferProviderPorts<Providers>>[];
  /**
   * Optional pure startup check for app workflow registrations.
   *
   * Runtime integrity compares app-declared workflow artifacts against the
   * registries passed to runtime entrypoints. It performs no filesystem,
   * provider, database, network, worker, or background-loop work, so it is safe
   * to run during serverless cold starts.
   */
  integrity?: RuntimeIntegrityCheck;
  /**
   * Server-owned request instrumentation.
   *
   * The server resolves a request ID and W3C trace context for every request
   * before user hooks and context creation, writes `x-request-id` and
   * `traceparent` response headers, and records request and error events into
   * the resolved provider instrumentation port (`ports.instrumentation`, then
   * `ports.devtools`) when one is installed.
   *
   * Pass `false` to disable headers and event recording. Context factories
   * still receive `requestId` and `trace` arguments.
   */
  instrumentation?: ServerInstrumentationOptions<Ctx> | false;
  /**
   * Whether route-owned responses are validated against the contract's
   * declared statuses and response schemas before they are sent.
   *
   * Disable this to trade response guarantees for throughput, mirroring the
   * client-side `validateResponses` option.
   *
   * @default true
   */
  validateResponses?: boolean;
  /**
   * Request body parsing limits for JSON/text contract routes.
   */
  requestBody?: RequestBodyOptions;
  /**
   * Route list to register up front.
   */
  routes?: Routes;
  /**
   * How to handle ports that are still unbound after all providers have
   * started.
   *
   * Ports declared as `deferred` in `definePorts(...)` boot as throwing
   * placeholders until a provider contributes them. The default `"error"`
   * fails startup and lists the unbound port keys. Apps that bind every port
   * directly are unaffected.
   *
   * @default "error"
   */
  onUnboundPorts?: "error" | "warn" | "ignore";
  /**
   * Global caught-error observer.
   */
  onCaughtError?: ServerCaughtErrorHook<Ctx>;
  /**
   * Global mapper for unexpected errors not handled by app error catalogs.
   */
  mapUnhandledError?: ServerUnhandledErrorMapper<Ctx>;
};

interface RouteBuilder<Ctx, C extends HttpContractConfig> {
  handle: (
    fn: Handler<Ctx, C>,
  ) => (req: HttpRequestLike) => Promise<HttpResponse>;
}

/**
 * Identity and hook metadata for a raw route.
 *
 * Raw routes are mounted by the adapter at their own path, so `method` and
 * `path` describe the route to hooks, instrumentation, and error mapping
 * instead of driving routing. `metadata` feeds metadata-driven hooks exactly
 * like contract metadata does, for example `rateLimit` and `idempotency`.
 */
export type RawRouteInit = {
  name: string;
  method: HttpContractConfig["method"];
  path: string;
  metadata?: HttpContractConfig["metadata"];
};

/**
 * Builder returned by `server.rawRoute(...)`.
 */
export interface RawRouteBuilder<Ctx> {
  handle: (
    fn: Handler<Ctx, HttpContractConfig>,
  ) => (req: HttpRequestLike) => Promise<HttpResponse>;
}

/**
 * Runtime server object returned by `createServer(...)`.
 */
export interface ServerInstance<
  Ctx,
  Ports extends AnyPorts = AnyPorts,
  ServiceInput = void,
> {
  /**
   * Catch-all request handler for platform adapters.
   */
  api: (req: HttpRequestLike) => Promise<HttpResponse>;
  /**
   * Register and build a single route handler imperatively.
   */
  route: <CLike extends ContractLike>(
    contractLike: CLike,
  ) => RouteBuilder<Ctx, ResolveContract<CLike>>;
  /**
   * Build a handler for a route that cannot be a contract — webhooks,
   * third-party auth callbacks, streaming endpoints — that still runs the
   * whole server pipeline: correlation, `onRequest`, route hooks,
   * `beforeHandle`, `beforeSend`, `afterSend`, context creation,
   * instrumentation, and framework error mapping.
   *
   * Request parsing and validation are skipped and the request body is left
   * unconsumed, so the handler owns body reading — for example webhook
   * signature verification over the exact raw bytes. Metadata-driven hooks
   * such as rate limiting and idempotency read `init.metadata`. Raw routes
   * are not added to the route registry; mount the returned handler at the
   * route's own path.
   */
  rawRoute: (init: RawRouteInit) => RawRouteBuilder<Ctx>;
  /**
   * Build a fully assembled request context from a framework-neutral request.
   *
   * Use this for adapter entry points outside the route pipeline, such as
   * server components or upload routes.
   */
  createRequestContext: (req: HttpRequestLike) => Promise<Ctx>;
  /**
   * Build a fully assembled service context for schedules, outbox drains,
   * tasks, and background work.
   *
   * Requires `context.service` to be declared in `createServer(...)`.
   *
   * Only call this from long-lived runtimes such as servers, workers, and
   * `bun test`. It enters the ambient correlation context with
   * `AsyncLocalStorage.enterWith`, and resuming that frame across top-level
   * await crashes Bun 1.3.x in plain scripts. Seeds and one-off scripts must
   * use `runServiceContext(...)` instead.
   */
  createServiceContext: (
    ...args: ServiceContextInputArgs<ServiceInput>
  ) => Promise<Ctx>;
  /**
   * Build a service context and run `fn` inside a scoped ambient correlation
   * frame, returning its result.
   *
   * This is the script-safe counterpart to `createServiceContext(...)`: the
   * ambient frame is entered with `AsyncLocalStorage.run`, so plain scripts
   * such as seeds and one-off maintenance work stay safe under top-level
   * await. Requires `context.service` to be declared in `createServer(...)`.
   */
  runServiceContext: <T>(
    ...args: [
      ...ServiceContextInputArgs<ServiceInput>,
      fn: (ctx: Ctx) => T | Promise<T>,
    ]
  ) => Promise<T>;
  /**
   * Contract configs registered through the `routes` option.
   */
  contracts: readonly HttpContractConfig[];
  /**
   * Stop installed providers in reverse setup order.
   */
  stop: () => Promise<void>;
  /**
   * Final app ports after provider setup.
   */
  ports: Ports;
}

/**
 * Create a Beignet server instance.
 *
 * The server owns route registration, provider setup/startup, request
 * validation, hook execution, response validation, and framework error mapping.
 * Use adapter packages such as `@beignet/next` to expose `server.api` to a
 * specific runtime.
 *
 * @param options - Ports, providers, routes, hooks, context blueprint, and
 * error mapping hooks for the server.
 * @returns A started server instance with final ports and a catch-all handler.
 */
export async function createServer<
  Ctx,
  Ports extends AnyPorts,
  ServiceInput = void,
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
  Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
  Providers extends readonly AnyServiceProvider[] = readonly [],
>(
  options: CreateServerOptions<Ctx, Ports, ServiceInput, Routes, Providers>,
): Promise<
  ServerInstance<Ctx, Ports & InferProviderPorts<Providers>, ServiceInput>
> {
  type RegisteredRoute = ResolvedRoute<Ctx, HttpContractConfig> & {
    compiled: CompiledPath;
    /**
     * Uppercased contract method, cached for dispatch.
     */
    method: string;
  };
  const registry: RegisteredRoute[] = [];
  // Routes can register after startup via server.route(...), so the registry
  // is re-sorted lazily before the next dispatch instead of on every
  // registration.
  let registryNeedsSort = false;
  type FinalPorts = Ports & InferProviderPorts<Providers>;
  const providers = (options.providers ?? []) as readonly AnyServiceProvider[];
  const env = options.providerEnv ?? process.env;
  const overrides = options.providerConfig ?? {};
  const providerResults: ProviderSetupResult<AnyPorts>[] = [];
  const finalPorts = { ...options.ports } as FinalPorts;
  const instrumentation = createServerInstrumentation<Ctx>(
    options.instrumentation,
  );
  const hooks = [
    ...(instrumentation.hook
      ? [instrumentation.hook as ServerHook<Ctx, FinalPorts>]
      : []),
    ...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
  ];
  const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
  const trustedProxy = options.trustedProxy ?? false;
  assertValidTrustedProxyConfig(trustedProxy);

  // Fail startup on hook misconfiguration before provider setup runs.
  for (const hook of hooks) {
    hook.validate?.({ contracts, trustedProxy });
  }

  const resolvedContext = resolveServerContext<Ctx, FinalPorts, ServiceInput>(
    options.context,
  );
  const finalizeContext = createContextFinalizer(
    resolvedContext,
    () => finalPorts,
  );
  const createRequestContext = async (
    req: HttpRequestLike,
    contract?: HttpContractConfig,
    requestInfo: TrustedRequestInfo = resolveTrustedRequest(req, trustedProxy),
  ): Promise<Ctx> => {
    const { requestId, trace } = instrumentation.prepareRequest(req);
    return finalizeContext(
      await resolvedContext.request({
        req,
        requestInfo: copyTrustedRequestInfo(requestInfo),
        ports: finalPorts,
        contract,
        requestId,
        trace,
      }),
    );
  };
  const createServiceContext = async (
    ...args: ServiceContextInputArgs<ServiceInput>
  ): Promise<Ctx> => {
    const serviceFactory = resolvedContext.service;
    if (!serviceFactory) {
      throw new Error(
        "Define context.service in createServer(...) to create service contexts.",
      );
    }

    const { requestId, trace } = instrumentation.createServiceCorrelation();
    // Enter the ambient context synchronously (before the factory awaits) so
    // it propagates to the caller's continuation. Identity fields are filled
    // onto the same object once the context is finalized, so jobs, listeners,
    // schedules, and tasks observe the service actor/tenant at record time.
    const ambient: ActiveRequestContext = {
      requestId,
      traceId: trace.traceId,
      spanId: trace.spanId,
      parentSpanId: trace.parentSpanId,
      traceparent: trace.traceparent,
    };
    enterActiveRequestContext(ambient);
    const ctx = finalizeContext(
      await serviceFactory({
        ports: finalPorts,
        input: args[0] as ServiceInput,
        requestId,
        trace,
      }),
    );
    ambient.actor = readContextActor(ctx);
    ambient.tenant = readContextTenant(ctx);
    return ctx;
  };
  const runServiceContext = async <T>(
    ...args: [
      ...ServiceContextInputArgs<ServiceInput>,
      fn: (ctx: Ctx) => T | Promise<T>,
    ]
  ): Promise<T> => {
    const serviceFactory = resolvedContext.service;
    if (!serviceFactory) {
      throw new Error(
        "Define context.service in createServer(...) to create service contexts.",
      );
    }

    const fn = args[args.length - 1] as (ctx: Ctx) => T | Promise<T>;
    const input = (args.length > 1 ? args[0] : undefined) as ServiceInput;
    const { requestId, trace } = instrumentation.createServiceCorrelation();
    const ambient: ActiveRequestContext = {
      requestId,
      traceId: trace.traceId,
      spanId: trace.spanId,
      parentSpanId: trace.parentSpanId,
      traceparent: trace.traceparent,
    };
    // AsyncLocalStorage.run scopes the ambient frame to this callback, so the
    // caller's continuation never resumes through an enterWith frame — the
    // pattern that crashes Bun 1.3.x in plain scripts under top-level await.
    // The memo scope shares the callback's lifetime, so createMemo(...)
    // wrappers dedupe lookups across the context factory and fn. The
    // createServiceContext(...) form has no such boundary and stays
    // unscoped: memoized functions call through uncached there.
    return runWithActiveRequestContext(ambient, () =>
      runWithMemoScope(
        { record: createMemoScopeRecorder(finalPorts) },
        async () => {
          const ctx = finalizeContext(
            await serviceFactory({
              ports: finalPorts,
              input,
              requestId,
              trace,
            }),
          );
          ambient.actor = readContextActor(ctx);
          ambient.tenant = readContextTenant(ctx);
          return await fn(ctx);
        },
      ),
    );
  };
  const contextRuntime = {
    createRequestContext,
    finalizeContext,
    resolveRequestInfo: (req: HttpRequestLike) =>
      resolveTrustedRequest(req, trustedProxy),
  };

  let serviceContextsAvailable = false;
  const lifecycleCreateServiceContext = async (
    input?: unknown,
  ): Promise<unknown> => {
    if (!serviceContextsAvailable) {
      throw new Error(
        "Service contexts are unavailable until providers have started.",
      );
    }

    return createServiceContext(
      ...([input] as ServiceContextInputArgs<ServiceInput>),
    );
  };

  let stopped = false;
  const stop = async () => {
    if (stopped) return;
    stopped = true;
    const errors: unknown[] = [];
    for (let i = providerResults.length - 1; i >= 0; i -= 1) {
      const result = providerResults[i];
      try {
        await result?.stop?.({
          ports: finalPorts,
          createServiceContext: lifecycleCreateServiceContext,
        });
      } catch (err) {
        errors.push(err);
      }
    }
    if (errors.length) {
      throw new AggregateError(errors, "Provider shutdown errors");
    }
  };

  const registeredPaths = new Set<string>();
  const registeredShapes = new Map<string, string>();
  const registeredNames = new Map<string, string>();
  const registeredOperationIds = new Map<string, string>();

  const registerRoute = <C extends HttpContractConfig>(
    contract: C,
    handler: Handler<Ctx, C>,
    routeHooks: readonly RouteHook<unknown, object>[] = [],
    responseValidationExemptStatus?: number,
  ): void => {
    assertValidContractLifecycle(contract);
    if (contract.body && !methodSupportsRequestBody(contract.method)) {
      throw new Error(
        `Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
      );
    }
    const compiled = compilePath(contract.path);
    const normalizedPath = compiled.normalizedPath;
    const routeKey = `${contract.method.toUpperCase()} ${normalizedPath}`;
    if (registeredPaths.has(routeKey)) {
      throw new Error(
        `Duplicate route: ${routeKey} is already registered. Each method + path combination must be unique.`,
      );
    }
    const shapeRouteKey = `${contract.method.toUpperCase()} ${compiled.shapeKey}`;
    const conflictingRoute = registeredShapes.get(shapeRouteKey);
    if (conflictingRoute) {
      throw new Error(
        `Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`,
      );
    }
    const conflictingName = registeredNames.get(contract.name);
    if (conflictingName) {
      throw new Error(
        `Duplicate contract name: "${contract.name}" is registered for both ${conflictingName} and ${routeKey}. Contract names must be unique because typed clients, OpenAPI operations, and devtools key on them.`,
      );
    }
    const operationId = getContractOperationId(contract);
    const conflictingOperationId = registeredOperationIds.get(operationId);
    if (conflictingOperationId) {
      throw new Error(
        `Duplicate OpenAPI operationId: "${operationId}" is registered for both ${conflictingOperationId} and ${routeKey}. Operation IDs must be unique across the registered route surface.`,
      );
    }
    if (contract.pathParams) {
      const shape = getObjectSchemaShape(contract.pathParams);
      if (shape) {
        const { missingKeys, extraKeys } = comparePathParamsToTemplate({
          pathKeys: compiled.keys,
          shapeKeys: Object.keys(shape),
        });
        if (missingKeys.length > 0 || extraKeys.length > 0) {
          const details = formatPathParamsMismatch({ missingKeys, extraKeys });
          throw new Error(
            `Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}). Path templates and pathParams schemas drive routing, clients, and OpenAPI together.`,
          );
        }
      }
    }
    registeredPaths.add(routeKey);
    registeredShapes.set(shapeRouteKey, routeKey);
    registeredNames.set(contract.name, routeKey);
    registeredOperationIds.set(operationId, routeKey);

    const builtHandler = buildHandler(
      options,
      finalPorts,
      contextRuntime,
      contract,
      handler,
      hooks,
      routeHooks,
      undefined,
      responseValidationExemptStatus,
    );
    registry.push({
      contract,
      compiled,
      method: contract.method.toUpperCase(),
      handler: builtHandler,
      match: (method, pathname) => {
        if (contract.method.toUpperCase() !== method.toUpperCase()) {
          return { matched: false as const };
        }
        const match = compiled.pattern.exec(pathname);
        if (!match) return { matched: false as const };
        return { matched: true as const };
      },
    });
    registryNeedsSort = true;
  };

  const createBuilder = <C extends HttpContractConfig>(
    contract: C,
    shouldRegister: boolean,
  ): RouteBuilder<Ctx, C> => ({
    handle: (fn) => {
      const wrapped = buildHandler(
        options,
        finalPorts,
        contextRuntime,
        contract,
        fn,
        hooks,
      );
      if (shouldRegister) registerRoute(contract, fn);
      return wrapped;
    },
  });

  if (options.routes) {
    try {
      for (const route of options.routes) {
        const contract = resolveContract(route.contract);
        const hasHandle = typeof route.handle === "function";
        const hasUseCase = isUseCaseRouteDef(route);

        if (hasHandle && hasUseCase) {
          throw new Error(
            `Route for contract "${contract.name}" declares both "handle" and "useCase". Bind the contract to exactly one of them.`,
          );
        }
        if (!hasHandle && !hasUseCase) {
          throw new Error(
            `Route for contract "${contract.name}" declares neither "handle" nor "useCase". Bind the contract to a use case or implement a handler.`,
          );
        }

        if (isUseCaseRouteDef(route)) {
          const { handler, responseValidationExemptStatus } =
            createUseCaseRouteHandler<Ctx, typeof contract>(contract, route);
          registerRoute(
            contract,
            handler,
            route.hooks as readonly RouteHook<unknown, object>[] | undefined,
            responseValidationExemptStatus,
          );
        } else {
          registerRoute(
            contract,
            route.handle as Handler<Ctx, typeof contract>,
            route.hooks as readonly RouteHook<unknown, object>[] | undefined,
          );
        }
      }
    } catch (error) {
      try {
        await stop();
      } catch (cleanupError) {
        throw new AggregateError(
          [error, cleanupError],
          "Server initialization failed and provider cleanup failed",
        );
      }
      throw error;
    }
  }

  runRuntimeIntegrityCheck(options.integrity);

  try {
    for (const provider of providers) {
      const cfg = await loadProviderConfig(provider, env, overrides);
      const result = await provider.setup({
        ports: finalPorts,
        config: cfg,
        createServiceContext: lifecycleCreateServiceContext,
      });
      if (result.ports) {
        Object.assign(finalPorts, result.ports);
      }
      providerResults.push(result);
    }

    for (const result of providerResults) {
      if (!result.start) continue;
      await result.start({
        ports: finalPorts,
        createServiceContext: lifecycleCreateServiceContext,
      });
    }

    instrumentation.attachPorts(finalPorts);

    const onUnboundPorts = options.onUnboundPorts ?? "error";
    if (onUnboundPorts !== "ignore") {
      const unboundKeys = Object.keys(finalPorts).filter((key) =>
        isUnboundPort((finalPorts as AnyPorts)[key]),
      );
      if (unboundKeys.length > 0) {
        const message =
          `Unbound ports after provider startup: ${unboundKeys.join(", ")}. ` +
          "Each port declared as deferred in definePorts(...) must be contributed " +
          "by a provider (server/providers.ts) or bound in infra/port-wiring.ts. " +
          'Pass onUnboundPorts: "warn" or "ignore" to change this behavior.';
        if (onUnboundPorts === "error") {
          throw new Error(message);
        }
        console.warn(`[beignet] ${message}`);
      }
    }

    serviceContextsAvailable = true;
  } catch (error) {
    try {
      await stop();
    } catch (cleanupError) {
      throw new AggregateError(
        [error, cleanupError],
        "Server initialization failed and provider cleanup failed",
      );
    }
    throw error;
  }

  // The fallback 404/405 pipeline is built once at server creation. Only the
  // contract surface that hooks observe (method, path, and the Allow set for
  // 405s) is assembled per unmatched request.
  const executeFallback = createRequestExecutor<
    Ctx,
    FinalPorts,
    HttpContractConfig
  >(options, finalPorts, contextRuntime, hooks, [], {
    skipRoutePreparation: true,
  });

  const fallbackContract = (
    name: string,
    method: string,
    path: string,
  ): HttpContractConfig => ({
    kind: "http",
    name,
    method: method as HttpContractConfig["method"],
    path,
    pathParams: null,
    query: null,
    body: null,
    responses: {},
    metadata: {},
  });

  const notFoundHandler: Handler<Ctx, HttpContractConfig> = async () =>
    errorResponse(404, "NOT_FOUND", "Not found");

  const api = async (req: HttpRequestLike) => {
    if (registryNeedsSort) {
      registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
      registryNeedsSort = false;
    }

    const url = new URL(req.url);
    const method = req.method.toUpperCase();

    let pathMatchedMethods: Set<string> | undefined;
    let headCandidate: RegisteredRoute | undefined;
    for (const entry of registry) {
      if (!entry.compiled.pattern.test(url.pathname)) continue;
      if (method !== "HEAD" && entry.method === method) {
        return await entry.handler(req);
      }
      if (!pathMatchedMethods) {
        pathMatchedMethods = new Set();
      }
      pathMatchedMethods.add(entry.method);
      if (method === "HEAD" && entry.method === "HEAD") {
        if (
          !headCandidate ||
          (headCandidate.method === "GET" &&
            headCandidate.compiled.shapeKey === entry.compiled.shapeKey)
        ) {
          headCandidate = entry;
        }
      }
      if (entry.method === "GET") {
        pathMatchedMethods.add("HEAD");
        if (method === "HEAD" && !headCandidate) {
          headCandidate = entry;
        }
      }
    }

    if (headCandidate) {
      return await headCandidate.handler(req);
    }

    const pathname = url.pathname || "/";

    if (pathMatchedMethods) {
      const allow = [...pathMatchedMethods].sort().join(", ");

      return await executeFallback(
        {
          contract: fallbackContract("methodNotAllowed", method, pathname),
          handler: async () => ({
            status: 405,
            headers: { allow },
            body: createErrorResponseBody({
              code: "METHOD_NOT_ALLOWED",
              message: `Method ${method} is not allowed for ${pathname}`,
            }),
          }),
        },
        req,
        {},
      );
    }

    return await executeFallback(
      {
        contract: fallbackContract("notFound", method, pathname),
        handler: notFoundHandler,
      },
      req,
      {},
    );
  };

  return {
    api,
    route: (contractLike) => {
      const contract = resolveContract(contractLike);
      return createBuilder(contract, true);
    },
    rawRoute: (init) => ({
      handle: (fn) => {
        const built = buildHandler(
          options,
          finalPorts,
          contextRuntime,
          rawRouteContract(init),
          fn,
          hooks,
          [],
          { rawRoute: true },
        );
        // The adapter owns routing for raw routes — the handler is mounted
        // at its own path — so path/method matching is skipped and
        // `init.path` stays identity for hooks and instrumentation.
        return (req) => built(req, {});
      },
    }),
    createRequestContext: (req) => createRequestContext(req),
    createServiceContext,
    runServiceContext,
    contracts,
    stop,
    ports: finalPorts,
  };
}

function rawRouteContract(init: RawRouteInit): HttpContractConfig {
  return {
    kind: "http",
    name: init.name,
    method: init.method,
    path: init.path,
    pathParams: null,
    query: null,
    body: null,
    responses: {},
    metadata: init.metadata ?? {},
  };
}
