// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials

// import { ReadableStreamPrototype } from "ext:deno_web/06_streams.js";

const core = globalThis.__bootstrap.core;
import { TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { type Deferred, deferred } from "ext:deno_node/_util/async.ts";
import { setTimeout } from "ext:deno_web/02_timers.js";
import {
  _normalizeArgs,
  // createConnection,
  ListenOptions,
  Socket,
} from "node:net";
import { Buffer } from "node:buffer";
import { ERR_SERVER_NOT_RUNNING } from "ext:deno_node/internal/errors.ts";
import { EventEmitter } from "node:events";
import { nextTick } from "ext:deno_node/_next_tick.ts";
import {
  validateBoolean,
  validateInteger,
  validateObject,
  validatePort,
} from "ext:deno_node/internal/validators.mjs";
import {
  addAbortSignal,
  finished,
  Readable as NodeReadable,
  Writable as NodeWritable,
} from "node:stream";
import {
  OutgoingMessage,
  parseUniqueHeadersOption,
  validateHeaderName,
} from "ext:deno_node/_http_outgoing.ts";
import { ok as assert } from "node:assert";
import { kOutHeaders } from "ext:deno_node/internal/http.ts";
import { _checkIsHttpToken as checkIsHttpToken } from "ext:deno_node/_http_common.ts";
import { Agent, globalAgent } from "ext:deno_node/_http_agent.mjs";
// import { chunkExpression as RE_TE_CHUNKED } from "ext:deno_node/_http_common.ts";
import { urlToHttpOptions } from "ext:deno_node/internal/url.ts";
import { kEmptyObject } from "ext:deno_node/internal/util.mjs";
import { constants, TCP } from "ext:deno_node/internal_binding/tcp_wrap.ts";
import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts";
import {
  connResetException,
  ERR_HTTP_HEADERS_SENT,
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_HTTP_TOKEN,
  ERR_INVALID_PROTOCOL,
  ERR_UNESCAPED_CHARACTERS,
} from "ext:deno_node/internal/errors.ts";
import { getTimerDuration } from "ext:deno_node/internal/timers.mjs";
import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.js";
import { createHttpClient } from "ext:deno_fetch/22_http_client.js";
import { headersEntries } from "ext:deno_fetch/20_headers.js";
import { timerId } from "ext:deno_web/03_abort_signal.js";
import { clearTimeout as webClearTimeout } from "ext:deno_web/02_timers.js";
import { TcpConn } from "ext:deno_net/01_net.js";

enum STATUS_CODES {
  /** RFC 7231, 6.2.1 */
  Continue = 100,
  /** RFC 7231, 6.2.2 */
  SwitchingProtocols = 101,
  /** RFC 2518, 10.1 */
  Processing = 102,
  /** RFC 8297 **/
  EarlyHints = 103,

  /** RFC 7231, 6.3.1 */
  OK = 200,
  /** RFC 7231, 6.3.2 */
  Created = 201,
  /** RFC 7231, 6.3.3 */
  Accepted = 202,
  /** RFC 7231, 6.3.4 */
  NonAuthoritativeInfo = 203,
  /** RFC 7231, 6.3.5 */
  NoContent = 204,
  /** RFC 7231, 6.3.6 */
  ResetContent = 205,
  /** RFC 7233, 4.1 */
  PartialContent = 206,
  /** RFC 4918, 11.1 */
  MultiStatus = 207,
  /** RFC 5842, 7.1 */
  AlreadyReported = 208,
  /** RFC 3229, 10.4.1 */
  IMUsed = 226,

  /** RFC 7231, 6.4.1 */
  MultipleChoices = 300,
  /** RFC 7231, 6.4.2 */
  MovedPermanently = 301,
  /** RFC 7231, 6.4.3 */
  Found = 302,
  /** RFC 7231, 6.4.4 */
  SeeOther = 303,
  /** RFC 7232, 4.1 */
  NotModified = 304,
  /** RFC 7231, 6.4.5 */
  UseProxy = 305,
  /** RFC 7231, 6.4.7 */
  TemporaryRedirect = 307,
  /** RFC 7538, 3 */
  PermanentRedirect = 308,

  /** RFC 7231, 6.5.1 */
  BadRequest = 400,
  /** RFC 7235, 3.1 */
  Unauthorized = 401,
  /** RFC 7231, 6.5.2 */
  PaymentRequired = 402,
  /** RFC 7231, 6.5.3 */
  Forbidden = 403,
  /** RFC 7231, 6.5.4 */
  NotFound = 404,
  /** RFC 7231, 6.5.5 */
  MethodNotAllowed = 405,
  /** RFC 7231, 6.5.6 */
  NotAcceptable = 406,
  /** RFC 7235, 3.2 */
  ProxyAuthRequired = 407,
  /** RFC 7231, 6.5.7 */
  RequestTimeout = 408,
  /** RFC 7231, 6.5.8 */
  Conflict = 409,
  /** RFC 7231, 6.5.9 */
  Gone = 410,
  /** RFC 7231, 6.5.10 */
  LengthRequired = 411,
  /** RFC 7232, 4.2 */
  PreconditionFailed = 412,
  /** RFC 7231, 6.5.11 */
  RequestEntityTooLarge = 413,
  /** RFC 7231, 6.5.12 */
  RequestURITooLong = 414,
  /** RFC 7231, 6.5.13 */
  UnsupportedMediaType = 415,
  /** RFC 7233, 4.4 */
  RequestedRangeNotSatisfiable = 416,
  /** RFC 7231, 6.5.14 */
  ExpectationFailed = 417,
  /** RFC 7168, 2.3.3 */
  Teapot = 418,
  /** RFC 7540, 9.1.2 */
  MisdirectedRequest = 421,
  /** RFC 4918, 11.2 */
  UnprocessableEntity = 422,
  /** RFC 4918, 11.3 */
  Locked = 423,
  /** RFC 4918, 11.4 */
  FailedDependency = 424,
  /** RFC 8470, 5.2 */
  TooEarly = 425,
  /** RFC 7231, 6.5.15 */
  UpgradeRequired = 426,
  /** RFC 6585, 3 */
  PreconditionRequired = 428,
  /** RFC 6585, 4 */
  TooManyRequests = 429,
  /** RFC 6585, 5 */
  RequestHeaderFieldsTooLarge = 431,
  /** RFC 7725, 3 */
  UnavailableForLegalReasons = 451,

  /** RFC 7231, 6.6.1 */
  InternalServerError = 500,
  /** RFC 7231, 6.6.2 */
  NotImplemented = 501,
  /** RFC 7231, 6.6.3 */
  BadGateway = 502,
  /** RFC 7231, 6.6.4 */
  ServiceUnavailable = 503,
  /** RFC 7231, 6.6.5 */
  GatewayTimeout = 504,
  /** RFC 7231, 6.6.6 */
  HTTPVersionNotSupported = 505,
  /** RFC 2295, 8.1 */
  VariantAlsoNegotiates = 506,
  /** RFC 4918, 11.5 */
  InsufficientStorage = 507,
  /** RFC 5842, 7.2 */
  LoopDetected = 508,
  /** RFC 2774, 7 */
  NotExtended = 510,
  /** RFC 6585, 6 */
  NetworkAuthenticationRequired = 511,
}

const METHODS = [
  "ACL",
  "BIND",
  "CHECKOUT",
  "CONNECT",
  "COPY",
  "DELETE",
  "GET",
  "HEAD",
  "LINK",
  "LOCK",
  "M-SEARCH",
  "MERGE",
  "MKACTIVITY",
  "MKCALENDAR",
  "MKCOL",
  "MOVE",
  "NOTIFY",
  "OPTIONS",
  "PATCH",
  "POST",
  "PROPFIND",
  "PROPPATCH",
  "PURGE",
  "PUT",
  "REBIND",
  "REPORT",
  "SEARCH",
  "SOURCE",
  "SUBSCRIBE",
  "TRACE",
  "UNBIND",
  "UNLINK",
  "UNLOCK",
  "UNSUBSCRIBE",
];

type Chunk = string | Buffer | Uint8Array;

const ENCODER = new TextEncoder();

export interface RequestOptions {
  agent?: Agent;
  auth?: string;
  createConnection?: () => unknown;
  defaultPort?: number;
  family?: number;
  headers?: Record<string, string>;
  hints?: number;
  host?: string;
  hostname?: string;
  insecureHTTPParser?: boolean;
  localAddress?: string;
  localPort?: number;
  lookup?: () => void;
  maxHeaderSize?: number;
  method?: string;
  path?: string;
  port?: number;
  protocol?: string;
  setHost?: boolean;
  socketPath?: string;
  timeout?: number;
  signal?: AbortSignal;
  href?: string;
}

function validateHost(host, name) {
  if (host !== null && host !== undefined && typeof host !== "string") {
    throw new ERR_INVALID_ARG_TYPE(`options.${name}`, [
      "string",
      "undefined",
      "null",
    ], host);
  }
  return host;
}

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
const kError = Symbol("kError");

const kUniqueHeaders = Symbol("kUniqueHeaders");

class FakeSocket extends EventEmitter {
  constructor(opts = {}) {
    super();
    this.remoteAddress = opts.hostname;
    this.remotePort = opts.port;
    this.encrypted = opts.encrypted;
    this.writable = true;
    this.readable = true;
  }

  setKeepAlive() {}

  end() {}

  destroy() {}

  setTimeout(callback, timeout = 0, ...args) {
    setTimeout(callback, timeout, args);
  }
}

/** ClientRequest represents the http(s) request from the client */
class ClientRequest extends OutgoingMessage {
  defaultProtocol = "http:";
  aborted = false;
  destroyed = false;
  agent: Agent;
  method: string;
  maxHeaderSize: number | undefined;
  insecureHTTPParser: boolean;
  useChunkedEncodingByDefault: boolean;
  path: string;

  constructor(
    input: string | URL,
    options?: RequestOptions,
    cb?: (res: IncomingMessageForClient) => void,
  ) {
    super();

    if (typeof input === "string") {
      const urlStr = input;
      input = urlToHttpOptions(new URL(urlStr));
    } else if (input instanceof URL) {
      // url.URL instance
      input = urlToHttpOptions(input);
    } else {
      cb = options;
      options = input;
      input = null;
    }

    if (typeof options === "function") {
      cb = options;
      options = input || kEmptyObject;
    } else {
      options = Object.assign(input || {}, options);
    }

    let agent = options!.agent;
    const defaultAgent = options!._defaultAgent || globalAgent;
    if (agent === false) {
      agent = new defaultAgent.constructor();
    } else if (agent === null || agent === undefined) {
      if (typeof options!.createConnection !== "function") {
        agent = defaultAgent;
      }
      // Explicitly pass through this statement as agent will not be used
      // when createConnection is provided.
    } else if (typeof agent.addRequest !== "function") {
      throw new ERR_INVALID_ARG_TYPE("options.agent", [
        "Agent-like Object",
        "undefined",
        "false",
      ], agent);
    }
    this.agent = agent;

    const protocol = options!.protocol || defaultAgent.protocol;
    let expectedProtocol = defaultAgent.protocol;
    if (this.agent?.protocol) {
      expectedProtocol = this.agent!.protocol;
    }

    if (options!.path) {
      const path = String(options.path);
      if (INVALID_PATH_REGEX.exec(path) !== null) {
        throw new ERR_UNESCAPED_CHARACTERS("Request path");
      }
    }

    if (protocol !== expectedProtocol) {
      throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol);
    }

    const defaultPort = options!.defaultPort || this.agent?.defaultPort;

    const port = options!.port = options!.port || defaultPort || 80;
    const host = options!.host = validateHost(options!.hostname, "hostname") ||
      validateHost(options!.host, "host") || "localhost";

    const setHost = options!.setHost === undefined || Boolean(options!.setHost);

    this.socketPath = options!.socketPath;

    if (options!.timeout !== undefined) {
      this.setTimeout(options.timeout);
    }

    const signal = options!.signal;
    if (signal) {
      addAbortSignal(signal, this);
    }
    let method = options!.method;
    const methodIsString = typeof method === "string";
    if (method !== null && method !== undefined && !methodIsString) {
      throw new ERR_INVALID_ARG_TYPE("options.method", "string", method);
    }

    if (methodIsString && method) {
      if (!checkIsHttpToken(method)) {
        throw new ERR_INVALID_HTTP_TOKEN("Method", method);
      }
      method = this.method = method.toUpperCase();
    } else {
      method = this.method = "GET";
    }

    const maxHeaderSize = options!.maxHeaderSize;
    if (maxHeaderSize !== undefined) {
      validateInteger(maxHeaderSize, "maxHeaderSize", 0);
    }
    this.maxHeaderSize = maxHeaderSize;

    const insecureHTTPParser = options!.insecureHTTPParser;
    if (insecureHTTPParser !== undefined) {
      validateBoolean(insecureHTTPParser, "options.insecureHTTPParser");
    }

    this.insecureHTTPParser = insecureHTTPParser;

    if (options!.joinDuplicateHeaders !== undefined) {
      validateBoolean(
        options!.joinDuplicateHeaders,
        "options.joinDuplicateHeaders",
      );
    }

    this.joinDuplicateHeaders = options!.joinDuplicateHeaders;

    this.path = options!.path || "/";
    if (cb) {
      this.once("response", cb);
    }

    if (
      method === "GET" ||
      method === "HEAD" ||
      method === "DELETE" ||
      method === "OPTIONS" ||
      method === "TRACE" ||
      method === "CONNECT"
    ) {
      this.useChunkedEncodingByDefault = false;
    } else {
      this.useChunkedEncodingByDefault = true;
    }

    this._ended = false;
    this.res = null;
    this.aborted = false;
    this.upgradeOrConnect = false;
    this.parser = null;
    this.maxHeadersCount = null;
    this.reusedSocket = false;
    this.host = host;
    this.protocol = protocol;
    this.port = port;
    this.hash = options.hash;
    this.search = options.search;
    this.auth = options.auth;

    if (this.agent) {
      // If there is an agent we should default to Connection:keep-alive,
      // but only if the Agent will actually reuse the connection!
      // If it's not a keepAlive agent, and the maxSockets==Infinity, then
      // there's never a case where this socket will actually be reused
      if (!this.agent.keepAlive && !Number.isFinite(this.agent.maxSockets)) {
        this._last = true;
        this.shouldKeepAlive = false;
      } else {
        this._last = false;
        this.shouldKeepAlive = true;
      }
    }

    const headersArray = Array.isArray(options!.headers);
    if (!headersArray) {
      if (options!.headers) {
        const keys = Object.keys(options!.headers);
        // Retain for(;;) loop for performance reasons
        // Refs: https://github.com/nodejs/node/pull/30958
        for (let i = 0; i < keys.length; i++) {
          const key = keys[i];
          this.setHeader(key, options!.headers[key]);
        }
      }

      if (host && !this.getHeader("host") && setHost) {
        let hostHeader = host;

        // For the Host header, ensure that IPv6 addresses are enclosed
        // in square brackets, as defined by URI formatting
        // https://tools.ietf.org/html/rfc3986#section-3.2.2
        const posColon = hostHeader.indexOf(":");
        if (
          posColon !== -1 &&
          hostHeader.includes(":", posColon + 1) &&
          hostHeader.charCodeAt(0) !== 91 /* '[' */
        ) {
          hostHeader = `[${hostHeader}]`;
        }

        if (port && +port !== defaultPort) {
          hostHeader += ":" + port;
        }
        this.setHeader("Host", hostHeader);
      }

      if (options!.auth && !this.getHeader("Authorization")) {
        this.setHeader(
          "Authorization",
          "Basic " +
            Buffer.from(options!.auth).toString("base64"),
        );
      }

      if (this.getHeader("expect") && this._header) {
        throw new ERR_HTTP_HEADERS_SENT("render");
      }
    } else {
      for (const [key, val] of options!.headers) {
        this.setHeader(key, val);
      }
    }

    this[kUniqueHeaders] = parseUniqueHeadersOption(options!.uniqueHeaders);

    let optsWithoutSignal = options as RequestOptions;
    if (optsWithoutSignal.signal) {
      optsWithoutSignal = Object.assign({}, options);
      delete optsWithoutSignal.signal;
    }

    if (options!.createConnection) {
      warnNotImplemented("ClientRequest.options.createConnection");
    }

    if (options!.lookup) {
      notImplemented("ClientRequest.options.lookup");
    }

    // initiate connection
    // TODO(crowlKats): finish this
    /*if (this.agent) {
      this.agent.addRequest(this, optsWithoutSignal);
    } else {
      // No agent, default to Connection:close.
      this._last = true;
      this.shouldKeepAlive = false;
      if (typeof optsWithoutSignal.createConnection === "function") {
        const oncreate = once((err, socket) => {
          if (err) {
            this.emit("error", err);
          } else {
            this.onSocket(socket);
          }
        });

        try {
          const newSocket = optsWithoutSignal.createConnection(
            optsWithoutSignal,
            oncreate,
          );
          if (newSocket) {
            oncreate(null, newSocket);
          }
        } catch (err) {
          oncreate(err);
        }
      } else {
        debug("CLIENT use net.createConnection", optsWithoutSignal);
        this.onSocket(createConnection(optsWithoutSignal));
      }
    }*/
    this.onSocket(new FakeSocket({ encrypted: this._encrypted }));
  }

  _writeHeader() {
    const url = this._createUrlStrFromOptions();

    const headers = [];
    for (const key in this[kOutHeaders]) {
      if (Object.hasOwn(this[kOutHeaders], key)) {
        const entry = this[kOutHeaders][key];
        this._processHeader(headers, entry[0], entry[1], false);
      }
    }

    const client = this._getClient() ?? createHttpClient({ http2: false });
    this._client = client;

    this._req = core.ops.op_node_http_request(
      this.method,
      url,
      headers,
      client.rid,
      (this.method === "POST" || this.method === "PATCH" ||
        this.method === "PUT") && this._contentLength !== 0,
    );
    this._bodyWriteRid = this._req.requestBodyRid;
  }

  _implicitHeader() {
    if (this._header) {
      throw new ERR_HTTP_HEADERS_SENT("render");
    }
    this._storeHeader(
      this.method + " " + this.path + " HTTP/1.1\r\n",
      this[kOutHeaders],
    );
  }

  _getClient(): Deno.HttpClient | undefined {
    return undefined;
  }

  // TODO(bartlomieju): handle error
  onSocket(socket, _err) {
    nextTick(() => {
      this.socket = socket;
      this.emit("socket", socket);
    });
  }

  // deno-lint-ignore no-explicit-any
  end(chunk?: any, encoding?: any, cb?: any): this {
    if (typeof chunk === "function") {
      cb = chunk;
      chunk = null;
      encoding = null;
    } else if (typeof encoding === "function") {
      cb = encoding;
      encoding = null;
    }

    this.finished = true;
    if (chunk) {
      this.write_(chunk, encoding, null, true);
    } else if (!this._headerSent) {
      this._contentLength = 0;
      this._implicitHeader();
      this._send("", "latin1");
    }

    (async () => {
      try {
        const [res, _] = await Promise.all([
          core.opAsync("op_fetch_send", this._req.requestRid),
          (async () => {
            if (this._bodyWriteRid) {
              try {
                await core.shutdown(this._bodyWriteRid);
              } catch (err) {
                this._requestSendError = err;
              }

              core.tryClose(this._bodyWriteRid);
            }
          })(),
        ]);
        try {
          cb?.();
        } catch (_) {
          //
        }
        if (this._timeout) {
          this._timeout.removeEventListener("abort", this._timeoutCb);
          webClearTimeout(this._timeout[timerId]);
        }
        this._client.close();
        const incoming = new IncomingMessageForClient(this.socket);
        incoming.req = this;
        this.res = incoming;

        // TODO(@crowlKats):
        // incoming.httpVersionMajor = versionMajor;
        // incoming.httpVersionMinor = versionMinor;
        // incoming.httpVersion = `${versionMajor}.${versionMinor}`;
        // incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
        //  parser.joinDuplicateHeaders;

        incoming.url = res.url;
        incoming.statusCode = res.status;
        incoming.statusMessage = res.statusText;
        incoming.upgrade = null;

        for (const [key, _value] of res.headers) {
          if (key.toLowerCase() === "upgrade") {
            incoming.upgrade = true;
            break;
          }
        }

        incoming._addHeaderLines(
          res.headers,
          Object.entries(res.headers).flat().length,
        );

        if (this._req.cancelHandleRid !== null) {
          core.tryClose(this._req.cancelHandleRid);
        }

        if (incoming.upgrade) {
          if (this.listenerCount("upgrade") === 0) {
            // No listeners, so we got nothing to do
            // destroy?
            return;
          }

          if (this.method === "CONNECT") {
            throw new Error("not implemented CONNECT");
          }

          const upgradeRid = await core.opAsync(
            "op_fetch_response_upgrade",
            res.responseRid,
          );
          assert(typeof res.remoteAddrIp !== "undefined");
          assert(typeof res.remoteAddrIp !== "undefined");
          const conn = new TcpConn(
            upgradeRid,
            {
              transport: "tcp",
              hostname: res.remoteAddrIp,
              port: res.remoteAddrIp,
            },
            // TODO(bartlomieju): figure out actual values
            {
              transport: "tcp",
              hostname: "127.0.0.1",
              port: 80,
            },
          );
          const socket = new Socket({
            handle: new TCP(constants.SERVER, conn),
          });

          this.upgradeOrConnect = true;

          this.emit("upgrade", incoming, socket, Buffer.from([]));
          this.destroyed = true;
          this._closed = true;
          this.emit("close");
        } else {
          {
            incoming._bodyRid = res.responseRid;
          }
          this.emit("response", incoming);
        }
      } catch (err) {
        if (this._req.cancelHandleRid !== null) {
          core.tryClose(this._req.cancelHandleRid);
        }

        if (this._requestSendError !== undefined) {
          // if the request body stream errored, we want to propagate that error
          // instead of the original error from opFetchSend
          throw new TypeError(
            "Failed to fetch: request body stream errored",
            {
              cause: this._requestSendError,
            },
          );
        }

        if (
          err.message.includes("connection closed before message completed")
        ) {
          // Node.js seems ignoring this error
        } else if (err.message.includes("The signal has been aborted")) {
          // Remap this error
          this.emit("error", connResetException("socket hang up"));
        } else {
          this.emit("error", err);
        }
      }
    })();
  }

  abort() {
    if (this.aborted) {
      return;
    }
    this.aborted = true;
    this.emit("abort");
    //process.nextTick(emitAbortNT, this);
    this.destroy();
  }

  // deno-lint-ignore no-explicit-any
  destroy(err?: any) {
    if (this.destroyed) {
      return this;
    }
    this.destroyed = true;

    const rid = this._client?.rid;
    if (rid) {
      core.tryClose(rid);
    }
    if (this._req.cancelHandleRid !== null) {
      core.tryClose(this._req.cancelHandleRid);
    }
    // If we're aborting, we don't care about any more response data.
    if (this.res) {
      this.res._dump();
    }

    this[kError] = err;
    this.socket?.destroy(err);

    return this;
  }

  _createCustomClient(): Promise<Deno.HttpClient | undefined> {
    return Promise.resolve(undefined);
  }

  _createUrlStrFromOptions(): string {
    if (this.href) {
      return this.href;
    }
    const protocol = this.protocol ?? this.defaultProtocol;
    const auth = this.auth;
    const host = this.host ?? this.hostname ?? "localhost";
    const hash = this.hash ? `#${this.hash}` : "";
    const defaultPort = this.agent?.defaultPort;
    const port = this.port ?? defaultPort ?? 80;
    let path = this.path ?? "/";
    if (!path.startsWith("/")) {
      path = "/" + path;
    }
    const url = new URL(
      `${protocol}//${auth ? `${auth}@` : ""}${host}${
        port === 80 ? "" : `:${port}`
      }${path}`,
    );
    url.hash = hash;
    return url.href;
  }

  setTimeout(msecs: number, callback?: () => void) {
    if (msecs === 0) {
      if (this._timeout) {
        this.removeAllListeners("timeout");
        this._timeout.removeEventListener("abort", this._timeoutCb);
        this._timeout = undefined;
      }

      return this;
    }
    if (this._ended || this._timeout) {
      return this;
    }

    msecs = getTimerDuration(msecs, "msecs");
    if (callback) this.once("timeout", callback);

    const timeout = AbortSignal.timeout(msecs);
    this._timeoutCb = () => this.emit("timeout");
    timeout.addEventListener("abort", this._timeoutCb);
    this._timeout = timeout;

    return this;
  }

  _processHeader(headers, key, value, validate) {
    if (validate) {
      validateHeaderName(key);
    }

    // If key is content-disposition and there is content-length
    // encode the value in latin1
    // https://www.rfc-editor.org/rfc/rfc6266#section-4.3
    // Refs: https://github.com/nodejs/node/pull/46528
    if (isContentDispositionField(key) && this._contentLength) {
      value = Buffer.from(value, "latin1");
    }

    if (Array.isArray(value)) {
      if (
        (value.length < 2 || !isCookieField(key)) &&
        (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(key.toLowerCase()))
      ) {
        // Retain for(;;) loop for performance reasons
        // Refs: https://github.com/nodejs/node/pull/30958
        for (let i = 0; i < value.length; i++) {
          headers.push([key, value[i]]);
        }
        return;
      }
      value = value.join("; ");
    }
    headers.push([key, value]);
  }
}

