import { type AcceptInit, Accept } from './accept.ts';
import { type AcceptEncodingInit, AcceptEncoding } from './accept-encoding.ts';
import { type AcceptLanguageInit, AcceptLanguage } from './accept-language.ts';
import { type CacheControlInit, CacheControl } from './cache-control.ts';
import { type ContentDispositionInit, ContentDisposition } from './content-disposition.ts';
import { type ContentTypeInit, ContentType } from './content-type.ts';
import { type CookieInit, Cookie } from './cookie.ts';
import { canonicalHeaderName } from './header-names.ts';
import { type HeaderValue } from './header-value.ts';
import { type IfNoneMatchInit, IfNoneMatch } from './if-none-match.ts';
import { type SetCookieInit, SetCookie } from './set-cookie.ts';
import { isIterable, quoteEtag } from './utils.ts';

type DateInit = number | Date;

interface SuperHeadersPropertyInit {
  /**
   * The [`Accept`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) header value.
   */
  accept?: string | AcceptInit;
  /**
   * The [`Accept-Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header value.
   */
  acceptEncoding?: string | AcceptEncodingInit;
  /**
   * The [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header value.
   */
  acceptLanguage?: string | AcceptLanguageInit;
  /**
   * The [`Accept-Ranges`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) header value.
   */
  acceptRanges?: string;
  /**
   * The [`Age`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age) header value.
   */
  age?: string | number;
  /**
   * The [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value.
   */
  cacheControl?: string | CacheControlInit;
  /**
   * The [`Connection`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) header value.
   */
  connection?: string;
  /**
   * The [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header value.
   */
  contentDisposition?: string | ContentDispositionInit;
  /**
   * The [`Content-Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) header value.
   */
  contentEncoding?: string | string[];
  /**
   * The [`Content-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) header value.
   */
  contentLanguage?: string | string[];
  /**
   * The [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) header value.
   */
  contentLength?: string | number;
  /**
   * The [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header value.
   */
  contentType?: string | ContentTypeInit;
  /**
   * The [`Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie) header value.
   */
  cookie?: string | CookieInit;
  /**
   * The [`Date`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) header value.
   */
  date?: string | DateInit;
  /**
   * The [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) header value.
   */
  etag?: string;
  /**
   * The [`Expires`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires) header value.
   */
  expires?: string | DateInit;
  /**
   * The [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header value.
   */
  host?: string;
  /**
   * The [`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since) header value.
   */
  ifModifiedSince?: string | DateInit;
  /**
   * The [`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) header value.
   */
  ifNoneMatch?: string | string[] | IfNoneMatchInit;
  /**
   * The [`If-Unmodified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since) header value.
   */
  ifUnmodifiedSince?: string | DateInit;
  /**
   * The [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) header value.
   */
  lastModified?: string | DateInit;
  /**
   * The [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) header value.
   */
  location?: string;
  /**
   * The [`Referer`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) header value.
   */
  referer?: string;
  /**
   * The [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header value(s).
   */
  setCookie?: string | (string | SetCookieInit)[];
}

export type SuperHeadersInit =
  | Iterable<[string, string]>
  | (SuperHeadersPropertyInit & Record<string, string | HeaderValue>);

const CRLF = '\r\n';

const AcceptKey = 'accept';
const AcceptEncodingKey = 'accept-encoding';
const AcceptLanguageKey = 'accept-language';
const AcceptRangesKey = 'accept-ranges';
const AgeKey = 'age';
const CacheControlKey = 'cache-control';
const ConnectionKey = 'connection';
const ContentDispositionKey = 'content-disposition';
const ContentEncodingKey = 'content-encoding';
const ContentLanguageKey = 'content-language';
const ContentLengthKey = 'content-length';
const ContentTypeKey = 'content-type';
const CookieKey = 'cookie';
const DateKey = 'date';
const ETagKey = 'etag';
const ExpiresKey = 'expires';
const HostKey = 'host';
const IfModifiedSinceKey = 'if-modified-since';
const IfNoneMatchKey = 'if-none-match';
const IfUnmodifiedSinceKey = 'if-unmodified-since';
const LastModifiedKey = 'last-modified';
const LocationKey = 'location';
const RefererKey = 'referer';
const SetCookieKey = 'set-cookie';

