/**
 * @since 4.0.0
 */
import type * as Arr from "../../Array.ts"
import * as Channel from "../../Channel.ts"
import * as Context from "../../Context.ts"
import * as Effect from "../../Effect.ts"
import type * as FileSystem from "../../FileSystem.ts"
import * as Inspectable from "../../Inspectable.ts"
import * as Option from "../../Option.ts"
import type * as Path from "../../Path.ts"
import type { ReadonlyRecord } from "../../Record.ts"
import * as Result from "../../Result.ts"
import * as Schema from "../../Schema.ts"
import type { ParseOptions } from "../../SchemaAST.ts"
import type * as Scope from "../../Scope.ts"
import * as Stream from "../../Stream.ts"
import * as Socket from "../socket/Socket.ts"
import * as Cookies from "./Cookies.ts"
import * as Headers from "./Headers.ts"
import * as HttpBody from "./HttpBody.ts"
import * as HttpClientRequest from "./HttpClientRequest.ts"
import * as HttpIncomingMessage from "./HttpIncomingMessage.ts"
import { hasBody, type HttpMethod } from "./HttpMethod.ts"
import { HttpServerError, type RequestError, RequestParseError } from "./HttpServerError.ts"
import * as Multipart from "./Multipart.ts"
import * as UrlParams from "./UrlParams.ts"

export {
  /**
   * @since 4.0.0
   * @category fiber refs
   */
  MaxBodySize
} from "./HttpIncomingMessage.ts"

/**
 * @since 4.0.0
 * @category Type IDs
 */
export const TypeId = "~effect/http/HttpServerRequest"

/**
 * @since 4.0.0
 * @category models
 */
export interface HttpServerRequest extends HttpIncomingMessage.HttpIncomingMessage<HttpServerError> {
  readonly [TypeId]: typeof TypeId
  readonly source: object
  readonly url: string
  readonly originalUrl: string
  readonly method: HttpMethod
  readonly cookies: ReadonlyRecord<string, string>

  readonly multipart: Effect.Effect<
    Multipart.Persisted,
    Multipart.MultipartError,
    Scope.Scope | FileSystem.FileSystem | Path.Path
  >
  readonly multipartStream: Stream.Stream<Multipart.Part, Multipart.MultipartError>

  readonly upgrade: Effect.Effect<Socket.Socket, HttpServerError>

  readonly modify: (
    options: {
      readonly url?: string
      readonly headers?: Headers.Headers
      readonly remoteAddress?: Option.Option<string>
    }
  ) => HttpServerRequest
}

/**
 * @since 4.0.0
 * @category context
 */
export const HttpServerRequest: Context.Service<HttpServerRequest, HttpServerRequest> = Context.Service(
  "effect/http/HttpServerRequest"
)

/**
 * @since 4.0.0
 * @category search params
 */
export class ParsedSearchParams extends Context.Service<
  ParsedSearchParams,
  ReadonlyRecord<string, string | Array<string>>
>()("effect/http/ParsedSearchParams") {}

/**
 * @since 4.0.0
 * @category search params
 */
export const searchParamsFromURL = (url: URL): ReadonlyRecord<string, string | Array<string>> => {
  const out: Record<string, string | Array<string>> = {}
  for (const [key, value] of url.searchParams.entries()) {
    const entry = out[key]
    if (entry !== undefined) {
      if (Array.isArray(entry)) {
        entry.push(value)
      } else {
        out[key] = [entry, value]
      }
    } else {
      out[key] = value
    }
  }
  return out
}

/**
 * @since 4.0.0
 * @category accessors
 */
export const upgradeChannel = <IE = never>(): Channel.Channel<
  Arr.NonEmptyReadonlyArray<Uint8Array>,
  HttpServerError | IE | Socket.SocketError,
  void,
  Arr.NonEmptyReadonlyArray<string | Uint8Array | Socket.CloseEvent>,
  IE,
  unknown,
  HttpServerRequest
> =>
  HttpServerRequest.asEffect().pipe(
    Effect.flatMap((_) => _.upgrade),
    Effect.map(Socket.toChannelWith<IE>()),
    Channel.unwrap
  )

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaCookies = <A, I extends Readonly<Record<string, string | undefined>>, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<A, Schema.SchemaError, RD | HttpServerRequest> => {
  const parse = Schema.decodeUnknownEffect(schema)
  return Effect.flatMap(HttpServerRequest.asEffect(), (req) => parse(req.cookies, options))
}

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaHeaders = <A, I extends Readonly<Record<string, string | undefined>>, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<A, Schema.SchemaError, HttpServerRequest | RD> => {
  const parse = Schema.decodeUnknownEffect(schema)
  return Effect.flatMap(HttpServerRequest.asEffect(), (req) => parse(req.headers, options))
}

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaSearchParams = <
  A,
  I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
  RD,
  RE
