/**
 * Layer-based server-side routing for Effect HTTP applications.
 *
 * `HttpRouter` collects routes and router middleware while an application layer
 * is being built, then exposes the registered route table as an
 * `HttpServerResponse` effect for each incoming `HttpServerRequest`. Use it for
 * APIs, webhooks, and Fetch handlers that want request-scoped services, schema
 * decoding, and typed middleware to participate in the same `Layer` graph as
 * the rest of the application.
 *
 * **Mental model**
 *
 * Route layers do not handle requests immediately. They register `Route` values
 * into the current `HttpRouter`; `serve`, `toHttpEffect`, and `toWebHandler`
 * build the layer, read the completed router, and run the matching handler for
 * each request. During a request, the router provides `HttpServerRequest`,
 * `Scope`, parsed search parameters, and `RouteContext`, so handlers can decode
 * path, search, and body data or access services supplied by router middleware.
 *
 * **Common tasks**
 *
 * - Register individual routes with `add` or batches with `addAll`.
 * - Group routes under a prefix with `addAll(..., { prefix })` or
 *   `router.prefixed`.
 * - Decode route, search, and JSON body data with `schemaPathParams`,
 *   `schemaParams`, `schemaNoBody`, and `schemaJson`.
 * - Apply route-scoped middleware with `middleware(...).layer`, or install
 *   global router middleware for all routes.
 * - Serve the layer with `serve` or create a Fetch-compatible handler with
 *   `toWebHandler`.
 *
 * **Gotchas**
 *
 * Paths must be absolute `/` paths or the wildcard `*`. `HEAD` falls back to
 * `GET`, and wildcard paths ending in `/*` also match the prefix path itself.
 * For prefixed routes the matched prefix is removed from
 * `HttpServerRequest.url` before the handler sees it. Middleware passed to
 * `serve` wraps the wider server chain; use router middleware when you need to
 * provide request dependencies, handle configured route errors, or change the
 * response that will be sent.
 *
 * @since 4.0.0
 */
import * as Arr from "../../Array.js";
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import { compose, dual, identity } from "../../Function.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as Schema from "../../Schema.js";
import * as Scope from "../../Scope.js";
import * as Tracer from "../../Tracer.js";
import * as FindMyWay from "./FindMyWay.js";
import * as HttpEffect from "./HttpEffect.js";
import * as HttpMiddleware from "./HttpMiddleware.js";
import * as HttpServer from "./HttpServer.js";
import * as HttpServerError from "./HttpServerError.js";
import * as HttpServerRequest from "./HttpServerRequest.js";
import * as HttpServerResponse from "./HttpServerResponse.js";
const TypeId = "~effect/http/HttpRouter";
/**
 * Service tag for the HTTP router used while constructing an HTTP application.
 * Route and middleware layers require this service to register themselves with
 * the router.
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const HttpRouter = /*#__PURE__*/Context.Service("effect/http/HttpRouter");
/**
 * Constructs an empty `HttpRouter` service.
 *
 * **Details**
 *
 * The returned router accepts route and middleware registrations and later routes
 * the current `HttpServerRequest` to the matching `HttpServerResponse`.
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const make = /*#__PURE__*/Effect.gen(function* () {
  const router = FindMyWay.make(yield* RouterConfig);
  const middleware = new Set();
  const addAll = routes => Effect.contextWith(context => {
    const middleware = getMiddleware(context);
    const applyMiddleware = effect => {
      for (let i = 0; i < middleware.length; i++) {
        effect = middleware[i](effect);
      }
      return effect;
    };
    for (let i = 0; i < routes.length; i++) {
      const route = middleware.length === 0 ? routes[i] : makeRoute({
        ...routes[i],
        handler: applyMiddleware(routes[i].handler)
      });
      if (route.method === "*") {
        if (route.path.endsWith("/*")) {
          router.all(route.path, route);
          router.all(route.path.slice(0, -2), route);
        } else {
          router.all(route.path, route);
        }
      } else {
        if (route.path.endsWith("/*")) {
          router.on(route.method, route.path, route);
          router.on(route.method, route.path.slice(0, -2), route);
        } else {
          router.on(route.method, route.path, route);
        }
      }
    }
    return Effect.void;
  });
  return HttpRouter.of({
    [TypeId]: TypeId,
    prefixed(prefix) {
      return HttpRouter.of({
        ...this,
        prefixed: newPrefix => this.prefixed(prefixPath(prefix, newPrefix)),
        addAll: routes => addAll(routes.map(prefixRoute(prefix))),
        add: (method, path, handler, options) => addAll([makeRoute({
          method,
          path: prefixPath(path, prefix),
          handler: HttpServerResponse.isHttpServerResponse(handler) ? Effect.succeed(handler) : Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler),
          uninterruptible: options?.uninterruptible ?? false,
          prefix
        })])
      });
    },
    addAll,
    add: (method, path, handler, options) => addAll([route(method, path, handler, options)]),
    addGlobalMiddleware: middleware_ => Effect.sync(() => {
      middleware.add(middleware_);
    }),
    asHttpEffect() {
      let handler = Effect.withFiber(fiber => {
        const contextMap = new Map(fiber.context.mapUnsafe);
        const request = contextMap.get(HttpServerRequest.HttpServerRequest.key);
        let result = router.find(request.method, request.url);
        if (result === undefined && request.method === "HEAD") {
          result = router.find("GET", request.url);
        }
        if (result === undefined) {
          return Effect.fail(new HttpServerError.HttpServerError({
            reason: new HttpServerError.RouteNotFound({
              request
            })
          }));
        }
        const route = result.handler;
        if (Option.isSome(route.prefix)) {
          contextMap.set(HttpServerRequest.HttpServerRequest.key, sliceRequestUrl(request, route.prefix.value));
        }
        contextMap.set(HttpServerRequest.ParsedSearchParams.key, result.searchParams);
        contextMap.set(RouteContext.key, {
          route,
          params: result.params
        });
        const span = contextMap.get(Tracer.ParentSpan.key);
        if (span && span._tag === "Span") {
          span.attribute("http.route", route.path);
        }
        return Effect.provideContext(route.uninterruptible ? route.handler : Effect.interruptible(route.handler), Context.makeUnsafe(contextMap));
      });
      if (middleware.size === 0) return handler;
      for (const fn of Arr.reverse(middleware)) {
        handler = fn(handler);
      }
      return handler;
    }
  });
});
function sliceRequestUrl(request, prefix) {
  const prefexLen = prefix.length;
  return request.modify({
    url: request.url.length <= prefexLen ? "/" : request.url.slice(prefexLen)
  });
}
/**
 * Context reference for low-level router configuration.
 *
 * **Details**
 *
 * The value is passed to the route matcher when an `HttpRouter` is created and
 * defaults to an empty configuration.
 *
 * @category configuration
 * @since 4.0.0
 */