// isCookieField performs a case-insensitive comparison of a provided string
// against the word "cookie." As of V8 6.6 this is faster than handrolling or
// using a case-insensitive RegExp.
function isCookieField(s) {
  return s.length === 6 && s.toLowerCase() === "cookie";
}

function isContentDispositionField(s) {
  return s.length === 19 &&
    s.toLowerCase() === "content-disposition";
}

const kHeaders = Symbol("kHeaders");
const kHeadersDistinct = Symbol("kHeadersDistinct");
const kHeadersCount = Symbol("kHeadersCount");
const kTrailers = Symbol("kTrailers");
const kTrailersDistinct = Symbol("kTrailersDistinct");
const kTrailersCount = Symbol("kTrailersCount");

/** IncomingMessage for http(s) client */
export class IncomingMessageForClient extends NodeReadable {
  decoder = new TextDecoder();

  constructor(socket: Socket) {
    super();

    this._readableState.readingMore = true;

    this.socket = socket;

    this.httpVersionMajor = null;
    this.httpVersionMinor = null;
    this.httpVersion = null;
    this.complete = false;
    this[kHeaders] = null;
    this[kHeadersCount] = 0;
    this.rawHeaders = [];
    this[kTrailers] = null;
    this[kTrailersCount] = 0;
    this.rawTrailers = [];
    this.joinDuplicateHeaders = false;
    this.aborted = false;

    this.upgrade = null;

    // request (server) only
    this.url = "";
    this.method = null;

    // response (client) only
    this.statusCode = null;
    this.statusMessage = null;
    this.client = socket;

    this._consuming = false;
    // Flag for when we decide that this message cannot possibly be
    // read by the user, so there's no point continuing to handle it.
    this._dumped = false;
  }

