# Source: lib/client.js

    var request = require('request');
    var StreamFeed = require('./feed');
    var signing = require('./signing');
    var errors = require('./errors');
    var utils = require('./utils');
    var BatchOperations = require('./batch_operations');
    var Promise = require('./promise');
    var qs = require('qs');
    var url = require('url');
    var Faye = require('faye');

    /**
     * @callback requestCallback
     * @param {object} [errors]
     * @param {object} response
     * @param {object} body
     */

    var StreamClient = function() {
      /**
       * Client to connect to Stream api
       * @class StreamClient
       */
      this.initialize.apply(this, arguments);
    };

    StreamClient.prototype = {
      baseUrl: 'https://api.getstream.io/api/',
      baseAnalyticsUrl: 'https://analytics.getstream.io/analytics/',

      initialize: function(apiKey, apiSecret, appId, options) {
        /**
         * Initialize a client
         * @method intialize
         * @memberof StreamClient.prototype
         * @param {string} apiKey - the api key
         * @param {string} [apiSecret] - the api secret
         * @param {string} [appId] - id of the app
         * @param {object} [options] - additional options
         * @param {string} [options.location] - which data center to use
         * @param {boolean} [options.expireTokens=false] - whether to use a JWT timestamp field (i.e. iat)
         * @example <caption>initialize is not directly called by via stream.connect, ie:</caption>
         * stream.connect(apiKey, apiSecret)
         * @example <caption>secret is optional and only used in server side mode</caption>
         * stream.connect(apiKey, null, appId);
         */
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.appId = appId;
        this.options = options || {};
        this.version = this.options.version || 'v1.0';
        this.fayeUrl = this.options.fayeUrl || 'https://faye.getstream.io/faye';
        this.fayeClient = null;
        // track a source name for the api calls, ie get started or databrowser
        this.group = this.options.group || 'unspecified';
        // track subscriptions made on feeds created by this client
        this.subscriptions = {};
        this.expireTokens = this.options.expireTokens ? this.options.expireTokens : false;
        // which data center to use
        this.location = this.options.location;
        if (this.location) {
          this.baseUrl = 'https://' + this.location + '-api.getstream.io/api/';
        }

        if (typeof (process) !== 'undefined' && process.env.LOCAL) {
          this.baseUrl = 'http://localhost:8000/api/';
        }

        if (typeof (process) !== 'undefined' && process.env.LOCAL_FAYE) {
          this.fayeUrl = 'http://localhost:9999/faye/';
        }

        this.handlers = {};
        this.browser = typeof (window) !== 'undefined';
        this.node = !this.browser;

        if (this.browser && this.apiSecret) {
          throw new errors.FeedError('You are publicly sharing your private key. Dont use the private key while in the browser.');
        }
      },

      on: function(event, callback) {
        /**
         * Support for global event callbacks
         * This is useful for generic error and loading handling
         * @method on
         * @memberof StreamClient.prototype
         * @param {string} event - Name of the event
         * @param {function} callback - Function that is called when the event fires
         * @example
         * client.on('request', callback);
         * client.on('response', callback);
         */
        this.handlers[event] = callback;
      },

      off: function(key) {
        /**
         * Remove one or more event handlers
         * @method off
         * @memberof StreamClient.prototype
         * @param {string} [key] - Name of the handler
         * @example
         * client.off() removes all handlers
         * client.off(name) removes the specified handler
         */
        if (key === undefined) {
          this.handlers = {};
        } else {
          delete this.handlers[key];
        }
      },

      send: function() {
        /**
         * Call the given handler with the arguments
         * @method send
         * @memberof StreamClient.prototype
         * @access private
         */
        var args = Array.prototype.slice.call(arguments);
        var key = args[0];
        args = args.slice(1);
        if (this.handlers[key]) {
          this.handlers[key].apply(this, args);
        }
      },

      wrapPromiseTask: function(cb, fulfill, reject) {
        /**
         * Wrap a task to be used as a promise
         * @method wrapPromiseTask
         * @memberof StreamClient.prototype
         * @private
         * @param {requestCallback} cb
         * @param {function} fulfill
         * @param {function} reject
         * @return {function}
         */
        var client = this;

        var callback = this.wrapCallback(cb);
        return function task(error, response, body) {
          if (error) {
            reject({
              error: error,
              response: response,
            });
          } else if (!/^2/.test('' + response.statusCode)) {
            reject({
              error: body,
              response: response,
            });
          } else {
            fulfill(body);
          }

          callback.apply(client, arguments);
        };
      },

      wrapCallback: function(cb) {
        /**
         * Wrap callback for HTTP request
         * @method wrapCallBack
         * @memberof StreamClient.prototype
         * @access private
         */
        var client = this;

        function callback() {
          // first hit the global callback, subsequently forward
          var args = Array.prototype.slice.call(arguments);
          var sendArgs = ['response'].concat(args);
          client.send.apply(client, sendArgs);
          if (cb !== undefined) {
            cb.apply(client, args);
          }
        }

        return callback;
      },

      userAgent: function() {
        /**
         * Get the current user agent
         * @method userAgent
         * @memberof StreamClient.prototype
         * @return {string} current user agent
         */
        var description = (this.node) ? 'node' : 'browser';
        // TODO: get the version here in a way which works in both and browserify
        var version = 'unknown';
        return 'stream-javascript-client-' + description + '-' + version;
      },

      getReadOnlyToken: function(feedSlug, userId) {
        /**
         * Returns a token that allows only read operations
         *
         * @method getReadOnlyToken
         * @memberof StreamClient.prototype
         * @param {string} feedSlug - The feed slug to get a read only token for
         * @param {string} userId - The user identifier
         * @return {string} token
         * @example
         * client.getReadOnlyToken('user', '1');
         */
        var feedId = '' + feedSlug + userId;
        return signing.JWTScopeToken(this.apiSecret, '*', 'read', { feedId: feedId, expireTokens: this.expireTokens });
      },

      getReadWriteToken: function(feedSlug, userId) {
        /**
         * Returns a token that allows read and write operations
         *
         * @method getReadWriteToken
         * @memberof StreamClient.prototype
         * @param {string} feedSlug - The feed slug to get a read only token for
         * @param {string} userId - The user identifier
         * @return {string} token
         * @example
         * client.getReadWriteToken('user', '1');
         */
        var feedId = '' + feedSlug + userId;
        return signing.JWTScopeToken(this.apiSecret, '*', '*', { feedId: feedId, expireTokens: this.expireTokens });
      },

      feed: function(feedSlug, userId, token, siteId, options) {
        /**
         * Returns a feed object for the given feed id and token
         * @method feed
         * @memberof StreamClient.prototype
         * @param {string} feedSlug - The feed slug
         * @param {string} userId - The user identifier
         * @param {string} [token] - The token
         * @param {string} [siteId] - The site identifier
         * @param {object} [options] - Additional function options
         * @param {boolean} [options.readOnly] - A boolean indicating whether to generate a read only token for this feed
         * @return {StreamFeed}
         * @example
         * client.feed('user', '1', 'token2');
         */

        options = options || {};

        if (!feedSlug || !userId) {
          throw new errors.FeedError('Please provide a feed slug and user id, ie client.feed("user", "1")');
        }

        if (feedSlug.indexOf(':') !== -1) {
          throw new errors.FeedError('Please initialize the feed using client.feed("user", "1") not client.feed("user:1")');
        }

        utils.validateFeedSlug(feedSlug);
        utils.validateUserId(userId);

        // raise an error if there is no token
        if (!this.apiSecret && !token) {
          throw new errors.FeedError('Missing token, in client side mode please provide a feed secret');
        }

        // create the token in server side mode
        if (this.apiSecret && !token) {
          var feedId = '' + feedSlug + userId;
          // use scoped token if read-only access is necessary
          token = options.readOnly ? this.getReadOnlyToken(feedSlug, userId) : signing.sign(this.apiSecret, feedId);
        }

        var feed = new StreamFeed(this, feedSlug, userId, token, siteId);
        return feed;
      },

      enrichUrl: function(relativeUrl) {
        /**
         * Combines the base url with version and the relative url
         * @method enrichUrl
         * @memberof StreamClient.prototype
         * @private
         * @param {string} relativeUrl
         */
        var url = this.baseUrl + this.version + '/' + relativeUrl;
        return url;
      },

      enrichKwargs: function(kwargs) {
        /**
         * Adds the API key and the signature
         * @method enrichKwargs
         * @memberof StreamClient.prototype
         * @param {object} kwargs
         * @private
         */
        kwargs.url = this.enrichUrl(kwargs.url);
        if (kwargs.qs === undefined) {
          kwargs.qs = {};
        }

        kwargs.qs['api_key'] = this.apiKey;
        kwargs.qs.location = this.group;
        kwargs.json = true;
        var signature = kwargs.signature || this.signature;
        kwargs.headers = {};

        // auto-detect authentication type and set HTTP headers accordingly
        if (signing.isJWTSignature(signature)) {
          kwargs.headers['stream-auth-type'] = 'jwt';
          signature = signature.split(' ').reverse()[0];
        } else {
          kwargs.headers['stream-auth-type'] = 'simple';
        }

        kwargs.headers.Authorization = signature;
        kwargs.headers['X-Stream-Client'] = this.userAgent();
        return kwargs;
      },

      signActivity: function(activity) {
        /**
         * We automatically sign the to parameter when in server side mode
         * @method signActivities
         * @memberof StreamClient.prototype
         * @private
         * @param  {object}       [activity] Activity to sign
         */
        return this.signActivities([activity])[0];
      },

      signActivities: function(activities) {
        /**
         * We automatically sign the to parameter when in server side mode
         * @method signActivities
         * @memberof StreamClient.prototype
         * @private
         * @param {array} Activities
         */
        if (!this.apiSecret) {
          return activities;
        }

        for (var i = 0; i < activities.length; i++) {
          var activity = activities[i];
          var to = activity.to || [];
          var signedTo = [];
          for (var j = 0; j < to.length; j++) {
            var feedId = to[j];
            var feedSlug = feedId.split(':')[0];
            var userId = feedId.split(':')[1];
            var token = this.feed(feedSlug, userId).token;
            var signedFeed = feedId + ' ' + token;
            signedTo.push(signedFeed);
          }

          activity.to = signedTo;
        }

        return activities;
      },

      getFayeAuthorization: function() {
        /**
         * Get the authorization middleware to use Faye with getstream.io
         * @method getFayeAuthorization
         * @memberof StreamClient.prototype
         * @private
         * @return {object} Faye authorization middleware
         */
        var apiKey = this.apiKey,
            self = this;

        return {
          incoming: function(message, callback) {
            callback(message);
          },

          outgoing: function(message, callback) {
            if (message.subscription && self.subscriptions[message.subscription]) {
              var subscription = self.subscriptions[message.subscription];

              message.ext = {
                'user_id': subscription.userId,
                'api_key': apiKey,
                'signature': subscription.token,
              };
            }

            callback(message);
          },
        };
      },

      getFayeClient: function() {
        /**
         * Returns this client's current Faye client
         * @method getFayeClient
         * @memberof StreamClient.prototype
         * @private
         * @return {object} Faye client
         */
        if (this.fayeClient === null) {
          this.fayeClient = new Faye.Client(this.fayeUrl);
          var authExtension = this.getFayeAuthorization();
          this.fayeClient.addExtension(authExtension);
        }

        return this.fayeClient;
      },

      get: function(kwargs, cb) {
        /**
         * Shorthand function for get request
         * @method get
         * @memberof StreamClient.prototype
         * @private
         * @param  {object}   kwargs
         * @param  {requestCallback} cb     Callback to call on completion
         * @return {Promise}                Promise object
         */
        return new Promise(function(fulfill, reject) {
          this.send('request', 'get', kwargs, cb);
          kwargs = this.enrichKwargs(kwargs);
          kwargs.method = 'GET';
          var callback = this.wrapPromiseTask(cb, fulfill, reject);
          request(kwargs, callback);
        }.bind(this));
      },

      post: function(kwargs, cb) {
        /**
         * Shorthand function for post request
         * @method post
         * @memberof StreamClient.prototype
         * @private
         * @param  {object}   kwargs
         * @param  {requestCallback} cb     Callback to call on completion
         * @return {Promise}                Promise object
         */
        return new Promise(function(fulfill, reject) {
          this.send('request', 'post', kwargs, cb);
          kwargs = this.enrichKwargs(kwargs);
          kwargs.method = 'POST';
          var callback = this.wrapPromiseTask(cb, fulfill, reject);
          request(kwargs, callback);
        }.bind(this));
      },

      delete: function(kwargs, cb) {
        /**
         * Shorthand function for delete request
         * @method delete
         * @memberof StreamClient.prototype
         * @private
         * @param  {object}   kwargs
         * @param  {requestCallback} cb     Callback to call on completion
         * @return {Promise}                Promise object
         */
        return new Promise(function(fulfill, reject) {
          this.send('request', 'delete', kwargs, cb);
          kwargs = this.enrichKwargs(kwargs);
          kwargs.method = 'DELETE';
          var callback = this.wrapPromiseTask(cb, fulfill, reject);
          request(kwargs, callback);
        }.bind(this));
      },

      updateActivities: function(activities, callback) {
        /**
         * Updates all supplied activities on the getstream-io api
         * @since  3.1.0
         * @param  {array} activities list of activities to update
         * @return {Promise}
         */
        if (! (activities instanceof Array)) {
          throw new TypeError('The activities argument should be an Array');
        }

        var authToken = signing.JWTScopeToken(this.apiSecret, 'activities', '*', { feedId: '*', expireTokens: this.expireTokens });

        var data = {
          activities: activities,
        };

        return this.post({
          url: 'activities/',
          body: data,
          signature: authToken,
        }, callback);
      },

      updateActivity: function(activity) {
        /**
         * Updates one activity on the getstream-io api
         * @since  3.1.0
         * @param  {object} activity The activity to update
         * @return {Promise}
         */
         return this.updateActivities([activity]);
      },

    };

    if (qs) {
      StreamClient.prototype.createRedirectUrl = function(targetUrl, userId, events) {
        /**
         * Creates a redirect url for tracking the given events in the context of
         * an email using Stream's analytics platform. Learn more at
         * getstream.io/personalization
         * @method createRedirectUrl
         * @memberof StreamClient.prototype
         * @param  {string} targetUrl Target url
         * @param  {string} userId    User id to track
         * @param  {array} events     List of events to track
         * @return {string}           The redirect url
         */
        var uri = url.parse(targetUrl);

        if (!(uri.host || (uri.hostname && uri.port)) && !uri.isUnix) {
          throw new errors.MissingSchemaError('Invalid URI: "' + url.format(uri) + '"');
        }

        var authToken = signing.JWTScopeToken(this.apiSecret, 'redirect_and_track', '*', { userId: userId, expireTokens: this.expireTokens });
        var analyticsUrl = this.baseAnalyticsUrl + 'redirect/';
        var kwargs = {
          'auth_type': 'jwt',
          'authorization': authToken,
          'url': targetUrl,
          'api_key': this.apiKey,
          'events': JSON.stringify(events),
        };

        var qString = utils.rfc3986(qs.stringify(kwargs, null, null, {}));

        return analyticsUrl + '?' + qString;
      };
    }

    // If we are in a node environment and batchOperations is available add the methods to the prototype of StreamClient
    if (BatchOperations) {
      for (var key in BatchOperations) {
        if (BatchOperations.hasOwnProperty(key)) {
          StreamClient.prototype[key] = BatchOperations[key];
        }
      }
    }

    module.exports = StreamClient;