text: // @flow
  
  import http from 'http';
  import https from 'https';
  import parseBoolean from 'boolean';
  import semver from 'semver';
  import Logger from '../Logger';
  import {
    HttpProxyAgent,
    HttpsProxyAgent,
  } from '../classes';
  import {
    UnexpectedStateError,
  } from '../errors';
  import {
    bindHttpMethod,
    isUrlMatchingNoProxy,
    parseProxyUrl,
  } from '../utilities';
  import type {
    ProxyAgentConfigurationInputType,
    ProxyAgentConfigurationType,
  } from '../types';
  import createProxyController from './createProxyController';
  
  const httpGet = http.get;
  const httpRequest = http.request;
  const httpsGet = https.get;
  const httpsRequest = https.request;
  
  const log = Logger.child({
    namespace: 'createGlobalProxyAgent',
  });
  
  const defaultConfigurationInput = {
    environmentVariableNamespace: undefined,
    forceGlobalAgent: undefined,
    socketConnectionTimeout: 60000,
  };
  
  const omitUndefined = (subject) => {
    const keys = Object.keys(subject);
  
    const result = {};
  
    for (const key of keys) {
      const value = subject[key];
  
      if (value !== undefined) {
        result[key] = value;
      }
    }
  
    return result;
  };
  
  const createConfiguration = (configurationInput: ProxyAgentConfigurationInputType): ProxyAgentConfigurationType => {
    // eslint-disable-next-line no-process-env
    const environment = process.env;
  
    const defaultConfiguration = {
      environmentVariableNamespace: typeof environment.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE === 'string' ? environment.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE : 'GLOBAL_AGENT_',
      forceGlobalAgent: typeof environment.GLOBAL_AGENT_FORCE_GLOBAL_AGENT === 'string' ? parseBoolean(environment.GLOBAL_AGENT_FORCE_GLOBAL_AGENT) : true,
      socketConnectionTimeout: typeof environment.GLOBAL_AGENT_SOCKET_CONNECTION_TIMEOUT === 'string' ? parseInt(environment.GLOBAL_AGENT_SOCKET_CONNECTION_TIMEOUT, 10) : defaultConfigurationInput.socketConnectionTimeout,
    };
  
    // $FlowFixMe
    return {
      ...defaultConfiguration,
      ...omitUndefined(configurationInput),
    };
  };
  
  export default (configurationInput: ProxyAgentConfigurationInputType = defaultConfigurationInput) => {
    const configuration = createConfiguration(configurationInput);
  
    const proxyController = createProxyController();
  
    // eslint-disable-next-line no-process-env
    proxyController.HTTP_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTP_PROXY'] || null;
  
    // eslint-disable-next-line no-process-env
    proxyController.HTTPS_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTPS_PROXY'] || null;
  
    // eslint-disable-next-line no-process-env
    proxyController.NO_PROXY = process.env[configuration.environmentVariableNamespace + 'NO_PROXY'] || null;
  
    log.info({
      configuration,
      state: proxyController,
    }, 'global agent has been initialized');
  
    const mustUrlUseProxy = (getProxy) => {
      return (url) => {
        if (!getProxy()) {
          return false;
        }
  
        if (!proxyController.NO_PROXY) {
          return true;
        }
  
        return !isUrlMatchingNoProxy(url, proxyController.NO_PROXY);
      };
    };
  
    const getUrlProxy = (getProxy) => {
      return () => {
        const proxy = getProxy();
  
        if (!proxy) {
          throw new UnexpectedStateError('HTTP(S) proxy must be configured.');
        }
  
        return parseProxyUrl(proxy);
      };
    };
  
    const getHttpProxy = () => {
      return proxyController.HTTP_PROXY;
    };
  
    const BoundHttpProxyAgent = class extends HttpProxyAgent {
      constructor () {
        super(
          () => {
            return getHttpProxy();
          },
          mustUrlUseProxy(getHttpProxy),
          getUrlProxy(getHttpProxy),
          http.globalAgent,
          configuration.socketConnectionTimeout,
        );
      }
    };
  
    const httpAgent = new BoundHttpProxyAgent();
  
    const getHttpsProxy = () => {
      return proxyController.HTTPS_PROXY || proxyController.HTTP_PROXY;
    };
  
    const BoundHttpsProxyAgent = class extends HttpsProxyAgent {
      constructor () {
        super(
          () => {
            return getHttpsProxy();
          },
          mustUrlUseProxy(getHttpsProxy),
          getUrlProxy(getHttpsProxy),
          https.globalAgent,
          configuration.socketConnectionTimeout,
        );
      }
    };
  
    const httpsAgent = new BoundHttpsProxyAgent();
  
    // Overriding globalAgent was added in v11.7.
    // @see https://nodejs.org/uk/blog/release/v11.7.0/
    if (semver.gte(process.version, 'v11.7.0')) {
      // @see https://github.com/facebook/flow/issues/7670
      // $FlowFixMe
      http.globalAgent = httpAgent;
  
      // $FlowFixMe
      https.globalAgent = httpsAgent;
    }
  
    // The reason this logic is used in addition to overriding http(s).globalAgent
    // is because there is no guarantee that we set http(s).globalAgent variable
    // before an instance of http(s).Agent has been already constructed by someone,
    // e.g. Stripe SDK creates instances of http(s).Agent at the top-level.
    // @see https://github.com/gajus/global-agent/pull/13
    //
    // We still want to override http(s).globalAgent when possible to enable logic
    // in `bindHttpMethod`.
    if (semver.gte(process.version, 'v10.0.0')) {
      // $FlowFixMe
      http.get = bindHttpMethod(httpGet, httpAgent, configuration.forceGlobalAgent);
  
      // $FlowFixMe
      http.request = bindHttpMethod(httpRequest, httpAgent, configuration.forceGlobalAgent);
  
      // $FlowFixMe
      https.get = bindHttpMethod(httpsGet, httpsAgent, configuration.forceGlobalAgent);
  
      // $FlowFixMe
      https.request = bindHttpMethod(httpsRequest, httpsAgent, configuration.forceGlobalAgent);
    } else {
      log.warn('attempt to initialize global-agent in unsupported Node.js version was ignored');
    }
  
    return proxyController;
  };
  