  get connection() {
    return this.socket;
  }

  set connection(val) {
    this.socket = val;
  }

  get headers() {
    if (!this[kHeaders]) {
      this[kHeaders] = {};

      const src = this.rawHeaders;
      const dst = this[kHeaders];

      for (let n = 0; n < this[kHeadersCount]; n += 2) {
        this._addHeaderLine(src[n + 0], src[n + 1], dst);
      }
    }
    return this[kHeaders];
  }

  set headers(val) {
    this[kHeaders] = val;
  }

  get headersDistinct() {
    if (!this[kHeadersDistinct]) {
      this[kHeadersDistinct] = {};

      const src = this.rawHeaders;
      const dst = this[kHeadersDistinct];

      for (let n = 0; n < this[kHeadersCount]; n += 2) {
        this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
      }
    }
    return this[kHeadersDistinct];
  }

  set headersDistinct(val) {
    this[kHeadersDistinct] = val;
  }

  get trailers() {
    if (!this[kTrailers]) {
      this[kTrailers] = {};

      const src = this.rawTrailers;
      const dst = this[kTrailers];

      for (let n = 0; n < this[kTrailersCount]; n += 2) {
        this._addHeaderLine(src[n + 0], src[n + 1], dst);
      }
    }
    return this[kTrailers];
  }