/**
 * An enhanced JavaScript `Headers` interface with type-safe access.
 *
 * [API Reference](https://github.com/mjackson/remix-the-web/tree/main/packages/headers)
 *
 * [MDN `Headers` Base Class Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
 */
export class SuperHeaders extends Headers {
  #map: Map<string, string | HeaderValue>;
  #setCookies: (string | SetCookie)[] = [];

  constructor(init?: string | SuperHeadersInit | Headers) {
    super();

    this.#map = new Map();

    if (init) {
      if (typeof init === 'string') {
        let lines = init.split(CRLF);
        for (let line of lines) {
          let match = line.match(/^([^:]+):(.*)/);
          if (match) {
            this.append(match[1].trim(), match[2].trim());
          }
        }
      } else if (isIterable(init)) {
        for (let [name, value] of init) {
          this.append(name, value);
        }
      } else if (typeof init === 'object') {
        for (let name of Object.getOwnPropertyNames(init)) {
          let value = init[name];

          let descriptor = Object.getOwnPropertyDescriptor(SuperHeaders.prototype, name);
          if (descriptor?.set) {
            descriptor.set.call(this, value);
          } else {
            this.set(name, value.toString());
          }
        }
      }
    }
  }

  /**
   * Appends a new header value to the existing set of values for a header,
   * or adds the header if it does not already exist.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/append)
   */
  append(name: string, value: string): void {
    let key = name.toLowerCase();
    if (key === SetCookieKey) {
      this.#setCookies.push(value);
    } else {
      let existingValue = this.#map.get(key);
      this.#map.set(key, existingValue ? `${existingValue}, ${value}` : value);
    }
  }

  /**
   * Removes a header.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/delete)
   */
  delete(name: string): void {
    let key = name.toLowerCase();
    if (key === SetCookieKey) {
      this.#setCookies = [];
    } else {
      this.#map.delete(key);
    }
  }

  /**
   * Returns a string of all the values for a header, or `null` if the header does not exist.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get)
   */
  get(name: string): string | null {
    let key = name.toLowerCase();
    if (key === SetCookieKey) {
      return this.getSetCookie().join(', ');
    } else {
      let value = this.#map.get(key);
      if (typeof value === 'string') {
        return value;
      } else if (value != null) {
        let str = value.toString();
        return str === '' ? null : str;
      } else {
        return null;
      }
    }
  }

  /**
   * Returns an array of all values associated with the `Set-Cookie` header. This is
   * useful when building headers for a HTTP response since multiple `Set-Cookie` headers
   * must be sent on separate lines.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie)
   */
  getSetCookie(): string[] {
    return this.#setCookies.map((v) => (typeof v === 'string' ? v : v.toString()));
  }

  /**
   * Returns `true` if the header is present in the list of headers.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/has)
   */
  has(name: string): boolean {
    let key = name.toLowerCase();
    return key === SetCookieKey ? this.#setCookies.length > 0 : this.get(key) != null;
  }

  /**
   * Sets a new value for the given header. If the header already exists, the new value
   * will replace the existing value.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/set)
   */
  set(name: string, value: string): void {
    let key = name.toLowerCase();
    if (key === SetCookieKey) {
      this.#setCookies = [value];
    } else {
      this.#map.set(key, value);
    }
  }

  /**
   * Returns an iterator of all header keys (lowercase).
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/keys)
   */
  *keys(): IterableIterator<string> {
    for (let [key] of this) yield key;
  }

  /**
   * Returns an iterator of all header values.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/values)
   */
  *values(): IterableIterator<string> {
    for (let [, value] of this) yield value;
  }

  /**
   * Returns an iterator of all header key/value pairs.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries)
   */
  *entries(): IterableIterator<[string, string]> {
    for (let [key] of this.#map) {
      let str = this.get(key);
      if (str) yield [key, str];
    }

    for (let value of this.getSetCookie()) {
      yield [SetCookieKey, value];
    }
  }

  [Symbol.iterator](): IterableIterator<[string, string]> {
    return this.entries();
  }