export const RouterConfig = /*#__PURE__*/Context.Reference("effect/http/HttpRouter/RouterConfig", {
  defaultValue: () => ({})
});
/**
 * Service for the matched HTTP route in the current request.
 *
 * **When to use**
 *
 * Use to read captured path parameters and route metadata while handling a
 * request matched by the router.
 *
 * **Details**
 *
 * It provides the route definition and the path parameters captured by the route
 * matcher.
 *
 * @category services
 * @since 4.0.0
 */
export class RouteContext extends /*#__PURE__*/Context.Service()("effect/http/HttpRouter/RouteContext") {}
/**
 * Effect that returns the path parameters captured for the current matched route.
 *
 * @category getters
 * @since 4.0.0
 */
export const params = /*#__PURE__*/Effect.map(RouteContext, _ => _.params);
/**
 * Decodes a schema from the current request and its JSON body.
 *
 * **Details**
 *
 * The input passed to the schema includes the request method, URL, headers,
 * cookies, path parameters, search parameters, and parsed JSON body. The effect
 * fails if the body cannot be parsed or the schema decode fails.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaJson = (schema, options) => {
  const parse = Schema.decodeUnknownEffect(schema);
  return Effect.contextWith(context => {
    const request = Context.get(context, HttpServerRequest.HttpServerRequest);
    const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
    const routeContext = Context.get(context, RouteContext);
    return Effect.flatMap(request.json, body => parse({
      method: request.method,
      url: request.url,
      headers: request.headers,
      cookies: request.cookies,
      pathParams: routeContext.params,
      searchParams,
      body
    }, options));
  });
};
/**
 * Decodes a schema from the current request without reading the request body.
 *
 * **Details**
 *
 * The input passed to the schema includes the request method, URL, headers,
 * cookies, path parameters, and search parameters.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaNoBody = (schema, options) => {
  const parse = Schema.decodeUnknownEffect(schema);
  return Effect.contextWith(context => {
    const request = Context.get(context, HttpServerRequest.HttpServerRequest);
    const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
    const routeContext = Context.get(context, RouteContext);
    return parse({
      method: request.method,
      url: request.url,
      headers: request.headers,
      cookies: request.cookies,
      pathParams: routeContext.params,
      searchParams
    }, options);
  });
};
/**
 * Decodes a schema from the current route path parameters and search parameters.
 *
 * **Details**
 *
 * When the same key appears in both sources, the path parameter value is used.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaParams = (schema, options) => {
  const parse = Schema.decodeUnknownEffect(schema);
  return Effect.contextWith(context => {
    const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
    const routeContext = Context.get(context, RouteContext);
    return parse({
      ...searchParams,
      ...routeContext.params
    }, options);
  });
};
/**
 * Decodes a schema from the path parameters captured for the current matched
 * route.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaPathParams = (schema, options) => {
  const parse = Schema.decodeUnknownEffect(schema);
  return Effect.flatMap(params, _ => parse(_, options));
};
/**
 * Creates a layer that accesses the current `HttpRouter` service and runs the
 * supplied effect.
 *
 * **When to use**
 *
 * Use when you use it to register routes or middleware with the router during layer
 * construction.
 *
 * **Example** (Registering routes during layer construction)
 *
 * ```ts
 * import { Effect, Layer } from "effect"
 * import { HttpRouter } from "effect/unstable/http"
 *
 * const MyRoute = Layer.effectDiscard(Effect.gen(function*() {
 *   const router = yield* HttpRouter.HttpRouter
 *
 *   // then use `yield* router.add(...)` to add a route
 * }))
 * ```
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const use = f => Layer.effectDiscard(Effect.flatMap(HttpRouter, f));
/**
 * Create a layer that adds a single route to the HTTP router.
 *
 * **Example** (Adding a GET route)
 *
 * ```ts
 * import { Effect } from "effect"
 * import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
 *
 * const Route = HttpRouter.add(
 *   "GET",
 *   "/hello",
 *   Effect.succeed(HttpServerResponse.text("Hello, World!"))
 * )
 * ```
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const add = (method, path, handler, options) => use(router => router.add(method, path, handler, options));
/**
 * Create a layer that adds multiple routes to the HTTP router.
 *
 * **Example** (Adding multiple routes)
 *
 * ```ts
 * import { Effect } from "effect"
 * import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
 *
 * const Routes = HttpRouter.addAll([
 *   HttpRouter.route(
 *     "GET",
 *     "/hello",
 *     Effect.succeed(HttpServerResponse.text("Hello, World!"))
 *   )
 * ])
 * ```
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const addAll = (routes, options) => Layer.effectDiscard(Effect.gen(function* () {
  const toAdd = Effect.isEffect(routes) ? yield* routes : routes;
  let router = yield* HttpRouter;
  if (options?.prefix) {
    router = router.prefixed(options.prefix);
  }
  yield* router.addAll(toAdd);
}));
/**
 * Layer that provides a newly constructed `HttpRouter`.
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const layer = /*#__PURE__*/Layer.effect(HttpRouter)(make);
/**
 * Builds an application layer with a router and returns the router as an HTTP
 * handler effect.
 *
 * **Details**
 *
 * The returned effect handles the current `HttpServerRequest` in the current
 * `Scope`; route request markers are converted into the ordinary requirements of
 * the returned handler.
 *
 * @category HttpRouter
 * @since 4.0.0
 */