  set trailers(val) {
    this[kTrailers] = val;
  }

  get trailersDistinct() {
    if (!this[kTrailersDistinct]) {
      this[kTrailersDistinct] = {};

      const src = this.rawTrailers;
      const dst = this[kTrailersDistinct];

      for (let n = 0; n < this[kTrailersCount]; n += 2) {
        this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
      }
    }
    return this[kTrailersDistinct];
  }

  set trailersDistinct(val) {
    this[kTrailersDistinct] = val;
  }

  setTimeout(msecs, callback) {
    if (callback) {
      this.on("timeout", callback);
    }
    this.socket.setTimeout(msecs);
    return this;
  }

  _read(_n) {
    if (!this._consuming) {
      this._readableState.readingMore = false;
      this._consuming = true;
    }

    const buf = new Uint8Array(16 * 1024);

    core.read(this._bodyRid, buf).then((bytesRead) => {
      if (bytesRead === 0) {
        this.push(null);
      } else {
        this.push(Buffer.from(buf.subarray(0, bytesRead)));
      }
    });
  }

  // It's possible that the socket will be destroyed, and removed from
  // any messages, before ever calling this.  In that case, just skip
  // it, since something else is destroying this connection anyway.
  _destroy(err, cb) {
    this.complete = true;
    if (!this.readableEnded || !this.complete) {
      this.aborted = true;
      this.emit("aborted");
    }

    core.tryClose(this._bodyRid);

    // If aborted and the underlying socket is not already destroyed,
    // destroy it.
    // We have to check if the socket is already destroyed because finished
    // does not call the callback when this method is invoked from `_http_client`
    // in `test/parallel/test-http-client-spurious-aborted.js`
    if (this.socket && !this.socket.destroyed && this.aborted) {
      this.socket.destroy(err);
      const cleanup = finished(this.socket, (e) => {
        if (e?.code === "ERR_STREAM_PREMATURE_CLOSE") {
          e = null;
        }
        cleanup();
        onError(this, e || err, cb);
      });
    } else {
      onError(this, err, cb);
    }
  }

