/**
 * Server builders for declarative `HttpApi` contracts.
 *
 * This module is the server-side bridge between an `HttpApi` description and an
 * `HttpRouter`. It turns endpoint metadata into routes, decodes incoming
 * request parts with `Schema`, runs HTTP API middleware, invokes the registered
 * handlers, and encodes successes or declared errors back into
 * `HttpServerResponse` values.
 *
 * **Mental model**
 *
 * `HttpApi` values describe what can be served; this module supplies how it is
 * served. Implement each group with {@link group}, then register the completed
 * API with {@link layer}. The layer reads the group implementations from the
 * Effect context, adds their routes to the active `HttpRouter`, and can also
 * expose the generated OpenAPI document.
 *
 * **Common tasks**
 *
 * Use {@link group} with `handlers.handle` to implement every endpoint in one
 * API group. Use `handlers.handleRaw` when the handler needs direct access to
 * the `HttpServerRequest` or must decode the payload manually. Use
 * {@link endpoint} when composing one endpoint route by hand, and use
 * {@link securityDecode} or {@link securitySetCookie} inside security-aware
 * middleware.
 *
 * **Gotchas**
 *
 * Every group in the API must have a matching {@link group} layer before
 * {@link layer} is evaluated; otherwise registration fails with a defect naming
 * the missing group service. Payload decoding is selected from the request
 * media type, so unsupported content types return `415` before the handler
 * runs. Request decoding failures are wrapped in `HttpApiSchemaError`; handler
 * failures are encoded only when they match the endpoint or middleware error
 * schemas.
 *
 * **See also**
 *
 * `HttpApi` for the top-level contract, `HttpApiGroup` and `HttpApiEndpoint`
 * for declaration, `HttpApiMiddleware` for server and client middleware, and
 * `OpenApi` for generated specifications.
 *
 * @since 4.0.0
 */
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import * as Encoding from "../../Encoding.js";
import * as Fiber from "../../Fiber.js";
import { identity } from "../../Function.js";
import { stringOrRedacted } from "../../internal/redacted.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import { pipeArguments } from "../../Pipeable.js";
import * as Redacted from "../../Redacted.js";
import * as Result from "../../Result.js";
import * as Schema from "../../Schema.js";
import * as AST from "../../SchemaAST.js";
import * as Issue from "../../SchemaIssue.js";
import * as Transformation from "../../SchemaTransformation.js";
import * as Scope from "../../Scope.js";
import * as Stream from "../../Stream.js";
import * as UndefinedOr from "../../UndefinedOr.js";
import * as HttpEffect from "../http/HttpEffect.js";
import * as HttpMethod from "../http/HttpMethod.js";
import * as HttpRouter from "../http/HttpRouter.js";
import * as Request from "../http/HttpServerRequest.js";
import { HttpServerRequest } from "../http/HttpServerRequest.js";
import * as Response from "../http/HttpServerResponse.js";
import * as Multipart from "../http/Multipart.js";
import * as UrlParams from "../http/UrlParams.js";
import * as HttpApiEndpoint from "./HttpApiEndpoint.js";
import { HttpApiSchemaError } from "./HttpApiError.js";
import * as HttpApiMiddleware from "./HttpApiMiddleware.js";
import * as HttpApiSchema from "./HttpApiSchema.js";
import * as OpenApi from "./OpenApi.js";
/**
 * Registers an `HttpApi` with a `HttpRouter`.
 *
 * @category constructors
 * @since 4.0.0
 */
export const layer = (api, options) => HttpRouter.use(Effect.fnUntraced(function* (router) {
  const services = yield* Effect.context();
  const routes = [];
  const availableGroups = Array.from(services.mapUnsafe.keys()).filter(key => key.startsWith("effect/httpapi/HttpApiGroup/"));
  for (const group of Object.values(api.groups)) {
    const groupRoutes = services.mapUnsafe.get(group.key)?.routes;
    if (groupRoutes === undefined) {
      const available = availableGroups.length === 0 ? "none" : availableGroups.join(", ");
      return yield* Effect.die(`HttpApiGroup "${group.identifier}" not found (key: "${group.key}"). Did you forget to provide HttpApiBuilder.group(api, "${group.identifier}", ...)? Available groups: ${available}`);
    }
    routes.push(...groupRoutes);
  }
  yield* router.addAll(routes);
  if (options?.openapiPath) {
    const spec = OpenApi.fromApi(api);
    yield* router.add("GET", options.openapiPath, Effect.succeed(Response.jsonUnsafe(spec)));
  }
}));
/**
 * Create a `Layer` that implements all endpoints in an `HttpApi` group.
 *
 * **Details**
 *
 * The `build` function receives an unimplemented `Handlers` instance that can
 * be used to add handlers to the group. Implement endpoints with
 * `handlers.handle`.
 *
 * @category handlers
 * @since 4.0.0
 */