export const toHttpEffect = appLayer => Effect.gen(function* () {
  const context = yield* Layer.build(Layer.provideMerge(appLayer, layer));
  const router = Context.get(context, HttpRouter);
  // @effect-diagnostics effect/returnEffectInGen:off
  return router.asHttpEffect();
});
const RouteTypeId = "~effect/http/HttpRouter/Route";
const makeRoute = options => ({
  ...options,
  uninterruptible: options.uninterruptible ?? false,
  prefix: typeof options.prefix === "string" ? Option.some(options.prefix) : options.prefix ?? Option.none(),
  [RouteTypeId]: RouteTypeId
});
/**
 * Constructs a `Route` from an HTTP method, path, and handler.
 *
 * **Details**
 *
 * The handler may be a static response, an effect that produces a response, or a
 * function from the current request to a response effect. Set `uninterruptible` to
 * prevent the route handler from being made interruptible while it runs.
 *
 * @category Route
 * @since 4.0.0
 */
export const route = (method, path, handler, options) => makeRoute({
  ...options,
  method,
  path,
  handler: HttpServerResponse.isHttpServerResponse(handler) ? Effect.succeed(handler) : Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler),
  uninterruptible: options?.uninterruptible ?? false
});
const removeTrailingSlash = path => path.endsWith("/") ? path.slice(0, -1) : path;
/**
 * Adds a path prefix to a route path.
 *
 * **Details**
 *
 * Trailing slashes are removed from the prefix; `/` becomes the prefix itself and
 * `*` becomes a wildcard route under the prefix.
 *
 * @category PathInput
 * @since 4.0.0
 */