  _addHeaderLines(headers, n) {
    if (headers && headers.length) {
      let dest;
      if (this.complete) {
        this.rawTrailers = headers.flat();
        this[kTrailersCount] = n;
        dest = this[kTrailers];
      } else {
        this.rawHeaders = headers.flat();
        this[kHeadersCount] = n;
        dest = this[kHeaders];
      }

      if (dest) {
        for (const header of headers) {
          this._addHeaderLine(header[0], header[1], dest);
        }
      }
    }
  }

  // Add the given (field, value) pair to the message
  //
  // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
  // same header with a ', ' if the header in question supports specification of
  // multiple values this way. The one exception to this is the Cookie header,
  // which has multiple values joined with a '; ' instead. If a header's values
  // cannot be joined in either of these ways, we declare the first instance the
  // winner and drop the second. Extended header fields (those beginning with
  // 'x-') are always joined.
  _addHeaderLine(field, value, dest) {
    field = matchKnownFields(field);
    const flag = field.charCodeAt(0);
    if (flag === 0 || flag === 2) {
      field = field.slice(1);
      // Make a delimited list
      if (typeof dest[field] === "string") {
        dest[field] += (flag === 0 ? ", " : "; ") + value;
      } else {
        dest[field] = value;
      }
    } else if (flag === 1) {
      // Array header -- only Set-Cookie at the moment
      if (dest["set-cookie"] !== undefined) {
        dest["set-cookie"].push(value);
      } else {
        dest["set-cookie"] = [value];
      }
    } else if (this.joinDuplicateHeaders) {
      // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
      // https://github.com/nodejs/node/issues/45699
      // allow authorization multiple fields
      // Make a delimited list
      if (dest[field] === undefined) {
        dest[field] = value;
      } else {
        dest[field] += ", " + value;
      }
    } else if (dest[field] === undefined) {
      // Drop duplicates
      dest[field] = value;
    }
  }