  /**
   * Invokes the `callback` for each header key/value pair.
   *
   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/forEach)
   */
  forEach(
    callback: (value: string, key: string, headers: SuperHeaders) => void,
    thisArg?: any,
  ): void {
    for (let [key, value] of this) {
      callback.call(thisArg, value, key, this);
    }
  }

  /**
   * Returns a string representation of the headers suitable for use in a HTTP message.
   */
  toString(): string {
    let lines: string[] = [];

    for (let [key, value] of this) {
      lines.push(`${canonicalHeaderName(key)}: ${value}`);
    }

    return lines.join(CRLF);
  }

  // Header-specific getters and setters

  /**
   * The `Accept` header is used by clients to indicate the media types that are acceptable
   * in the response.
   *
   * [MDN `Accept` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
   */
  get accept(): Accept {
    return this.#getHeaderValue(AcceptKey, Accept);
  }

  set accept(value: string | AcceptInit | undefined | null) {
    this.#setHeaderValue(AcceptKey, Accept, value);
  }

  /**
   * The `Accept-Encoding` header contains information about the content encodings that the client
   * is willing to accept in the response.
   *
   * [MDN `Accept-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
   */
  get acceptEncoding(): AcceptEncoding {
    return this.#getHeaderValue(AcceptEncodingKey, AcceptEncoding);
  }

  set acceptEncoding(value: string | AcceptEncodingInit | undefined | null) {
    this.#setHeaderValue(AcceptEncodingKey, AcceptEncoding, value);
  }

  /**
   * The `Accept-Language` header contains information about preferred natural language for the
   * response.
   *
   * [MDN `Accept-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5)
   */
  get acceptLanguage(): AcceptLanguage {
    return this.#getHeaderValue(AcceptLanguageKey, AcceptLanguage);
  }

  set acceptLanguage(value: string | AcceptLanguageInit | undefined | null) {
    this.#setHeaderValue(AcceptLanguageKey, AcceptLanguage, value);
  }

  /**
   * The `Accept-Ranges` header indicates the server's acceptance of range requests.
   *
   * [MDN `Accept-Ranges` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7233#section-2.3)
   */
  get acceptRanges(): string | null {
    return this.#getStringValue(AcceptRangesKey);
  }

  set acceptRanges(value: string | undefined | null) {
    this.#setStringValue(AcceptRangesKey, value);
  }

  /**
   * The `Age` header contains the time in seconds an object was in a proxy cache.
   *
   * [MDN `Age` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.1)
   */
  get age(): number | null {
    return this.#getNumberValue(AgeKey);
  }

  set age(value: string | number | undefined | null) {
    this.#setNumberValue(AgeKey, value);
  }

  /**
   * The `Cache-Control` header contains directives for caching mechanisms in both requests and responses.
   *
   * [MDN `Cache-Control` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2)
   */
  get cacheControl(): CacheControl {
    return this.#getHeaderValue(CacheControlKey, CacheControl);
  }

  set cacheControl(value: string | CacheControlInit | undefined | null) {
    this.#setHeaderValue(CacheControlKey, CacheControl, value);
  }

  /**
   * The `Connection` header controls whether the network connection stays open after the current
   * transaction finishes.
   *
   * [MDN `Connection` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1)
   */
  get connection(): string | null {
    return this.#getStringValue(ConnectionKey);
  }

  set connection(value: string | undefined | null) {
    this.#setStringValue(ConnectionKey, value);
  }

  /**
   * The `Content-Disposition` header is a response-type header that describes how the payload is displayed.
   *
   * [MDN `Content-Disposition` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
   *
   * [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266)
   */
  get contentDisposition(): ContentDisposition {
    return this.#getHeaderValue(ContentDispositionKey, ContentDisposition);
  }

  set contentDisposition(value: string | ContentDispositionInit | undefined | null) {
    this.#setHeaderValue(ContentDispositionKey, ContentDisposition, value);
  }

  /**
   * The `Content-Encoding` header specifies the encoding of the resource.
   *
   * Note: If multiple encodings have been used, this value may be a comma-separated list. However, most often this
   * header will only contain a single value.
   *
   * [MDN `Content-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding)
   *
   * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-encoding)
   */
  get contentEncoding(): string | null {
    return this.#getStringValue(ContentEncodingKey);
  }

