text: 'use strict';
  
  const util = require('util');
  const _ = require('underscore');
  
  const extend = _.extend;
  const memoize = _.memoize;
  
  const perf = require('abacus-perf');
  const batch = require('abacus-batch');
  const retry = require('abacus-retry');
  const moment = require('abacus-moment');
  const urienv = require('abacus-urienv');
  const breaker = require('abacus-breaker');
  const request = require('abacus-request');
  const throttle = require('abacus-throttle');
  
  const debug = require('abacus-debug')('abacus-client');
  const xdebug = require('abacus-debug')('x-abacus-client');
  const edebug = require('abacus-debug')('e-abacus-client');
  
  // Resolve service URIs
  const uris = memoize(() =>
    urienv({
      collector: 9080
    })
  );
  
  // if a batch is throttled, then throttle limits the number of calls made to
  // the batch function limiting the number of batches. In order to avoid that
  // all the batch functions when throttled should have a throttle value that is
  // multiplied by the batch.
  const reliableRequest = throttle(retry(breaker(batch(request))),
    batch.defaults().maxSize * throttle.defaults().maxCalls);
  
  const secured = process.env.SECURED === 'true';
  
  const authHeader = (token) => token ? { authorization: token() } : {};
  
  // Report usage utility. Use like this:
  //
  // const errors = {
  //   missingToken: false,
  //   noReportEverHappened: true,
  //   consecutiveReportFailures: 0,
  //   lastError: '',
  //   lastErrorTimestamp: ''
  // };
  // const errorFn = (message, error, response, name, time) => { ... };
  //
  // const reporter = require('abacus-client')(errors);
  // reporter.reportUsage(usage, token, (error, response) => {
  //   ... <process error> ...
  //   ... <use the response> ...
  // });
  const reporter = (errors, errorFn) => {
    const registerError = (message, error, response, name, time) => {
      const errorMessage =
        error || response ? util.format('%s; error: %s; response: %j', message, error, response) : message;
  
      errors.lastError = errorMessage;
      errors.lastErrorTimestamp = moment.utc().toISOString();
  
      edebug(errorMessage);
  
      if (name && time) perf.report(name, time, undefined, new Error(errorMessage));
    };
  
    const errorCb = errorFn ? errorFn : registerError;
  
    const processMissingToken = (cb) => {
      errors.missingToken = true;
  
      const message = 'Missing resource token';
      debug(message);
      errorCb(message);
  
      const err = new Error(message);
      xdebug('Calling back with error: %o and no response', err);
      cb(err, undefined);
    };
  
    const processSuccessfulReport = (response, usage, t0, cb) => {
      if (!response.headers || !response.headers.location) {
        const message = util.format('No Location header found in ' + 'response %j for usage %j', response, usage);
        edebug(message);
        cb(new Error(message));
        return;
      }
  
      debug('Successfully reported usage %j with headers %j', usage, response.headers);
      errors.noReportEverHappened = false;
      errors.consecutiveReportFailures = 0;
      perf.report('report', t0);
  
      xdebug('Calling back without error and response: %o', response);
      cb(undefined, response);
    };
  
    const processFailedReport = (response, usage, t0, cb) => {
      debug('Failed reporting usage: %j. Response: %j', usage, response);
      switch (response.statusCode) {
        case 409:
        case 451:
          errors.noReportEverHappened = false;
          errors.consecutiveReportFailures = 0;
          perf.report('report', t0);
          cb(undefined, response);
          return;
      }
      errors.consecutiveReportFailures++;
      const message = util.format('Failed reporting usage: %j. Response: %j, Consecutive failures: %d',
        usage, response, errors.consecutiveReportFailures);
      const body = response.body;
      const err = body && body.error ? extend(new Error(), body) : new Error(message);
      errorCb(message, err, undefined, 'report', t0);
      xdebug('Calling back with error: %o and response: %o', err, response);
      cb(err, response);
    };
  
    const processReportError = (error, response, t0, cb) => {
      errors.consecutiveReportFailures++;
  
      const message = util.format(
        'Failed reporting usage. ' + 'Consecutive failures: %d',
        errors.consecutiveReportFailures
      );
      errorCb(message, error, response, 'report', t0);
  
      xdebug('Calling back with error: %o and response: %o', error, response);
      cb(error, response);
    };
  
    const reportUsage = (usage, token, cb = () => {}) => {
      const t0 = moment.now();
      xdebug('Reporting usage %o', usage);
  
      if (secured && !token()) {
        processMissingToken(cb);
        return;
      }
      errors.missingToken = false;
  
      reliableRequest.post(
        ':collector/v1/metering/collected/usage',
        {
          collector: uris().collector,
          headers: authHeader(token),
          body: usage
        },
        (error, response) => {
          if (error || !response) {
            processReportError(error, response, t0, cb);
            return;
          }
  
          const bodyError = response.body && response.body.error ? response.body.error : undefined;
          if (response.statusCode === 202 && !bodyError)
            processSuccessfulReport(response, usage, t0, cb);
          else
            processFailedReport(response, usage, t0, cb);
        }
      );
    };
  
    return {
      reportUsage: reportUsage,
      registerError: errorCb
    };
  };
  
  module.exports = reporter;
  