  _addHeaderLineDistinct(field, value, dest) {
    field = field.toLowerCase();
    if (!dest[field]) {
      dest[field] = [value];
    } else {
      dest[field].push(value);
    }
  }

  // Call this instead of resume() if we want to just
  // dump all the data to /dev/null
  _dump() {
    if (!this._dumped) {
      this._dumped = true;
      // If there is buffered data, it may trigger 'data' events.
      // Remove 'data' event listeners explicitly.
      this.removeAllListeners("data");
      this.resume();
    }
  }
}

// This function is used to help avoid the lowercasing of a field name if it
// matches a 'traditional cased' version of a field name. It then returns the
// lowercased name to both avoid calling toLowerCase() a second time and to
// indicate whether the field was a 'no duplicates' field. If a field is not a
// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception
// to this is the Set-Cookie header which is indicated by a `1` byte flag, since
// it is an 'array' field and thus is treated differently in _addHeaderLines().
function matchKnownFields(field, lowercased) {
  switch (field.length) {
    case 3:
      if (field === "Age" || field === "age") return "age";
      break;
    case 4:
      if (field === "Host" || field === "host") return "host";
      if (field === "From" || field === "from") return "from";
      if (field === "ETag" || field === "etag") return "etag";
      if (field === "Date" || field === "date") return "\u0000date";
      if (field === "Vary" || field === "vary") return "\u0000vary";
      break;
    case 6:
      if (field === "Server" || field === "server") return "server";
      if (field === "Cookie" || field === "cookie") return "\u0002cookie";
      if (field === "Origin" || field === "origin") return "\u0000origin";
      if (field === "Expect" || field === "expect") return "\u0000expect";
      if (field === "Accept" || field === "accept") return "\u0000accept";
      break;
    case 7:
      if (field === "Referer" || field === "referer") return "referer";
      if (field === "Expires" || field === "expires") return "expires";
      if (field === "Upgrade" || field === "upgrade") return "\u0000upgrade";
      break;
    case 8:
      if (field === "Location" || field === "location") {
        return "location";
      }
      if (field === "If-Match" || field === "if-match") {
        return "\u0000if-match";
      }
      break;
    case 10:
      if (field === "User-Agent" || field === "user-agent") {
        return "user-agent";
      }
      if (field === "Set-Cookie" || field === "set-cookie") {
        return "\u0001";
      }
      if (field === "Connection" || field === "connection") {
        return "\u0000connection";
      }
      break;
    case 11:
      if (field === "Retry-After" || field === "retry-after") {
        return "retry-after";
      }
      break;
    case 12:
      if (field === "Content-Type" || field === "content-type") {
        return "content-type";
      }
      if (field === "Max-Forwards" || field === "max-forwards") {
        return "max-forwards";
      }
      break;
    case 13:
      if (field === "Authorization" || field === "authorization") {
        return "authorization";
      }
      if (field === "Last-Modified" || field === "last-modified") {
        return "last-modified";
      }
      if (field === "Cache-Control" || field === "cache-control") {
        return "\u0000cache-control";
      }
      if (field === "If-None-Match" || field === "if-none-match") {
        return "\u0000if-none-match";
      }
      break;
    case 14:
      if (field === "Content-Length" || field === "content-length") {
        return "content-length";
      }
      break;
    case 15:
      if (field === "Accept-Encoding" || field === "accept-encoding") {
        return "\u0000accept-encoding";
      }
      if (field === "Accept-Language" || field === "accept-language") {
        return "\u0000accept-language";
      }
      if (field === "X-Forwarded-For" || field === "x-forwarded-for") {
        return "\u0000x-forwarded-for";
      }
      break;
    case 16:
      if (field === "Content-Encoding" || field === "content-encoding") {
        return "\u0000content-encoding";
      }
      if (field === "X-Forwarded-Host" || field === "x-forwarded-host") {
        return "\u0000x-forwarded-host";
      }
      break;
    case 17:
      if (field === "If-Modified-Since" || field === "if-modified-since") {
        return "if-modified-since";
      }
      if (field === "Transfer-Encoding" || field === "transfer-encoding") {
        return "\u0000transfer-encoding";
      }
      if (field === "X-Forwarded-Proto" || field === "x-forwarded-proto") {
        return "\u0000x-forwarded-proto";
      }
      break;
    case 19:
      if (field === "Proxy-Authorization" || field === "proxy-authorization") {
        return "proxy-authorization";
      }
      if (field === "If-Unmodified-Since" || field === "if-unmodified-since") {
        return "if-unmodified-since";
      }
      break;
  }
  if (lowercased) {
    return "\u0000" + field;
  }
  return matchKnownFields(field.toLowerCase(), true);
}