>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<A, Schema.SchemaError, ParsedSearchParams | RD> => {
  const parse = Schema.decodeUnknownEffect(schema)
  return Effect.flatMap(ParsedSearchParams.asEffect(), (params) => parse(params, options))
}
/**
 * @since 4.0.0
 * @category schema
 */
export const schemaBodyJson = <A, I, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<A, HttpServerError | Schema.SchemaError, HttpServerRequest | RD> => {
  const parse = HttpIncomingMessage.schemaBodyJson(schema, options)
  return Effect.flatMap(HttpServerRequest.asEffect(), parse)
}

const isMultipart = (request: HttpServerRequest) =>
  request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") === true ||
  getFormDataBody(request) !== undefined

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaBodyForm = <A, I extends Partial<Multipart.Persisted>, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
) => {
  const parseMultipart = Multipart.schemaPersisted(schema)
  const parseUrlParams = HttpIncomingMessage.schemaBodyUrlParams(schema as Schema.Codec<A, any, RD, RE>, options)
  return Effect.flatMap(HttpServerRequest.asEffect(), (request): Effect.Effect<
    A,
    Multipart.MultipartError | Schema.SchemaError | HttpServerError,
    RD | HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path
  > => {
    if (isMultipart(request)) {
      return Effect.flatMap(request.multipart, (_) => parseMultipart(_, options))
    }
    return parseUrlParams(request)
  })
}

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaBodyUrlParams = <
  A,
  I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
  RD,
  RE
>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<A, HttpServerError | Schema.SchemaError, HttpServerRequest | RD> => {
  const parse = HttpIncomingMessage.schemaBodyUrlParams(schema, options)
  return Effect.flatMap(HttpServerRequest.asEffect(), parse)
}

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaBodyMultipart = <A, I extends Partial<Multipart.Persisted>, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
): Effect.Effect<
  A,
  Multipart.MultipartError | Schema.SchemaError,
  HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path | RD
> => {
  const parse = Multipart.schemaPersisted(schema)
  return HttpServerRequest.asEffect().pipe(
    Effect.flatMap((_) => _.multipart),
    Effect.flatMap((_) => parse(_, options))
  )
}

/**
 * @since 4.0.0
 * @category schema
 */
export const schemaBodyFormJson = <A, I, RD, RE>(
  schema: Schema.Codec<A, I, RD, RE>,
  options?: ParseOptions | undefined
) => {
  const parseMultipart = Multipart.schemaJson(schema, options)
  return (field: string) => {
    const parseUrlParams = UrlParams.schemaJsonField(field).pipe(
      Schema.decodeTo(schema),
      Schema.decodeEffect
    )
    return Effect.flatMap(
      HttpServerRequest.asEffect(),
      (request): Effect.Effect<
        A,
        Schema.SchemaError | HttpServerError,
        RD | FileSystem.FileSystem | Path.Path | Scope.Scope | HttpServerRequest
      > => {
        if (isMultipart(request)) {
          return Effect.flatMap(
            Effect.mapError(request.multipart, (cause) =>
              new HttpServerError({
                reason: new RequestParseError({
                  request,
                  cause
                })
              })),
            parseMultipart(field)
          )
        }
        return Effect.flatMap(request.urlParamsBody, (_) => parseUrlParams(_, options))
      }
    )
  }
}

/**
 * @since 4.0.0
 * @category conversions
 */
export const fromClientRequest = (request: HttpClientRequest.HttpClientRequest): HttpServerRequest => {
  const url = Option.match(HttpClientRequest.toUrl(request), {
    onNone: () => request.url,
    onSome: (url) => url.toString()
  })
  return new ClientRequestImpl(request, url)
}

/**
 * @since 4.0.0
 * @category conversions
 */
export const fromWeb = (request: globalThis.Request): HttpServerRequest =>
  new ServerRequestImpl(request, removeHost(request.url))

/**
 * @since 4.0.0
 * @category conversions
 */
