/**
 * Runtime boundary for serving Effect HTTP responses on a concrete HTTP server.
 *
 * `HttpServer` is the service implemented by platform adapters and consumed by
 * routers, API layers, tests, and applications that start serving from a
 * `Layer`. It exposes the listening address and a `serve` operation that runs
 * an `HttpServerResponse` effect for each incoming request.
 *
 * **Mental model**
 *
 * The server owns the listener and supplies `HttpServerRequest` for every
 * request. The application effect only describes how to produce a response, so
 * request-scoped data comes from the server while the rest of the environment is
 * still provided by the surrounding layer graph. `serve` ties listener lifetime
 * to a layer scope, and `serveEffect` performs the same work inside an explicit
 * `Scope`.
 *
 * **Common tasks**
 *
 * - Build a server service from a platform adapter with `make`.
 * - Serve an HTTP app with `serve` or `serveEffect`.
 * - Format or log the listening address with `formatAddress`,
 *   `addressFormattedWith`, `logAddress`, or `withLogAddress`.
 * - Create a test client that targets the running server with `makeTestClient`
 *   or `layerTestClient`.
 *
 * **Gotchas**
 *
 * Middleware is applied at the server boundary, after the server has supplied
 * the request service. Test clients only support TCP addresses, and rewrite
 * `0.0.0.0` to `127.0.0.1` for local requests.
 *
 * @since 4.0.0
 */
import * as Context from "../../Context.ts"
import * as Effect from "../../Effect.ts"
import * as FileSystem from "../../FileSystem.ts"
import { dual } from "../../Function.ts"
import * as Layer from "../../Layer.ts"
import * as Path from "../../Path.ts"
import type * as Scope from "../../Scope.ts"
import * as Etag from "./Etag.ts"
import * as HttpClient from "./HttpClient.ts"
import * as ClientRequest from "./HttpClientRequest.ts"
import type * as Middleware from "./HttpMiddleware.ts"
import * as HttpPlatform from "./HttpPlatform.ts"
import type { HttpServerRequest } from "./HttpServerRequest.ts"
import type { HttpServerResponse } from "./HttpServerResponse.ts"

/**
 * Service tag for an HTTP server runtime.
 *
 * **Details**
 *
 * The service can serve an HTTP response effect and exposes the address where the
 * server is listening.
 *
 * @category models
 * @since 4.0.0
 */
export class HttpServer extends Context.Service<HttpServer, {
  readonly serve: {
    <E, R>(effect: Effect.Effect<HttpServerResponse, E, R>): Effect.Effect<
      void,
      never,
      Exclude<R, HttpServerRequest> | Scope.Scope
    >
    <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
      effect: Effect.Effect<HttpServerResponse, E, R>,
      middleware: Middleware.HttpMiddleware.Applied<App, E, R>
    ): Effect.Effect<
      void,
      never,
      Exclude<R, HttpServerRequest> | Scope.Scope
    >
  }

  readonly address: Address
}>()("effect/http/HttpServer") {}

/**
 * Address where an HTTP server is listening.
 *
 * **Details**
 *
 * The address is either a TCP host and port or a Unix domain socket path.
 *
 * @category address
 * @since 4.0.0
 */
export type Address = UnixAddress | TcpAddress

/**
 * TCP address for an HTTP server, identified by hostname and port.
 *
 * @category address
 * @since 4.0.0
 */
export interface TcpAddress {
  readonly _tag: "TcpAddress"
  readonly hostname: string
  readonly port: number
}

/**
 * Unix domain socket address for an HTTP server.
 *
 * @category address
 * @since 4.0.0
 */
export interface UnixAddress {
  readonly _tag: "UnixAddress"
  readonly path: string
}

/**
 * Constructs an `HttpServer` service from a serving implementation and listening
 * address.
 *
 * @category constructors
 * @since 4.0.0
 */
export const make = (
  options: {
    readonly serve: (
      httpEffect: Effect.Effect<HttpServerResponse, unknown, HttpServerRequest | Scope.Scope>,
      middleware?: Middleware.HttpMiddleware
    ) => Effect.Effect<void, never, Scope.Scope>
    readonly address: Address
  }
): HttpServer["Service"] => options