function onError(self, error, cb) {
  // This is to keep backward compatible behavior.
  // An error is emitted only if there are listeners attached to the event.
  if (self.listenerCount("error") === 0) {
    cb();
  } else {
    cb(error);
  }
}

export class ServerResponse extends NodeWritable {
  statusCode?: number = undefined;
  statusMessage?: string = undefined;
  #headers = new Headers({});
  #readable: ReadableStream;
  override writable = true;
  // used by `npm:on-finished`
  finished = false;
  headersSent = false;
  #firstChunk: Chunk | null = null;
  #resolve: (value: Response | PromiseLike<Response>) => void;

  static #enqueue(controller: ReadableStreamDefaultController, chunk: Chunk) {
    if (typeof chunk === "string") {
      controller.enqueue(ENCODER.encode(chunk));
    } else {
      controller.enqueue(chunk);
    }
  }

  /** Returns true if the response body should be null with the given
   * http status code */
  static #bodyShouldBeNull(status: number) {
    return status === 101 || status === 204 || status === 205 || status === 304;
  }

  constructor(
    resolve: (value: Response | PromiseLike<Response>) => void,
    socket: FakeSocket,
  ) {
    let controller: ReadableByteStreamController;
    const readable = new ReadableStream({
      start(c) {
        controller = c as ReadableByteStreamController;
      },
    });
    super({
      autoDestroy: true,
      defaultEncoding: "utf-8",
      emitClose: true,
      write: (chunk, _encoding, cb) => {
        if (!this.headersSent) {
          if (this.#firstChunk === null) {
            this.#firstChunk = chunk;
            return cb();
          } else {
            ServerResponse.#enqueue(controller, this.#firstChunk);
            this.#firstChunk = null;
            this.respond(false);
          }
        }
        ServerResponse.#enqueue(controller, chunk);
        return cb();
      },
      final: (cb) => {
        if (this.#firstChunk) {
          this.respond(true, this.#firstChunk);
        } else if (!this.headersSent) {
          this.respond(true);
        }
        controller.close();
        return cb();
      },
      destroy: (err, cb) => {
        if (err) {
          controller.error(err);
        }
        return cb(null);
      },
    });
    this.#readable = readable;
    this.#resolve = resolve;
    this.socket = socket;
  }

  setHeader(name: string, value: string) {
    this.#headers.set(name, value);
    return this;
  }

  getHeader(name: string) {
    return this.#headers.get(name);
  }
  removeHeader(name: string) {
    return this.#headers.delete(name);
  }
  getHeaderNames() {
    return Array.from(this.#headers.keys());
  }
  hasHeader(name: string) {
    return this.#headers.has(name);
  }

  writeHead(status: number, headers: Record<string, string> = {}) {
    this.statusCode = status;
    for (const k in headers) {
      if (Object.hasOwn(headers, k)) {
        this.#headers.set(k, headers[k]);
      }
    }
    return this;
  }

  #ensureHeaders(singleChunk?: Chunk) {
    if (this.statusCode === undefined) {
      this.statusCode = 200;
      this.statusMessage = "OK";
    }
    if (
      typeof singleChunk === "string" &&
      !this.hasHeader("content-type")
    ) {
      this.setHeader("content-type", "text/plain;charset=UTF-8");
    }
  }

  respond(final: boolean, singleChunk?: Chunk) {
    this.headersSent = true;
    this.#ensureHeaders(singleChunk);
    let body = singleChunk ?? (final ? null : this.#readable);
    if (ServerResponse.#bodyShouldBeNull(this.statusCode!)) {
      body = null;
    }
    this.#resolve(
      new Response(body, {
        headers: this.#headers,
        status: this.statusCode,
        statusText: this.statusMessage,
      }),
    );
  }

  // deno-lint-ignore no-explicit-any
  override end(chunk?: any, encoding?: any, cb?: any): this {
    this.finished = true;
    if (!chunk && this.#headers.has("transfer-encoding")) {
      // FIXME(bnoordhuis) Node sends a zero length chunked body instead, i.e.,
      // the trailing "0\r\n", but respondWith() just hangs when I try that.
      this.#headers.set("content-length", "0");
      this.#headers.delete("transfer-encoding");
    }

    // @ts-expect-error The signature for cb is stricter than the one implemented here
    return super.end(chunk, encoding, cb);
  }

  // Undocumented API used by `npm:compression`.
  _implicitHeader() {
    this.writeHead(this.statusCode);
  }
}

// TODO(@AaronO): optimize
export class IncomingMessageForServer extends NodeReadable {
  #req: Request;
  #headers: Record<string, string>;
  url: string;
  method: string;
  // Polyfills part of net.Socket object.
  // These properties are used by `npm:forwarded` for example.
  socket: { remoteAddress: string; remotePort: number };

  constructor(req: Request, socket: FakeSocket) {
    // Check if no body (GET/HEAD/OPTIONS/...)
    const reader = req.body?.getReader();
    super({
      autoDestroy: true,
      emitClose: true,
      objectMode: false,
      read: async function (_size) {
        if (!reader) {
          return this.push(null);
        }

        try {
          const { value } = await reader!.read();
          this.push(value !== undefined ? Buffer.from(value) : null);
        } catch (err) {
          this.destroy(err as Error);
        }
      },
      destroy: (err, cb) => {
        reader?.cancel().finally(() => cb(err));
      },
    });
    // TODO(@bartlomieju): consider more robust path extraction, e.g:
    // url: (new URL(request.url).pathname),
    this.url = req.url?.slice(req.url.indexOf("/", 8));
    this.method = req.method;
    this.socket = socket;
    this.#req = req;
  }

  get aborted() {
    return false;
  }

  get httpVersion() {
    return "1.1";
  }