export const toClientRequest = (request: HttpServerRequest): HttpClientRequest.HttpClientRequest =>
  HttpClientRequest.setUrl(
    HttpClientRequest.makeWith(
      request.method,
      "",
      UrlParams.empty,
      Option.none(),
      request.headers,
      toClientBody(request)
    ),
    Option.getOrElse(toURL(request), () => request.url)
  )

const toClientBody = (request: HttpServerRequest): HttpBody.HttpBody =>
  hasBody(request.method)
    ? HttpBody.stream(
      request.stream,
      request.headers["content-type"],
      parseContentLength(request.headers["content-length"])
    )
    : HttpBody.empty

const parseContentLength = (contentLength: string | undefined): number | undefined => {
  if (contentLength === undefined) {
    return undefined
  }
  const parsed = Number.parseInt(contentLength, 10)
  return Number.isNaN(parsed) ? undefined : parsed
}

const removeHost = (url: string) => {
  if (url[0] === "/") {
    return url
  }
  const index = url.indexOf("/", url.indexOf("//") + 2)
  return index === -1 ? "/" : url.slice(index)
}

class ServerRequestImpl extends Inspectable.Class implements HttpServerRequest {
  readonly [TypeId]: typeof TypeId
  readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId
  readonly source: Request
  readonly url: string
  public headersOverride?: Headers.Headers | undefined
  private remoteAddressOverride?: Option.Option<string> | undefined

  constructor(
    source: Request,
    url: string,
    headersOverride?: Headers.Headers,
    remoteAddressOverride?: Option.Option<string>
  ) {
    super()
    this[TypeId] = TypeId
    this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
    this.source = source
    this.url = url
    this.headersOverride = headersOverride
    this.remoteAddressOverride = remoteAddressOverride
  }
  toJSON(): unknown {
    return HttpIncomingMessage.inspect(this, {
      _id: "HttpServerRequest",
      method: this.method,
      url: this.originalUrl
    })
  }
  modify(
    options: {
      readonly url?: string | undefined
      readonly headers?: Headers.Headers | undefined
      readonly remoteAddress?: Option.Option<string> | undefined
    }
  ) {
    return new ServerRequestImpl(
      this.source,
      options.url ?? this.url,
      options.headers ?? this.headersOverride,
      "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
    )
  }
  get method(): HttpMethod {
    return this.source.method.toUpperCase() as HttpMethod
  }
  get originalUrl() {
    return this.source.url
  }
  get remoteAddress(): Option.Option<string> {
    return this.remoteAddressOverride ?? Option.none()
  }
  get headers(): Headers.Headers {
    this.headersOverride ??= Headers.fromInput(this.source.headers as any)
    return this.headersOverride
  }

  private cachedCookies: ReadonlyRecord<string, string> | undefined
  get cookies() {
    if (this.cachedCookies) {
      return this.cachedCookies
    }
    return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
  }

  get stream(): Stream.Stream<Uint8Array, HttpServerError> {
    return this.source.body
      ? Stream.fromReadableStream({
        evaluate: () => this.source.body as any,
        onError: (cause) =>
          new HttpServerError({
            reason: new RequestParseError({
              request: this,
              cause
            })
          })
      })
      : Stream.fail(
        new HttpServerError({
          reason: new RequestParseError({
            request: this,
            description: "can not create stream from empty body"
          })
        })
      )
  }

  private textEffect: Effect.Effect<string, HttpServerError> | undefined
  get text(): Effect.Effect<string, HttpServerError> {
    if (this.textEffect) {
      return this.textEffect
    }
    this.textEffect = Effect.runSync(Effect.cached(
      Effect.tryPromise({
        try: () => this.source.text(),
        catch: (cause) =>
          new HttpServerError({
            reason: new RequestParseError({
              request: this,
              cause
            })
          })
      })
    ))
    return this.textEffect
  }

  get json(): Effect.Effect<Schema.Json, HttpServerError> {
    return Effect.flatMap(this.text, (text) =>
      Effect.try({
        try: () => JSON.parse(text) as Schema.Json,
        catch: (cause) =>
          new HttpServerError({
            reason: new RequestParseError({
              request: this,
              cause
            })
          })
      }))
  }

  get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, HttpServerError> {
    return Effect.flatMap(this.text, (_) =>
      Effect.try({
        try: () => UrlParams.fromInput(new URLSearchParams(_)),
        catch: (cause) =>
          new HttpServerError({
            reason: new RequestParseError({
              request: this,
              cause
            })
          })
      }))
  }