export const group = (api, groupName, build) => Layer.effectContext(Effect.gen(function* () {
  const services = (yield* Effect.context()).pipe(Context.omit(Scope.Scope));
  const group = api.groups[groupName];
  const result = build(makeHandlers(group));
  const handlers = Effect.isEffect(result) ? yield* result : result;
  const routes = [];
  for (const item of handlers.handlers.values()) {
    routes.push(handlerToRoute(group, item, services));
  }
  return Context.makeUnsafe(new Map([[group.key, {
    routes,
    handlers: handlers.handlers
  }]]));
}));
/**
 * Type identifier symbol used to brand `Handlers` values.
 *
 * @category type IDs
 * @since 4.0.0
 */
export const HandlersTypeId = /*#__PURE__*/Symbol.for("@effect/platform/HttpApiBuilder/Handlers");
/**
 * Builds the server-side HTTP effect for a single endpoint in an API group using
 * the endpoint metadata, middleware, codecs, and supplied handler.
 *
 * @category handlers
 * @since 4.0.0
 */
export const endpoint = (api, groupName, endpointName, handler) => Effect.contextWith(context => {
  const group = api.groups[groupName];
  const endpoint = group.endpoints[endpointName];
  return Effect.succeed(handlerToHttpEffect(group, endpoint, Context.omit(Scope.Scope)(context), handler, false));
});
/**
 * Decodes credentials for an HTTP API security scheme from the current request,
 * supporting bearer, API key, and basic authentication inputs.
 *
 * @category security
 * @since 4.0.0
 */
export const securityDecode = self => {
  switch (self._tag) {
    case "Http":
      {
        return Effect.map(HttpServerRequest, request => Redacted.make((request.headers.authorization ?? "").slice(self.schemeLength)));
      }
    case "ApiKey":
      {
        const key = self.in === "header" ? self.key.toLowerCase() : self.key;
        const schema = Schema.Struct({
          [key]: Schema.String
        });
        const decode = self.in === "query" ? Request.schemaSearchParams(schema) : self.in === "cookie" ? Request.schemaCookies(schema) : Request.schemaHeaders(schema);
        return Effect.match(decode, {
          onFailure: () => Redacted.make(""),
          onSuccess: match => Redacted.make(match[key])
        });
      }
    case "Basic":
      {
        const empty = {
          username: "",
          password: Redacted.make("")
        };
        return HttpServerRequest.pipe(Effect.flatMap(request => Effect.fromResult(Encoding.decodeBase64String((request.headers.authorization ?? "").slice(basicLen)))), Effect.match({
          onFailure: () => empty,
          onSuccess: header => {
            const parts = header.split(":");
            if (parts.length !== 2) {
              return empty;
            }
            return {
              username: parts[0],
              password: Redacted.make(parts[1])
            };
          }
        }));
      }
  }
};
/**
 * Registers a pre-response handler that sets an API-key cookie on the outgoing
 * response, defaulting the cookie to `secure` and `httpOnly` unless overridden.
 *
 * @category security
 * @since 4.0.0
 */