  get headers() {
    if (!this.#headers) {
      this.#headers = {};
      const entries = headersEntries(this.#req.headers);
      for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        this.#headers[entry[0]] = entry[1];
      }
    }
    return this.#headers;
  }

  get upgrade(): boolean {
    return Boolean(
      this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") &&
        this.#req.headers.get("upgrade"),
    );
  }

  // connection is deprecated, but still tested in unit test.
  get connection() {
    return this.socket;
  }
}

export type ServerHandler = (
  req: IncomingMessageForServer,
  res: ServerResponse,
) => void;

export function Server(opts, requestListener?: ServerHandler): ServerImpl {
  return new ServerImpl(opts, requestListener);
}

export class ServerImpl extends EventEmitter {
  #httpConnections: Set<Deno.HttpConn> = new Set();
  #listener?: Deno.Listener;

  #addr: Deno.NetAddr;
  #hasClosed = false;
  #server: Deno.Server;
  #unref = false;
  #ac?: AbortController;
  #servePromise: Deferred<void>;
  listening = false;

  constructor(opts, requestListener?: ServerHandler) {
    super();

    if (typeof opts === "function") {
      requestListener = opts;
      opts = kEmptyObject;
    } else if (opts == null) {
      opts = kEmptyObject;
    } else {
      validateObject(opts, "options");
    }

    this._opts = opts;

    this.#servePromise = deferred();
    this.#servePromise.then(() => this.emit("close"));
    if (requestListener !== undefined) {
      this.on("request", requestListener);
    }
  }

  listen(...args: unknown[]): this {
    // TODO(bnoordhuis) Delegate to net.Server#listen().
    const normalized = _normalizeArgs(args);
    const options = normalized[0] as Partial<ListenOptions>;
    const cb = normalized[1];

    if (cb !== null) {
      // @ts-ignore change EventEmitter's sig to use CallableFunction
      this.once("listening", cb);
    }

    let port = 0;
    if (typeof options.port === "number" || typeof options.port === "string") {
      validatePort(options.port, "options.port");
      port = options.port | 0;
    }

    // TODO(bnoordhuis) Node prefers [::] when host is omitted,
    // we on the other hand default to 0.0.0.0.
    const hostname = options.host ?? "0.0.0.0";
    this.#addr = {
      hostname,
      port,
    } as Deno.NetAddr;
    this.listening = true;
    nextTick(() => this._serve());

    return this;
  }

  _serve() {
    const ac = new AbortController();
    const handler = (request: Request, info: Deno.ServeHandlerInfo) => {
      const socket = new FakeSocket({
        remoteAddress: info.remoteAddr.hostname,
        remotePort: info.remoteAddr.port,
        encrypted: this._encrypted,
      });
      const req = new IncomingMessageForServer(request, socket);
      if (req.upgrade && this.listenerCount("upgrade") > 0) {
        const { conn, response } = upgradeHttpRaw(request);
        const socket = new Socket({
          handle: new TCP(constants.SERVER, conn),
        });
        this.emit("upgrade", req, socket, Buffer.from([]));
        return response;
      } else {
        return new Promise<Response>((resolve): void => {
          const res = new ServerResponse(resolve, socket);
          this.emit("request", req, res);
        });
      }
    };

    if (this.#hasClosed) {
      return;
    }
    this.#ac = ac;
    try {
      this.#server = serve(
        {
          handler: handler as Deno.ServeHandler,
          ...this.#addr,
          signal: ac.signal,
          // @ts-ignore Might be any without `--unstable` flag
          onListen: ({ port }) => {
            this.#addr!.port = port;
            this.emit("listening");
          },
          ...this._additionalServeOptions?.(),
        },
      );
    } catch (e) {
      this.emit("error", e);
      return;
    }

    if (this.#unref) {
      this.#server.unref();
    }
    this.#server.finished.then(() => this.#servePromise!.resolve());
  }

  setTimeout() {
    console.error("Not implemented: Server.setTimeout()");
  }

  ref() {
    if (this.#server) {
      this.#server.ref();
    }
    this.#unref = false;
  }

  unref() {
    if (this.#server) {
      this.#server.unref();
    }
    this.#unref = true;
  }

  close(cb?: (err?: Error) => void): this {
    const listening = this.listening;
    this.listening = false;

    this.#hasClosed = true;
    if (typeof cb === "function") {
      if (listening) {
        this.once("close", cb);
      } else {
        this.once("close", function close() {
          cb(new ERR_SERVER_NOT_RUNNING());
        });
      }
    }

    if (listening && this.#ac) {
      this.#ac.abort();
      this.#ac = undefined;
    } else {
      this.#servePromise!.resolve();
    }

    this.#server = undefined;
    return this;
  }

  address() {
    return {
      port: this.#addr.port,
      address: this.#addr.hostname,
    };
  }
}

Server.prototype = ServerImpl.prototype;

export function createServer(opts, requestListener?: ServerHandler) {
  return Server(opts, requestListener);
}

/** Makes an HTTP request. */
export function request(
  url: string | URL,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
export function request(
  opts: RequestOptions,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
export function request(
  url: string | URL,
  opts: RequestOptions,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
// deno-lint-ignore no-explicit-any
export function request(...args: any[]) {
  return new ClientRequest(args[0], args[1], args[2]);
}

/** Makes a `GET` HTTP request. */
export function get(
  url: string | URL,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
export function get(
  opts: RequestOptions,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
export function get(
  url: string | URL,
  opts: RequestOptions,
  cb?: (res: IncomingMessageForClient) => void,
): ClientRequest;
// deno-lint-ignore no-explicit-any
export function get(...args: any[]) {
  const req = request(args[0], args[1], args[2]);
  req.end();
  return req;
}

export {
  Agent,
  ClientRequest,
  globalAgent,
  IncomingMessageForServer as IncomingMessage,
  METHODS,
  OutgoingMessage,
  STATUS_CODES,
};
export default {
  Agent,
  globalAgent,
  ClientRequest,
  STATUS_CODES,
  METHODS,
  createServer,
  Server,
  IncomingMessage: IncomingMessageForServer,
  IncomingMessageForClient,
  IncomingMessageForServer,
  OutgoingMessage,
  ServerResponse,
  request,
  get,
};