export const prefixPath = /*#__PURE__*/dual(2, (self, prefix) => {
  prefix = removeTrailingSlash(prefix);
  if (self === "*") return `${prefix}/*`;else if (self === "/") return prefix;
  return prefix + self;
});
/**
 * Returns a copy of a route with its path prefixed.
 *
 * **Details**
 *
 * The prefix is also tracked on the route so that, when the route handles a
 * request, the matched prefix can be removed from the request URL seen by the
 * handler.
 *
 * @category Route
 * @since 4.0.0
 */
export const prefixRoute = /*#__PURE__*/dual(2, (self, prefix) => makeRoute({
  ...self,
  path: prefixPath(self.path, prefix),
  prefix: Option.match(self.prefix, {
    onNone: () => prefix,
    onSome: existingPrefix => prefixPath(existingPrefix, prefix)
  })
}));
const MiddlewareTypeId = "~effect/http/HttpRouter/Middleware";
/**
 * Create a middleware layer that can be used to modify requests and responses.
 *
 * **Details**
 *
 * By default, the middleware only affects the routes that it is provided to.
 *
 * If you want to create a middleware that applies globally to all routes, pass
 * the `global` option as `true`.
 *
 * **Example** (Applying route and global middleware)
 *
 * ```ts
 * import { Context, Effect, Layer } from "effect"
 * import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/http"
 *
 * // Here we are defining a CORS middleware
 * const CorsMiddleware = HttpRouter.middleware(HttpMiddleware.cors()).layer
 * // You can also use HttpRouter.cors() to create a CORS middleware
 *
 * class CurrentSession extends Context.Service<CurrentSession, {
 *   readonly token: string
 * }>()("CurrentSession") {}
 *
 * // You can create middleware that provides a service to the HTTP requests.
 * const SessionMiddleware = HttpRouter.middleware<{
 *   provides: CurrentSession
 * }>()(
 *   Effect.gen(function*() {
 *     yield* Effect.log("SessionMiddleware initialized")
 *
 *     return (httpEffect) =>
 *       Effect.provideService(httpEffect, CurrentSession, {
 *         token: "dummy-token"
 *       })
 *   })
 * ).layer
 *
 * Effect.gen(function*() {
 *   const router = yield* HttpRouter.HttpRouter
 *   yield* router.add(
 *     "GET",
 *     "/hello",
 *     Effect.gen(function*() {
 *       // Requests can now access the current session
 *       const session = yield* CurrentSession
 *       return HttpServerResponse.text(
 *         `Hello, World! Your token is ${session.token}`
 *       )
 *     })
 *   )
 * }).pipe(
 *   Layer.effectDiscard,
 *   // Provide the SessionMiddleware & CorsMiddleware to some routes
 *   Layer.provide([SessionMiddleware, CorsMiddleware])
 * )
 * ```
 *
 * @category middleware
 * @since 4.0.0
 */
