assign
  createBrotliDecompress = zlib.createBrotliDecompress;
  return createBrotliDecompress();
}

/**
 * Create an unzip handler.
 * @returns {import('zlib').Unzip}
 */
function createUnzip() {
  zlib ??= require('zlib'); // [1]
  // eslint-disable-next-line no-func-assign
  createUnzip = zlib.createUnzip;
  return createUnzip();
}

/**
 * Redirection status code as per section 6.4 of RFC 7231:
 * https://datatracker.ietf.org/doc/html/rfc7231#section-6.4
 * and RFC 7238:
 * https://datatracker.ietf.org/doc/html/rfc7238
 * @param {number} statusCode
 * @returns {boolean}
 */
function isRedirect(statusCode) {
  switch (statusCode) {
    case 300: // Multiple Choices
    case 301: // Moved Permanently
    case 302: // Found
    case 303: // See Other
    case 307: // Temporary Redirect
    case 308: // Permanent Redirect
      return true;
    default:
      return false;
  }
}

/**
 * @param {URL} parsed
 * @returns {Promise<CacheEntry> | CacheEntry}
 */
function fetchWithRedirects(parsed) {
  const existing = cacheForGET.get(parsed.href);
  if (existing) {
    return existing;
  }
  const handler = parsed.protocol === 'http:' ? HTTPGet : HTTPSGet;
  const result = (async () => {
    const req = handler(parsed, {
      headers: { Accept: '*/*' },
    });
    // Note that `once` is used here to handle `error` and that it hits the
    // `finally` on network error/timeout.
    const { 0: res } = await once(req, 'response');
    try {
      const hasLocation = ObjectPrototypeHasOwnProperty(res.headers, 'location');
      if (isRedirect(res.statusCode) && hasLocation) {
        const location = new URL(res.headers.location, parsed);
        if (location.protocol !== 'http:' && location.protocol !== 'https:') {
          throw new ERR_NETWORK_IMPORT_DISALLOWED(
            res.headers.location,
            parsed.href,
            'cannot redirect to non-network location',
          );
        }
        const entry = await fetchWithRedirects(location);
        cacheForGET.set(parsed.href, entry);
        return entry;
      }
      if (res.statusCode === 404) {
        const err = new ERR_MODULE_NOT_FOUND(parsed.href, null, parsed);
        err.message = `Cannot find module '${parsed.href}', HTTP 404`;
        throw err;
      }
      // This condition catches all unsupported status codes, including
      // 3xx redirection codes without `Location` HTTP header.
      if (res.statusCode < 200 || res.statusCode >= 300) {
        throw new ERR_NETWORK_IMPORT_DISALLOWED(
          res.headers.location,
          parsed.href,
          'cannot redirect to non-network location');
      }
      const { headers } = res;
      const contentType = headers['content-type'];
      if (!contentType) {
        throw new ERR_NETWORK_IMPORT_BAD_RESPONSE(
          parsed.href,
          "the 'Content-Type' header is required",
        );
      }
      /**
       * @type {CacheEntry}
       */
      const entry = {
        resolvedHREF: parsed.href,
        headers: {
          'content-type': res.headers['content-type'],
        },
        body: (async () => {
          let bodyStream = res;
          if (res.headers['content-encoding'] === 'br') {
            bodyStream = compose(res, createBrotliDecompress());
          } else if (
            res.headers['content-encoding'] === 'gzip' ||
            res.headers['content-encoding'] === 'deflate'
          ) {
            bodyStream = compose(res, createUnzip());
          }
          const buffers = await bodyStream.toArray();
          const body = BufferConcat(buffers);
          entry.body = body;
          return body;
        })(),
      };
      cacheForGET.set(parsed.href, entry);
      await entry.body;
      return entry;
    } finally {
      req.destroy();
    }
  })();
  cacheForGET.set(parsed.href, result);
  return result;
}

const allowList = new net.BlockList();
allowList.addAddress('::1', 'ipv6');
allowList.addRange('127.0.0.1', '127.255.255.255');

/**
 * Returns if an address has local status by if it is going to a local
 * interface or is an address resolved by DNS to be a local interface
 * @param {string} hostname url.hostname to test
 * @returns {Promise<boolean>}
 */
async function isLocalAddress(hostname) {
  try {
    if (
      StringPrototypeStartsWith(hostname, '[') &&
      StringPrototypeEndsWith(hostname, ']')
    ) {
      hostname = StringPrototypeSlice(hostname, 1, -1);
    }
    const addr = await dnsLookup(hostname, { verbatim: true });
    const ipv = addr.family === 4 ? 'ipv4' : 'ipv6';
    return allowList.check(addr.address, ipv);
  } catch {
    // If it errored, the answer is no.
  }
  return false;
}

/**
 * Fetches a location with a shared cache following redirects.
 * Does not respect HTTP cache headers.
 *
 * This splits the header and body Promises so that things only needing
 * headers don't need to wait on the body.
 *
 * In cases where the request & response have already settled, this returns the
 * cache value synchronously.
 * @param {URL} parsed
 * @param {ESModuleContext} context
 * @returns {ReturnType<typeof fetchWithRedirects>}
 */
function fetchModule(parsed, { parentURL }) {
  const { href } = parsed;
  const existing = cacheForGET.get(href);
  if (existing) {
    return existing;
  }
  if (parsed.protocol === 'http:') {
    return PromisePrototypeThen(isLocalAddress(parsed.hostname), (is) => {
      if (is !== true) {
        throw new ERR_NETWORK_IMPORT_DISALLOWED(
          href,
          parentURL,
          'http can only be used to load local resources (use https instead).',
        );
      }
      return fetchWithRedirects(parsed);
    });
  }
  return fetchWithRedirects(parsed);
}

module.exports = {
  fetchModule,
};