  set contentEncoding(value: string | string[] | undefined | null) {
    this.#setStringValue(ContentEncodingKey, Array.isArray(value) ? value.join(', ') : value);
  }

  /**
   * The `Content-Language` header describes the natural language(s) of the intended audience for the response content.
   *
   * Note: If the response content is intended for multiple audiences, this value may be a comma-separated list. However,
   * most often this header will only contain a single value.
   *
   * [MDN `Content-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language)
   *
   * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-language)
   */
  get contentLanguage(): string | null {
    return this.#getStringValue(ContentLanguageKey);
  }

  set contentLanguage(value: string | string[] | undefined | null) {
    this.#setStringValue(ContentLanguageKey, Array.isArray(value) ? value.join(', ') : value);
  }

  /**
   * The `Content-Length` header indicates the size of the entity-body in bytes.
   *
   * [MDN `Content-Length` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2)
   */
  get contentLength(): number | null {
    return this.#getNumberValue(ContentLengthKey);
  }

  set contentLength(value: string | number | undefined | null) {
    this.#setNumberValue(ContentLengthKey, value);
  }

  /**
   * The `Content-Type` header indicates the media type of the resource.
   *
   * [MDN `Content-Type` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5)
   */
  get contentType(): ContentType {
    return this.#getHeaderValue(ContentTypeKey, ContentType);
  }

  set contentType(value: string | ContentTypeInit | undefined | null) {
    this.#setHeaderValue(ContentTypeKey, ContentType, value);
  }

  /**
   * The `Cookie` request header contains stored HTTP cookies previously sent by the server with
   * the `Set-Cookie` header.
   *
   * [MDN `Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.4)
   */
  get cookie(): Cookie {
    return this.#getHeaderValue(CookieKey, Cookie);
  }

  set cookie(value: string | CookieInit | undefined | null) {
    this.#setHeaderValue(CookieKey, Cookie, value);
  }

  /**
   * The `Date` header contains the date and time at which the message was sent.
   *
   * [MDN `Date` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2)
   */
  get date(): Date | null {
    return this.#getDateValue(DateKey);
  }

  set date(value: string | DateInit | undefined | null) {
    this.#setDateValue(DateKey, value);
  }

  /**
   * The `ETag` header provides a unique identifier for the current version of the resource.
   *
   * [MDN `ETag` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
   */
  get etag(): string | null {
    return this.#getStringValue(ETagKey);
  }

  set etag(value: string | undefined | null) {
    this.#setStringValue(ETagKey, typeof value === 'string' ? quoteEtag(value) : value);
  }

  /**
   * The `Expires` header contains the date/time after which the response is considered stale.
   *
   * [MDN `Expires` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3)
   */
  get expires(): Date | null {
    return this.#getDateValue(ExpiresKey);
  }

  set expires(value: string | DateInit | undefined | null) {
    this.#setDateValue(ExpiresKey, value);
  }

  /**
   * The `Host` header specifies the domain name of the server and (optionally) the TCP port number.
   *
   * [MDN `Host` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7230#section-5.4)
   */
  get host(): string | null {
    return this.#getStringValue(HostKey);
  }

  set host(value: string | undefined | null) {
    this.#setStringValue(HostKey, value);
  }

  /**
   * The `If-Modified-Since` header makes a request conditional on the last modification date of the
   * requested resource.
   *
   * [MDN `If-Modified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3)
   */
  get ifModifiedSince(): Date | null {
    return this.#getDateValue(IfModifiedSinceKey);
  }

  set ifModifiedSince(value: string | DateInit | undefined | null) {
    this.#setDateValue(IfModifiedSinceKey, value);
  }

  /**
   * The `If-None-Match` header makes a request conditional on the absence of a matching ETag.
   *
   * [MDN `If-None-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2)
   */
  get ifNoneMatch(): IfNoneMatch {
    return this.#getHeaderValue(IfNoneMatchKey, IfNoneMatch);
  }

  set ifNoneMatch(value: string | string[] | IfNoneMatchInit | undefined | null) {
    this.#setHeaderValue(IfNoneMatchKey, IfNoneMatch, value);
  }