  private multipartEffect:
    | Effect.Effect<
      Multipart.Persisted,
      Multipart.MultipartError,
      Scope.Scope | FileSystem.FileSystem | Path.Path
    >
    | undefined
  get multipart(): Effect.Effect<
    Multipart.Persisted,
    Multipart.MultipartError,
    Scope.Scope | FileSystem.FileSystem | Path.Path
  > {
    if (this.multipartEffect) {
      return this.multipartEffect
    }
    this.multipartEffect = Effect.runSync(Effect.cached(
      Multipart.toPersisted(this.multipartStream)
    ))
    return this.multipartEffect
  }

  get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
    return Stream.pipeThroughChannel(
      Stream.mapError(this.stream, (cause) => Multipart.MultipartError.fromReason("InternalError", cause)),
      Multipart.makeChannel(this.headers)
    )
  }

  private arrayBufferEffect: Effect.Effect<ArrayBuffer, HttpServerError> | undefined
  get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError> {
    if (this.arrayBufferEffect) {
      return this.arrayBufferEffect
    }
    this.arrayBufferEffect = Effect.runSync(Effect.cached(
      Effect.tryPromise({
        try: () => this.source.arrayBuffer(),
        catch: (cause) =>
          new HttpServerError({
            reason: new RequestParseError({
              request: this,
              cause
            })
          })
      })
    ))
    return this.arrayBufferEffect
  }

  get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
    return Effect.fail(
      new HttpServerError({
        reason: new RequestParseError({
          request: this,
          description: "Not an upgradeable ServerRequest"
        })
      })
    )
  }
}

class ClientRequestImpl extends Inspectable.Class implements HttpServerRequest {
  readonly [TypeId]: typeof TypeId
  readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId
  readonly source: HttpClientRequest.HttpClientRequest
  public originalUrl: string
  public headersOverride?: Headers.Headers | undefined
  private remoteAddressOverride?: Option.Option<string> | undefined
  private urlOverride?: string | undefined

  constructor(
    source: HttpClientRequest.HttpClientRequest,
    originalUrl: string,
    urlOverride?: string,
    headersOverride?: Headers.Headers,
    remoteAddressOverride?: Option.Option<string>
  ) {
    super()
    this[TypeId] = TypeId
    this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
    this.source = source
    this.originalUrl = originalUrl
    this.urlOverride = urlOverride
    this.headersOverride = headersOverride
    this.remoteAddressOverride = remoteAddressOverride
  }

  toJSON(): unknown {
    return HttpIncomingMessage.inspect(this, {
      _id: "HttpServerRequest",
      method: this.method,
      url: this.originalUrl
    })
  }

  modify(
    options: {
      readonly url?: string | undefined
      readonly headers?: Headers.Headers | undefined
      readonly remoteAddress?: Option.Option<string> | undefined
    }
  ) {
    return new ClientRequestImpl(
      this.source,
      this.originalUrl,
      options.url ?? this.url,
      options.headers ?? this.headersOverride,
      "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
    )
  }

  get method(): HttpMethod {
    return this.source.method
  }

  get url(): string {
    return this.urlOverride ?? removeHost(this.originalUrl)
  }

  get remoteAddress(): Option.Option<string> {
    return this.remoteAddressOverride ?? Option.none()
  }

  get headers(): Headers.Headers {
    return this.headersOverride ??= this.source.headers
  }

  private cachedCookies: ReadonlyRecord<string, string> | undefined
  get cookies() {
    if (this.cachedCookies) {
      return this.cachedCookies
    }
    return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
  }

  get stream(): Stream.Stream<Uint8Array, HttpServerError> {
    const body = this.source.body
    switch (body._tag) {
      case "Empty": {
        return Stream.empty
      }
      case "Uint8Array": {
        return Stream.succeed(body.body)
      }
      case "Stream": {
        return Stream.mapError(body.stream, (cause) => requestParseError(this, undefined, cause))
      }
      case "FormData": {
        return streamFromReadable(this, new Response(body.formData).body)
      }
      case "Raw": {
        return rawBodyStream(this, body.body)
      }
    }
  }