/**
 * Creates a layer that starts serving an HTTP response effect with the current
 * `HttpServer`.
 *
 * **Details**
 *
 * The request service is supplied by the server for each request; the returned
 * layer still requires the server, a scope, and any non-request dependencies of
 * the response effect or middleware.
 *
 * @category accessors
 * @since 4.0.0
 */
export const serve: {
  /**
   * Creates a layer that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the returned
   * layer still requires the server, a scope, and any non-request dependencies of
   * the response effect or middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  (): <E, R>(
    effect: Effect.Effect<HttpServerResponse, E, R>
  ) => Layer.Layer<never, never, HttpServer | Exclude<R, HttpServerRequest | Scope.Scope>>
  /**
   * Creates a layer that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the returned
   * layer still requires the server, a scope, and any non-request dependencies of
   * the response effect or middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(middleware: Middleware.HttpMiddleware.Applied<App, E, R>): (
    effect: Effect.Effect<HttpServerResponse, E, R>
  ) => Layer.Layer<
    never,
    never,
    HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
  >
  /**
   * Creates a layer that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the returned
   * layer still requires the server, a scope, and any non-request dependencies of
   * the response effect or middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R>(effect: Effect.Effect<HttpServerResponse, E, R>): Layer.Layer<never, never, HttpServer | Exclude<R, HttpServerRequest | Scope.Scope>>
  /**
   * Creates a layer that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the returned
   * layer still requires the server, a scope, and any non-request dependencies of
   * the response effect or middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
    effect: Effect.Effect<HttpServerResponse, E, R>,
    middleware: Middleware.HttpMiddleware.Applied<App, E, R>
  ): Layer.Layer<
    never,
    never,
    HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
  >
} = dual((args) => Effect.isEffect(args[0]), <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
  effect: Effect.Effect<HttpServerResponse, E, R>,
  middleware?: Middleware.HttpMiddleware.Applied<App, E, R>
): Layer.Layer<
  never,
  never,
  HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
> =>
  Layer.effectDiscard(
    HttpServer.use((server) => server.serve(effect, middleware!))
  ) as any)

/**
 * Effect that starts serving an HTTP response effect with the current
 * `HttpServer`.
 *
 * **Details**
 *
 * The request service is supplied by the server for each request; the effect
 * requires a scope and any non-request dependencies of the response effect or
 * middleware.
 *
 * @category accessors
 * @since 4.0.0
 */
export const serveEffect: {
  /**
   * Effect that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the effect
   * requires a scope and any non-request dependencies of the response effect or
   * middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  (): <E, R>(
    effect: Effect.Effect<HttpServerResponse, E, R>
  ) => Effect.Effect<void, never, Scope.Scope | HttpServer | Exclude<R, HttpServerRequest>>
  /**
   * Effect that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the effect
   * requires a scope and any non-request dependencies of the response effect or
   * middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(middleware: Middleware.HttpMiddleware.Applied<App, E, R>): (
    effect: Effect.Effect<HttpServerResponse, E, R>
  ) => Effect.Effect<
    void,
    never,
    Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
  >
  /**
   * Effect that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the effect
   * requires a scope and any non-request dependencies of the response effect or
   * middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R>(effect: Effect.Effect<HttpServerResponse, E, R>): Effect.Effect<void, never, Scope.Scope | HttpServer | Exclude<R, HttpServerRequest>>
  /**
   * Effect that starts serving an HTTP response effect with the current
   * `HttpServer`.
   *
   * **Details**
   *
   * The request service is supplied by the server for each request; the effect
   * requires a scope and any non-request dependencies of the response effect or
   * middleware.
   *
   * @category accessors
   * @since 4.0.0
   */
  <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
    effect: Effect.Effect<HttpServerResponse, E, R>,
    middleware: Middleware.HttpMiddleware.Applied<App, E, R>
  ): Effect.Effect<
    void,
    never,
    Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
  >
} = dual((args) => Effect.isEffect(args[0]), <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
  effect: Effect.Effect<HttpServerResponse, E, R>,
  middleware?: Middleware.HttpMiddleware.Applied<App, E, R>
): Effect.Effect<
  void,
  never,
  Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