  /**
   * The `If-Unmodified-Since` header makes a request conditional on the last modification date of the
   * requested resource.
   *
   * [MDN `If-Unmodified-Since` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4)
   */
  get ifUnmodifiedSince(): Date | null {
    return this.#getDateValue(IfUnmodifiedSinceKey);
  }

  set ifUnmodifiedSince(value: string | DateInit | undefined | null) {
    this.#setDateValue(IfUnmodifiedSinceKey, value);
  }

  /**
   * The `Last-Modified` header contains the date and time at which the resource was last modified.
   *
   * [MDN `Last-Modified` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2)
   */
  get lastModified(): Date | null {
    return this.#getDateValue(LastModifiedKey);
  }

  set lastModified(value: string | DateInit | undefined | null) {
    this.#setDateValue(LastModifiedKey, value);
  }

  /**
   * The `Location` header indicates the URL to redirect to.
   *
   * [MDN `Location` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2)
   */
  get location(): string | null {
    return this.#getStringValue(LocationKey);
  }

  set location(value: string | undefined | null) {
    this.#setStringValue(LocationKey, value);
  }

  /**
   * The `Referer` header contains the address of the previous web page from which a link to the
   * currently requested page was followed.
   *
   * [MDN `Referer` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.2)
   */
  get referer(): string | null {
    return this.#getStringValue(RefererKey);
  }

  set referer(value: string | undefined | null) {
    this.#setStringValue(RefererKey, value);
  }

  /**
   * The `Set-Cookie` header is used to send cookies from the server to the user agent.
   *
   * [MDN `Set-Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)
   *
   * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1)
   */
  get setCookie(): SetCookie[] {
    let setCookies = this.#setCookies;

    for (let i = 0; i < setCookies.length; ++i) {
      if (typeof setCookies[i] === 'string') {
        setCookies[i] = new SetCookie(setCookies[i]);
      }
    }

    return setCookies as SetCookie[];
  }

  set setCookie(value: (string | SetCookieInit)[] | string | SetCookieInit | undefined | null) {
    if (value != null) {
      this.#setCookies = (Array.isArray(value) ? value : [value]).map((v) =>
        typeof v === 'string' ? v : new SetCookie(v),
      );
    } else {
      this.#setCookies = [];
    }
  }

  // Helpers

  #getHeaderValue<T extends HeaderValue>(key: string, ctor: new (init?: any) => T): T {
    let value = this.#map.get(key);

    if (value !== undefined) {
      if (typeof value === 'string') {
        let obj = new ctor(value);
        this.#map.set(key, obj); // cache the new object
        return obj;
      } else {
        return value as T;
      }
    }

    let obj = new ctor();
    this.#map.set(key, obj); // cache the new object
    return obj;
  }

  #setHeaderValue(key: string, ctor: new (init?: string) => HeaderValue, value: any): void {
    if (value != null) {
      this.#map.set(key, typeof value === 'string' ? value : new ctor(value));
    } else {
      this.#map.delete(key);
    }
  }

  #getDateValue(key: string): Date | null {
    let value = this.#map.get(key);
    return value === undefined ? null : new Date(value as string);
  }

  #setDateValue(key: string, value: string | DateInit | undefined | null): void {
    if (value != null) {
      this.#map.set(
        key,
        typeof value === 'string'
          ? value
          : (typeof value === 'number' ? new Date(value) : value).toUTCString(),
      );
    } else {
      this.#map.delete(key);
    }
  }

  #getNumberValue(key: string): number | null {
    let value = this.#map.get(key);
    return value === undefined ? null : parseInt(value as string, 10);
  }

  #setNumberValue(key: string, value: string | number | undefined | null): void {
    if (value != null) {
      this.#map.set(key, typeof value === 'string' ? value : value.toString());
    } else {
      this.#map.delete(key);
    }
  }

  #getStringValue(key: string): string | null {
    let value = this.#map.get(key);
    return value === undefined ? null : (value as string);
  }

  #setStringValue(key: string, value: string | undefined | null): void {
    if (value != null) {
      this.#map.set(key, value);
    } else {
      this.#map.delete(key);
    }
  }
}