  private bytesEffect: Effect.Effect<Uint8Array, HttpServerError> | undefined
  private get bytes(): Effect.Effect<Uint8Array, HttpServerError> {
    if (this.bytesEffect) {
      return this.bytesEffect
    }
    const body = this.source.body
    let effect: Effect.Effect<Uint8Array, HttpServerError>
    switch (body._tag) {
      case "Empty": {
        effect = Effect.succeed(new Uint8Array(0))
        break
      }
      case "Uint8Array": {
        effect = Effect.succeed(body.body)
        break
      }
      case "FormData": {
        effect = bytesFromBodyInit(this, body.formData)
        break
      }
      case "Stream": {
        effect = Stream.mkUint8Array(this.stream)
        break
      }
      case "Raw": {
        effect = rawBodyBytes(this, body.body)
        break
      }
    }
    this.bytesEffect = Effect.runSync(Effect.cached(effect))
    return this.bytesEffect
  }

  get text(): Effect.Effect<string, HttpServerError> {
    return Effect.map(this.bytes, (bytes) => textDecoder.decode(bytes))
  }

  get json(): Effect.Effect<Schema.Json, HttpServerError> {
    return Effect.flatMap(this.text, (text) =>
      Effect.try({
        try: () => text === "" ? null : JSON.parse(text),
        catch: (cause) => requestParseError(this, undefined, cause)
      }))
  }

  get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, HttpServerError> {
    return Effect.flatMap(this.text, (_) =>
      Effect.try({
        try: () => UrlParams.fromInput(new URLSearchParams(_)),
        catch: (cause) => requestParseError(this, undefined, cause)
      }))
  }

  private multipartEffect:
    | Effect.Effect<
      Multipart.Persisted,
      Multipart.MultipartError,
      Scope.Scope | FileSystem.FileSystem | Path.Path
    >
    | undefined
  get multipart(): Effect.Effect<
    Multipart.Persisted,
    Multipart.MultipartError,
    Scope.Scope | FileSystem.FileSystem | Path.Path
  > {
    if (this.multipartEffect) {
      return this.multipartEffect
    }
    this.multipartEffect = Effect.runSync(Effect.cached(
      Multipart.toPersisted(this.multipartStream)
    ))
    return this.multipartEffect
  }

  get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
    const formData = this.source.body._tag === "FormData" && this.source.body.formData
    if (formData) {
      return Stream.fromIterable(formDataToParts(formData))
    }
    return Stream.pipeThroughChannel(
      Stream.mapError(this.stream, (cause) => Multipart.MultipartError.fromReason("InternalError", cause)),
      Multipart.makeChannel(this.headers)
    )
  }

  get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError> {
    return Effect.map(this.bytes, (bytes) => bytes.slice().buffer)
  }

  get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
    return Effect.fail(requestParseError(this, "Not an upgradeable ServerRequest"))
  }
}

const getFormDataBody = (request: HttpServerRequest): FormData | undefined => {
  if (!HttpClientRequest.isHttpClientRequest(request.source)) {
    return undefined
  }
  const body = request.source.body
  if (body._tag === "FormData") {
    return body.formData
  }
  if (body._tag === "Raw" && isFormData(body.body)) {
    return body.body
  }
  return undefined
}

const rawBodyStream = (request: HttpServerRequest, body: unknown): Stream.Stream<Uint8Array, HttpServerError> => {
  if (body instanceof Request) {
    return streamFromReadable(request, body.body)
  }
  if (isFormData(body)) {
    return streamFromReadable(request, new Response(body).body)
  }
  if (isReadableStream(body)) {
    return streamFromReadable(request, body)
  }
  return Stream.fail(requestParseError(request, "Unsupported body type"))
}

const rawBodyBytes = (request: HttpServerRequest, body: unknown): Effect.Effect<Uint8Array, HttpServerError> => {
  if (body instanceof Blob) {
    return bytesFromBodyInit(request, body)
  }
  if (body instanceof Request) {
    return Effect.tryPromise({
      try: () => body.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
      catch: (cause) => requestParseError(request, undefined, cause)
    })
  }
  return Effect.fail(requestParseError(request, "Unsupported body type"))
}

const bytesFromBodyInit = (request: HttpServerRequest, body: BodyInit): Effect.Effect<Uint8Array, HttpServerError> =>
  Effect.tryPromise({
    try: () => new Response(body).arrayBuffer().then((buffer) => new Uint8Array(buffer)),
    catch: (cause) => requestParseError(request, undefined, cause)
  })

const streamFromReadable = (
  request: HttpServerRequest,
  body: ReadableStream<Uint8Array> | null | undefined
): Stream.Stream<Uint8Array, HttpServerError> =>
  body
    ? Stream.fromReadableStream({
      evaluate: () => body,
      onError: (cause) => requestParseError(request, undefined, cause)
    })
    : Stream.empty