export const middleware = function () {
  if (arguments.length === 0) {
    return makeMiddleware;
  }
  return makeMiddleware(arguments[0], arguments[1]);
};
const makeMiddleware = (middleware, options) => options?.global ? Layer.effectDiscard(Effect.gen(function* () {
  const router = yield* HttpRouter;
  const fn = Effect.isEffect(middleware) ? yield* middleware : middleware;
  yield* router.addGlobalMiddleware(fn);
})) : new MiddlewareImpl(Effect.isEffect(middleware) ? Layer.effectContext(Effect.map(middleware, fn => Context.makeUnsafe(new Map([[fnContextKey, fn]])))) : Layer.succeedContext(Context.makeUnsafe(new Map([[fnContextKey, middleware]]))));
let middlewareId = 0;
const fnContextKey = "effect/http/HttpRouter/MiddlewareFn";
class MiddlewareImpl {
  [MiddlewareTypeId] = {};
  layerFn;
  dependencies;
  constructor(layerFn, dependencies) {
    this.layerFn = layerFn;
    this.dependencies = dependencies;
    const contextKey = `effect/http/HttpRouter/Middleware-${++middlewareId}`;
    this.layer = Layer.effectContext(Effect.gen({
      self: this
    }, function* () {
      const context = yield* Effect.context();
      const stack = [context.mapUnsafe.get(fnContextKey)];
      if (this.dependencies) {
        const memoMap = yield* Layer.CurrentMemoMap;
        const scope = Context.get(context, Scope.Scope);
        const depsContext = yield* Layer.buildWithMemoMap(this.dependencies, memoMap, scope);
        stack.push(...getMiddleware(depsContext));
      }
      return Context.makeUnsafe(new Map([[contextKey, stack]]));
    })).pipe(Layer.provide(this.layerFn));
  }
  layer;
  combine(other) {
    return new MiddlewareImpl(this.layerFn, this.dependencies ? Layer.provideMerge(this.dependencies, other.layer) : other.layer);
  }
}
const middlewareCache = /*#__PURE__*/new WeakMap();
const getMiddleware = context => {
  let arr = middlewareCache.get(context);
  if (arr) return arr;
  const topLevel = Arr.empty();
  let maxLength = 0;
  for (const [key, value] of context.mapUnsafe) {
    if (key.startsWith("effect/http/HttpRouter/Middleware-")) {
      topLevel.push(value);
      if (value.length > maxLength) {
        maxLength = value.length;
      }
    }
  }
  if (topLevel.length === 0) {
    arr = [];
  } else {
    const middleware = new Set();
    for (let i = maxLength - 1; i >= 0; i--) {
      for (const arr of topLevel) {
        if (i < arr.length) {
          middleware.add(arr[i]);
        }
      }
    }
    arr = Arr.fromIterable(middleware).reverse();
  }
  middlewareCache.set(context, arr);
  return arr;
};
/**
 * Middleware that applies CORS headers to the HTTP response.
 *
 * @category middleware
 * @since 4.0.0
 */
export const cors = options => middleware(HttpMiddleware.cors(options), {
  global: true
});
/**
 * Middleware that disables the logger for some routes.
 *
 * **Example** (Disabling route logging)
 *
 * ```ts
 * import { Effect, Layer } from "effect"
 * import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
 *
 * const Route = HttpRouter.add(
 *   "GET",
 *   "/hello",
 *   Effect.succeed(HttpServerResponse.text("Hello, World!"))
 * ).pipe(
 *   // disable the logger for this route
 *   Layer.provide(HttpRouter.disableLogger)
 * )
 * ```
 *
 * @category middleware
 * @since 4.0.0
 */
export const disableLogger = /*#__PURE__*/middleware(HttpMiddleware.withLoggerDisabled).layer;
/**
 * Provides request-level dependencies to some routes.
 *
 * @category middleware
 * @since 4.0.0
 */
export const provideRequest = layer => self => Layer.provide(self, middleware()(Effect.gen(function* () {
  const services = yield* Layer.build(layer);
  return effect => Effect.provideContext(effect, services);
})).layer);
/**
 * Runs the provided application layer as an HTTP server.
 *
 * @category server
 * @since 4.0.0
 */
export const serve = (appLayer, options) => {
  let middleware = options?.middleware;
  if (options?.disableLogger !== true) {
    middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger;
  }
  const RouterLayer = options?.routerConfig ? Layer.provide(layer, Layer.succeed(RouterConfig)(options.routerConfig)) : layer;
  return Effect.gen(function* () {
    const router = yield* HttpRouter;
    const handler = router.asHttpEffect();
    return middleware ? HttpServer.serve(handler, middleware) : HttpServer.serve(handler);
  }).pipe(Layer.unwrap, Layer.provideMerge(appLayer), Layer.provide(RouterLayer), options?.disableListenLog ? identity : HttpServer.withLogAddress);
};
/**
 * Builds a Fetch-compatible request handler from an HTTP router application
 * layer.
 *
 * **Details**
 *
 * The result contains a `handler` function that converts Web `Request` values to
 * Web `Response` values and a `dispose` function for releasing the layer
 * resources.
 *
 * @category server
 * @since 4.0.0
 */
export const toWebHandler = (appLayer, options) => {
  let middleware = options?.middleware;
  if (options?.disableLogger !== true) {
    middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger;
  }
  const RouterLayer = options?.routerConfig ? Layer.provide(layer, Layer.succeed(RouterConfig)(options.routerConfig)) : layer;
  return HttpEffect.toWebHandlerLayerWith(Layer.provideMerge(appLayer, RouterLayer), {
    toHandler: s => Effect.succeed(Context.get(s, HttpRouter).asHttpEffect()),
    middleware,
    memoMap: options?.memoMap
  });
};
//# sourceMappingURL=HttpRouter.js.map