export const securitySetCookie = (self, value, options) => HttpEffect.appendPreResponseHandler((_req, response) => Effect.orDie(Response.setCookie(response, self.key, stringOrRedacted(value), {
  secure: true,
  httpOnly: true,
  ...options
})));
// -----------------------------------------------------------------------------
// Internal
// -----------------------------------------------------------------------------
const basicLen = `Basic `.length;
const HandlersProto = {
  [HandlersTypeId]: {
    _Endpoints: identity
  },
  pipe() {
    return pipeArguments(this, arguments);
  },
  handle(name, handler, options) {
    const endpoint = this.group.endpoints[name];
    this.handlers.set(name, {
      endpoint,
      handler,
      isRaw: false,
      uninterruptible: options?.uninterruptible ?? false
    });
    return this;
  },
  handleRaw(name, handler, options) {
    const endpoint = this.group.endpoints[name];
    this.handlers.set(name, {
      endpoint,
      handler,
      isRaw: true,
      uninterruptible: options?.uninterruptible ?? false
    });
    return this;
  }
};
const makeHandlers = group => {
  const self = Object.create(HandlersProto);
  self.group = group;
  self.handlers = new Map();
  return self;
};
function buildPayloadDecoders(payloadMap) {
  const result = new Map();
  payloadMap.forEach(({
    encoding,
    schemas
  }, contentType) => {
    const decode = Schema.decodeUnknownEffect(Schema.Union(schemas));
    if (encoding._tag === "Multipart") {
      result.set(contentType, {
        _tag: "Multipart",
        mode: encoding.mode,
        limits: encoding.limits,
        decode
      });
    } else {
      result.set(contentType, {
        _tag: encoding._tag,
        decode,
        nullOnEmpty: schemas.some(s => AST.isNull(AST.toEncoded(s.ast)))
      });
    }
  });
  return result;
}
function decodePayload(payloadBy, httpRequest, query) {
  const hasBody = HttpMethod.hasBody(httpRequest.method);
  const contentType = hasBody ? getRequestMediaType(httpRequest) : "application/x-www-form-urlencoded";
  const existing = payloadBy.get(contentType);
  if (!existing) {
    return Response.text(`Unsupported content-type: ${contentType}`, {
      status: 415
    });
  }
  const {
    _tag,
    decode
  } = existing;
  switch (_tag) {
    case "Multipart":
      {
        if (existing.mode === "buffered") {
          let eff = Effect.orDie(httpRequest.multipart);
          if (existing.limits) {
            eff = Effect.provideContext(eff, Multipart.limitsServices(existing.limits));
          }
          return Effect.flatMap(eff, decode);
        }
        return Effect.succeed(existing.limits ? Stream.provideContext(httpRequest.multipartStream, Multipart.limitsServices(existing.limits)) : httpRequest.multipartStream);
      }
    case "Json":
      const json = Effect.orDie(Effect.flatMap(httpRequest.text, text => {
        if (text === "") {
          return existing.nullOnEmpty ? Effect.succeed(null) : Effect.undefined;
        }
        return Effect.succeed(JSON.parse(text));
      }));
      return Effect.flatMap(json, decode);
    case "Text":
      return Effect.flatMap(Effect.orDie(httpRequest.text), decode);
    case "FormUrlEncoded":
      {
        const source = hasBody ? Effect.map(Effect.orDie(httpRequest.urlParamsBody), UrlParams.toRecord) : Effect.succeed(query);
        return Effect.flatMap(source, decode);
      }
    case "Uint8Array":
      return Effect.flatMap(Effect.map(Effect.orDie(httpRequest.arrayBuffer), buffer => new Uint8Array(buffer)), decode);
  }
}
function handlerToHttpEffect(group, endpoint, context, handler, isRaw) {
  const encodeSuccess = Schema.encodeUnknownEffect(makeSuccessSchema(endpoint));
  const encodeError = Schema.encodeUnknownEffect(makeErrorSchema(endpoint));
  const decodeParams = UndefinedOr.map(endpoint.params, Schema.decodeUnknownEffect);
  const decodeHeaders = UndefinedOr.map(endpoint.headers, Schema.decodeUnknownEffect);
  const decodeQuery = UndefinedOr.map(endpoint.query, Schema.decodeUnknownEffect);
  const shouldParsePayload = endpoint.payload.size > 0 && !isRaw;
  const payloadBy = shouldParsePayload ? buildPayloadDecoders(endpoint.payload) : undefined;
  return applyMiddleware(group, endpoint, context, Effect.gen(function* () {
    const fiber = Fiber.getCurrent();
    const context = fiber.context;
    const httpRequest = Context.getUnsafe(context, HttpServerRequest);
    const routeContext = Context.getUnsafe(context, HttpRouter.RouteContext);
    const query = Context.getUnsafe(context, Request.ParsedSearchParams);
    const request = {
      request: httpRequest,
      endpoint,
      group
    };
    if (decodeParams) {
      request.params = yield* HttpApiSchemaError.wrap("Params", decodeParams(routeContext.params));
    }
    if (decodeHeaders) {
      request.headers = yield* HttpApiSchemaError.wrap("Headers", decodeHeaders(httpRequest.headers));
    }
    if (decodeQuery) {
      request.query = yield* HttpApiSchemaError.wrap("Query", decodeQuery(query));
    }
    if (payloadBy) {
      const result = decodePayload(payloadBy, httpRequest, query);
      if (Response.isHttpServerResponse(result)) {
        return result;
      }
      if (result !== undefined) {
        request.payload = yield* HttpApiSchemaError.wrap("Payload", result);
      }
    }
    const response = yield* handler(request);
    return Response.isHttpServerResponse(response) ? response : yield* HttpApiSchemaError.wrap("Body", encodeSuccess(response));
  })).pipe(Effect.withErrorReporting, Effect.catch(error => {
    if (HttpApiSchemaError.is(error)) return Effect.die(error);
    return Effect.orDie(encodeError(error));
  }), Effect.provideContext(context));
}
/** @internal */
export function handlerToRoute(group, handler, context) {
  const endpoint = handler.endpoint;
  return HttpRouter.route(endpoint.method, endpoint.path, handlerToHttpEffect(group, endpoint, context, handler.handler, handler.isRaw), {
    uninterruptible: handler.uninterruptible
  });
}
const getRequestContentType = request => request.headers["content-type"] ? request.headers["content-type"].toLowerCase().trim() : "application/json";
const getRequestMediaType = request => {
  const contentType = getRequestContentType(request);
  const index = contentType.indexOf(";");
  return index === -1 ? contentType : contentType.slice(0, index).trim();
};
const applyMiddleware = (group, endpoint, context, handler) => {
  const options = {
    group,
    endpoint
  };
  for (const key_ of endpoint.middlewares) {
    const key = key_;
    const service = Context.getUnsafe(context, key);
    const apply = HttpApiMiddleware.isSecurity(key) ? makeSecurityMiddleware(key, service) : service;
    handler = apply(handler, options);
  }
  return handler;
};
const securityMiddlewareCache = /*#__PURE__*/new WeakMap();
const makeSecurityMiddleware = (key, service) => {
  const cached = securityMiddlewareCache.get(service);
  if (cached !== undefined) {
    return cached;
  }
  const entries = Object.entries(key.security).map(([securityKey, security]) => ({
    decode: securityDecode(security),
    middleware: service[securityKey]
  }));
  if (entries.length === 0) {
    return identity;
  }
  const middleware = Effect.fnUntraced(function* (handler, options) {
    let lastResult;
    for (let i = 0; i < entries.length; i++) {
      const {
        decode,
        middleware
      } = entries[i];
      const result = yield* Effect.result(Effect.flatMap(decode, credential => middleware(handler, {
        credential,
        endpoint: options.endpoint,
        group: options.group
      })));
      if (Result.isFailure(result)) {
        lastResult = result;
        continue;
      }
      return result.success;
    }
    return yield* Effect.fromResult(lastResult);
  });
  securityMiddlewareCache.set(service, middleware);
  return middleware;
};
const $HttpServerResponse = /*#__PURE__*/Schema.declare(Response.isHttpServerResponse);
const toResponseSuccessSchema = /*#__PURE__*/toResponseSchema(HttpApiSchema.getStatusSuccess);
const toResponseErrorSchema = /*#__PURE__*/toResponseSchema(HttpApiSchema.getStatusError);
function makeSuccessSchema(endpoint) {
  const schemas = HttpApiEndpoint.getSuccessSchemas(endpoint).map(toResponseSuccessSchema);
  return schemas.length === 1 ? schemas[0] : Schema.Union(schemas);
}
function makeErrorSchema(endpoint) {
  const schemas = HttpApiEndpoint.getErrorSchemas(endpoint).map(toResponseErrorSchema);
  if (schemas.length === 0) return Schema.Never;
  return schemas.length === 1 ? schemas[0] : Schema.Union(schemas);
}
function toResponseSchema(getStatus) {
  const cache = new WeakMap();
  return schema => {
    const cached = cache.get(schema.ast);
    if (cached !== undefined) {
      return cached;
    }
    const responseSchema = $HttpServerResponse.pipe(Schema.decodeTo(schema, getResponseTransformation(getStatus, schema)));
    cache.set(responseSchema.ast, responseSchema);
    return responseSchema;
  };
}
function getResponseTransformation(getStatus, schema) {
  const ast = schema.ast;
  const encode = getResponseEncode(getStatus(ast), HttpApiSchema.getResponseEncoding(ast), HttpApiSchema.isNoContent(ast));
  return Transformation.transformOrFail({
    decode: res => Effect.fail(new Issue.Forbidden(Option.some(res), {
      message: "Encode only schema"
    })),
    encode
  });
}
function getResponseEncode(status, encoding, isNoContent) {
  switch (encoding._tag) {
    case "Json":
      {
        return e => {
          if (e === undefined || isNoContent) {
            return Effect.succeed(Response.empty({
              status
            }));
          }
          try {
            const s = JSON.stringify(e);
            return Effect.succeed(Response.text(s, {
              status,
              contentType: encoding.contentType
            }));
          } catch (error) {
            return Effect.fail(new Issue.InvalidValue(Option.some(e), {
              message: globalThis.String(error)
            }));
          }
        };
      }
    case "Text":
      return e => Effect.succeed(Response.text(e, {
        status,
        contentType: encoding.contentType
      }));
    case "Uint8Array":
      return e => Effect.succeed(Response.uint8Array(e, {
        status,
        contentType: encoding.contentType
      }));
    case "FormUrlEncoded":
      return e => Effect.succeed(Response.urlParams(e, {
        status
      }).pipe(Response.setHeader("content-type", encoding.contentType)));
  }
}
//# sourceMappingURL=HttpApiBuilder.js.map