const requestParseError = (
  request: HttpServerRequest,
  description?: string,
  cause?: unknown
) =>
  new HttpServerError({
    reason: new RequestParseError({
      request,
      ...(description === undefined ? undefined : { description }),
      ...(cause === undefined ? undefined : { cause })
    })
  })

const formDataToParts = (formData: FormData): Array<Multipart.Part> => {
  const parts: Array<Multipart.Part> = []
  for (const [key, value] of formData.entries()) {
    parts.push(typeof value === "string" ? new MultipartFieldPart(key, value) : new MultipartFilePart(key, value))
  }
  return parts
}

class MultipartFieldPart extends Inspectable.Class implements Multipart.Field {
  readonly [Multipart.TypeId]: typeof Multipart.TypeId
  readonly _tag = "Field"
  readonly contentType = "text/plain"
  readonly key: string
  readonly value: string

  constructor(
    key: string,
    value: string
  ) {
    super()
    this[Multipart.TypeId] = Multipart.TypeId
    this.key = key
    this.value = value
  }

  toJSON(): unknown {
    return {
      _id: "@effect/platform/Multipart/Part",
      _tag: "Field",
      key: this.key,
      contentType: this.contentType,
      value: this.value
    }
  }
}

class MultipartFilePart extends Inspectable.Class implements Multipart.File {
  readonly [Multipart.TypeId]: typeof Multipart.TypeId
  readonly _tag = "File"
  readonly key: string
  readonly name: string
  readonly contentType: string
  readonly content: Stream.Stream<Uint8Array, Multipart.MultipartError>
  readonly contentEffect: Effect.Effect<Uint8Array, Multipart.MultipartError>

  constructor(
    key: string,
    file: File
  ) {
    super()
    this[Multipart.TypeId] = Multipart.TypeId
    this.key = key
    this.name = file.name
    this.contentType = file.type
    this.content = Stream.fromReadableStream({
      evaluate: () => file.stream() as ReadableStream<Uint8Array>,
      onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
    })
    this.contentEffect = Effect.tryPromise({
      try: () => file.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
      catch: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
    })
  }

  toJSON(): unknown {
    return {
      _id: "@effect/platform/Multipart/Part",
      _tag: "File",
      key: this.key,
      name: this.name,
      contentType: this.contentType
    }
  }
}

const isReadableStream = (u: unknown): u is ReadableStream<Uint8Array> =>
  typeof ReadableStream !== "undefined" && u instanceof ReadableStream

const isFormData = (u: unknown): u is FormData => typeof FormData !== "undefined" && u instanceof FormData

const textDecoder = new TextDecoder()

/**
 * @since 4.0.0
 * @category conversions
 */
export const toURL = (self: HttpServerRequest): Option.Option<URL> => {
  const host = self.headers.host ?? "localhost"
  const protocol = self.headers["x-forwarded-proto"] === "https" ? "https" : "http"
  try {
    return Option.some(new URL(self.url, `${protocol}://${host}`))
  } catch {
    return Option.none()
  }
}

/**
 * @since 4.0.0
 * @category conversions
 */
export const toWebResult = (self: HttpServerRequest, options?: {
  readonly signal?: AbortSignal | undefined
  readonly context?: Context.Context<never> | undefined
}): Result.Result<Request, RequestError> => {
  if (self.source instanceof Request) {
    return Result.succeed(self.source)
  }
  const url = toURL(self)
  if (Option.isNone(url)) {
    return Result.fail(
      new RequestParseError({
        request: self,
        description: "Invalid URL"
      })
    )
  }
  const requestInit: RequestInit = {
    method: self.method,
    headers: self.headers
  }
  if (options?.signal) {
    requestInit.signal = options.signal
  }
  if (hasBody(self.method)) {
    requestInit.body = Stream.toReadableStreamWith(self.stream, options?.context ?? Context.empty())
    ;(requestInit as any).duplex = "half"
  }
  return Result.succeed(new Request(url.value, requestInit))
}

/**
 * @since 4.0.0
 * @category conversions
 */
export const toWeb = (self: HttpServerRequest, options?: {
  readonly signal?: AbortSignal | undefined
}): Effect.Effect<Request, RequestError> =>
  Effect.contextWith((context) =>
    toWebResult(self, {
      context,
      signal: options?.signal
    }).asEffect()
  )