> => HttpServer.use((server) => server.serve(effect, middleware!)) as any)

/**
 * Formats a server address as a display string.
 *
 * **Details**
 *
 * TCP addresses are formatted as `http://host:port`; Unix socket addresses are
 * formatted as `unix://path`.
 *
 * @category address
 * @since 4.0.0
 */
export const formatAddress = (address: Address): string => {
  switch (address._tag) {
    case "UnixAddress":
      return `unix://${address.path}`
    case "TcpAddress":
      return `http://${address.hostname}:${address.port}`
  }
}

/**
 * Reads the current server address, formats it with `formatAddress`, and passes
 * the formatted address to the supplied effectful function.
 *
 * @category address
 * @since 4.0.0
 */
export const addressFormattedWith = <A, E, R>(
  f: (address: string) => Effect.Effect<A, E, R>
): Effect.Effect<A, E, HttpServer | R> =>
  Effect.flatMap(
    HttpServer,
    (server) => f(formatAddress(server.address))
  )

/**
 * Logs the formatted address of the current HTTP server.
 *
 * @category address
 * @since 4.0.0
 */
export const logAddress: Effect.Effect<void, never, HttpServer> = addressFormattedWith((_) =>
  Effect.log(`Listening on ${_}`)
)

/**
 * Adds address logging to a layer that provides an `HttpServer`.
 *
 * @category address
 * @since 4.0.0
 */
export const withLogAddress = <A, E, R>(
  layer: Layer.Layer<A, E, R>
): Layer.Layer<A, E, R | Exclude<HttpServer, A>> =>
  Layer.effectDiscard(logAddress).pipe(
    Layer.provideMerge(layer)
  )

/**
 * Builds an `HttpClient` that sends requests to the current test HTTP server.
 *
 * **Details**
 *
 * For TCP servers, requests are prefixed with the server URL and `0.0.0.0` is
 * rewritten to `127.0.0.1`.
 *
 * **Gotchas**
 *
 * Unix socket addresses are not supported.
 *
 * @category testing
 * @since 4.0.0
 */
export const makeTestClient: Effect.Effect<
  HttpClient.HttpClient,
  never,
  HttpServer | HttpClient.HttpClient
> = Effect.gen(function*() {
  const server = yield* HttpServer
  const client = yield* HttpClient.HttpClient
  const address = server.address
  if (address._tag === "UnixAddress") {
    return yield* Effect.die(new Error("HttpServer.layerTestClient: UnixAddress not supported"))
  }
  const host = address.hostname === "0.0.0.0" ? "127.0.0.1" : address.hostname
  const url = `http://${host}:${address.port}`
  return HttpClient.mapRequest(client, ClientRequest.prependUrl(url))
})

/**
 * Layer that provides the test `HttpClient` created by `makeTestClient`.
 *
 * @category testing
 * @since 4.0.0
 */
export const layerTestClient: Layer.Layer<
  HttpClient.HttpClient,
  never,
  HttpServer | HttpClient.HttpClient
> = Layer.effect(HttpClient.HttpClient)(makeTestClient)

/**
 * Layer that provides the platform services commonly needed by HTTP
 * server tests.
 *
 * **Details**
 *
 * It includes `HttpPlatform`, `Path`, a weak ETag generator, and a no-op
 * `FileSystem`.
 *
 * @category testing
 * @since 4.0.0
 */
export const layerServices: Layer.Layer<
  | Path.Path
  | HttpPlatform.HttpPlatform
  | FileSystem.FileSystem
  | Etag.Generator
> = Layer.mergeAll(
  HttpPlatform.layer,
  Path.layer,
  Etag.layerWeak
).pipe(
  Layer.provideMerge(FileSystem.layerNoop({}))
)
