/**
 * This is a module that makes a bot
 * It expects to receive messages via the botkit.ingest function
 **/
var mustache = require('mustache');
// var simple_storage = require(__dirname + '/storage/simple_storage.js');

const logging = require('../logging');
// var ConsoleLogger = require(__dirname + '/console_logger.js');
// var LogLevels = ConsoleLogger.LogLevels;

var ware = require('ware');
var clone = require('clone');
var fs = require('fs');
// var studio = require('./Studio.js');
var os = require('os');
var async = require('async');
// var PKG_VERSION = require('../package.json').version;
// var express = require('express');
// var bodyParser = require('body-parser');

function Botkit(configuration) {
  var botkit = {
    events: {}, // this will hold event handlers
    config: {}, // this will hold the configuration
    tasks: [],
    taskCount: 0,
    convoCount: 0,
    my_version: null,
    my_user_agent: null,
    memory_store: {
      users: {},
      channels: {},
      teams: {},
    }
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  // TODO: externalize this into some sort of utterances.json file
  botkit.utterances = {
    yes: new RegExp(/^(yes|yea|yup|yep|ya|sure|ok|y|yeah|yah)/i),
    no: new RegExp(/^(no|nah|nope|n)/i),
    quit: new RegExp(/^(quit|cancel|end|stop|done|exit|nevermind|never mind)/i)
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  // define some middleware points where custom functions
  // can plug into key points of botkits process
  botkit.middleware = {
    spawn: ware(),
    ingest: ware(),
    normalize: ware(),
    categorize: ware(),
    receive: ware(),
    heard: ware(), // best place for heavy i/o because fewer messages
    triggered: ware(), // like heard, but for other events
    capture: ware(),
    format: ware(),
    send: ware(),
  };

  botkit.ingest = function (bot, payload, source) {

    // keep an unmodified copy of the message
    payload.raw_message = clone(payload);

    payload._pipeline = {
      stage: 'ingest',
    };

    botkit.middleware.ingest.run(bot, payload, source, function (err, bot, payload, source) {
      if (err) {
        console.error('An error occured in the ingest middleware: ', err);
        return;
      }
      botkit.normalize(bot, payload);
    });
  };

  botkit.normalize = function (bot, payload) {
    payload._pipeline.stage = 'normalize';
    botkit.middleware.normalize.run(bot, payload, function (err, bot, message) {
      if (err) {
        console.error('An error occured in the normalize middleware: ', err);
        return;
      }

      if (!message.type) {
        message.type = 'message_received';
      }
      botkit.categorize(bot, message);
    });
  };

  botkit.categorize = function (bot, message) {
    message._pipeline.stage = 'categorize';
    botkit.middleware.categorize.run(bot, message, function (err, bot, message) {
      if (err) {
        console.error('An error occured in the categorize middleware: ', err);
        return;
      }

      botkit.receiveMessage(bot, message);
    });
  };

  botkit.receiveMessage = function (bot, message) {
    message._pipeline.stage = 'receive';
    botkit.middleware.receive.run(bot, message, function (err, bot, message) {
      if (err) {
        console.error('An error occured in the receive middleware: ', err);
        return;
      } else {
        botkit.logger.debug('RECEIVED MESSAGE');
        bot.findConversation(message, function (convo) {
          if (convo) {
            convo.handle(message);
          } else {
            botkit.trigger(message.type, [bot, message]);
          }
        });
      }
    });
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  function Conversation(task, message) {

    this.messages = [];
    this.sent = [];
    this.transcript = [];

    this.context = {
      user: message.user,
      channel: message.channel,
      bot: task.bot,
    };

    this.events = {};

    this.vars = {};

    this.threads = {};
    this.thread = null;

    this.status = 'new';
    this.task = task;
    this.source_message = message;
    this.handler = null;
    this.responses = {};
    this.capture_options = {};
    this.startTime = new Date();
    this.lastActive = new Date();
    /** will be pointing to a callback which will be called after timeout,
     * conversation will be not be ended and should be taken care by callback
     */
    this.timeOutHandler = null;

    this.collectResponse = function (key, value) {
      this.responses[key] = value;
    };

    this.capture = function (response, cb) {

      var that = this;
      var capture_key = this.sent[this.sent.length - 1].text;
      botkit.middleware.capture.run(that.task.bot, response, that, function (err, bot, response, convo) {
        if (response.text) {
          response.text = response.text.trim();
        } else {
          response.text = '';
        }

        if (that.capture_options.key != undefined) {
          capture_key = that.capture_options.key;
        }

        // capture the question that was asked
        // if text is an array, get 1st
        if (typeof (that.sent[that.sent.length - 1].text) == 'string') {
          response.question = that.sent[that.sent.length - 1].text;
        } else if (Array.isArray(that.sent[that.sent.length - 1].text)) {
          response.question = that.sent[that.sent.length - 1].text[0];
        } else {
          response.question = '';
        }

        if (that.capture_options.multiple) {
          if (!that.responses[capture_key]) {
            that.responses[capture_key] = [];
          }
          that.responses[capture_key].push(response);
        } else {
          that.responses[capture_key] = response;
        }

        if (cb) cb(response);
      });

    };

    this.handle = function (message) {

      var that = this;
      this.lastActive = new Date();
      this.transcript.push(message);
      botkit.logger.debug('HANDLING MESSAGE IN CONVO', message);
      // do other stuff like call custom callbacks
      if (this.handler) {
        this.capture(message, function (message) {
          // if the handler is a normal function, just execute it!
          // NOTE: anyone who passes in their own handler has to call
          // convo.next() to continue after completing whatever it is they want to do.
          if (typeof (that.handler) == 'function') {
            that.handler(message, that);
          } else {
            // handle might be a mapping of keyword to callback.
            // lets see if the message matches any of the keywords
            let match, patterns = that.handler;
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].pattern && botkit.hears_test([patterns[p].pattern], message)) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

            // none of the messages matched! What do we do?
            // if a default exists, fire it!
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].default) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

          }
        });
      } else {
        // do nothing
      }
    };

    this.setVar = function (field, value) {
      if (!this.vars) {
        this.vars = {};
      }
      this.vars[field] = value;
    };

    this.activate = function () {
      this.task.trigger('conversationStarted', [this]);
      this.task.botkit.trigger('conversationStarted', [this.task.bot, this]);
      this.status = 'active';
    };

    /**
     * active includes both ACTIVE and ENDING
     * in order to allow the timeout end scripts to play out
     **/
    this.isActive = function () {
      return (this.status == 'active' || this.status == 'ending');
    };

    this.deactivate = function () {
      this.status = 'inactive';
    };

    this.say = function (message) {
      this.addMessage(message);
    };

    this.sayFirst = function (message) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }
      this.messages.unshift(message);
    };

    this.on = function (event, cb) {
      botkit.logger.debug('Setting up a handler for', event);
      var events = event.split(/\,/g);
      for (var e in events) {
        if (!this.events[events[e]]) {
          this.events[events[e]] = [];
        }
        this.events[events[e]].push(cb);
      }
      return this;
    };

    this.trigger = function (event, data) {
      if (this.events[event]) {
        for (var e = 0; e < this.events[event].length; e++) {
          var res = this.events[event][e].apply(this, data);
          if (res === false) {
            return;
          }
        }
      } else { }
    };

    // proceed to the next message after waiting for an answer
    this.next = function () {
      this.handler = null;
    };

    this.repeat = function () {
      if (this.sent.length) {
        this.messages.push(this.sent[this.sent.length - 1]);
      } else {
        // console.log('TRIED TO REPEAT, NOTHING TO SAY');
      }
    };

    this.silentRepeat = function () {
      return;
    };

    this.addQuestion = function (message, cb, capture_options, thread) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (capture_options) {
        message.capture_options = capture_options;
      }

      message.handler = cb;
      this.addMessage(message, thread);
    };

    this.ask = function (message, cb, capture_options) {
      this.addQuestion(message, cb, capture_options, this.thread || 'default');
    };

    this.addMessage = function (message, thread) {
      if (!thread) {
        thread = this.thread;
      }
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (!this.threads[thread]) {
        this.threads[thread] = [];
      }
      this.threads[thread].push(message);

      // this is the current topic, so add it here as well
      if (this.thread == thread) {
        this.messages.push(message);
      }
    };

    // how long should the bot wait while a user answers?
    this.setTimeout = function (timeout) {
      this.task.timeLimit = timeout;
    };

    // For backwards compatibility, wrap gotoThread in its previous name
    this.changeTopic = function (topic) {
      this.gotoThread(topic);
    };

    this.hasThread = function (thread) {
      return (this.threads[thread] != undefined);
    };

    this.transitionTo = function (thread, message) {

      // add a new transition thread
      // add this new message to it
      // set that message action to execute the actual transition
      // then change threads to transition thread

      var num = 1;
      while (this.hasThread('transition_' + num)) {
        num++;
      }

      var threadname = 'transition_' + num;

      if (typeof (message) == 'string') {
        message = {
          text: message,
          action: thread
        };
      } else {
        message.action = thread;
      }

      this.addMessage(message, threadname);

      this.gotoThread(threadname);

    };

    this.beforeThread = function (thread, callback) {
      if (!this.before_hooks) {
        this.before_hooks = {};
      }

      if (!this.before_hooks[thread]) {
        this.before_hooks[thread] = [];
      }
      this.before_hooks[thread].push(callback);
    };

    this.gotoThread = function (thread) {
      var that = this;
      that.next_thread = thread;
      that.processing = true;

      var makeChange = function () {
        if (!that.hasThread(that.next_thread)) {
          if (that.next_thread == 'default') {
            that.threads[that.next_thread] = [];
          } else {
            botkit.logger.debug('WARN: gotoThread() to an invalid thread!', thread);
            that.stop('unknown_thread');
            return;
          }
        }

        that.thread = that.next_thread;
        that.messages = that.threads[that.next_thread].slice();

        that.handler = null;
        that.processing = false;
      };

      if (that.before_hooks && that.before_hooks[that.next_thread]) {

        // call any beforeThread hooks in sequence
        async.eachSeries(this.before_hooks[that.next_thread], function (before_hook, next) {
          before_hook(that, next);
        }, function (err) {
          if (!err) {
            makeChange();
          }
        });

      } else {

        makeChange();

      }

    };

    this.combineMessages = function (messages) {
      if (!messages) {
        return '';
      }

      if (Array.isArray(messages) && !messages.length) {
        return '';
      }

      if (messages.length > 1) {
        var txt = [];
        var last_user = null;
        var multi_users = false;
        last_user = messages[0].user;
        for (let x = 0; x < messages.length; x++) {
          if (messages[x].user != last_user) {
            multi_users = true;
          }
        }
        last_user = '';
        for (let x = 0; x < messages.length; x++) {
          if (multi_users && messages[x].user != last_user) {
            last_user = messages[x].user;
            if (txt.length) {
              txt.push('');
            }
            txt.push('<@' + messages[x].user + '>:');
          }
          txt.push(messages[x].text);
        }
        return txt.join('\n');
      } else {
        if (messages.length) {
          return messages[0].text;
        } else {
          return messages.text;
        }
      }
    };

    this.getResponses = function () {

      var res = {};
      for (var key in this.responses) {

        res[key] = {
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        };
      }
      return res;
    };

    this.getResponsesAsArray = function () {

      var res = [];
      for (var key in this.responses) {

        res.push({
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        });
      }
      return res;
    };

    this.extractResponses = function () {

      var res = {};
      for (var key in this.responses) {
        res[key] = this.extractResponse(key);
      }
      return res;
    };

    this.extractResponse = function (key) {
      return this.combineMessages(this.responses[key]);
    };

    this.replaceAttachmentTokens = function (attachments) {

      if (attachments && attachments.length) {
        for (let a = 0; a < attachments.length; a++) {
          for (let key in attachments[a]) {
            if (typeof (attachments[a][key]) == 'string') {
              attachments[a][key] = this.replaceTokens(attachments[a][key]);
            } else {
              attachments[a][key] = this.replaceAttachmentTokens(attachments[a][key]);
            }
          }
        }
      } else {
        for (let a in attachments) {
          if (typeof (attachments[a]) == 'string') {
            attachments[a] = this.replaceTokens(attachments[a]);
          } else {
            attachments[a] = this.replaceAttachmentTokens(attachments[a]);
          }
        }
      }

      return attachments;
    };

    this.replaceTokens = function (text) {

      var vars = {
        identity: this.task.bot.identity,
        responses: this.extractResponses(),
        origin: this.task.source_message,
        vars: this.vars,
      };

      var rendered = '';

      try {
        rendered = mustache.render(text, vars);
      } catch (err) {
        botkit.logger.error('Error in message template. Mustache failed with error: ', err);
        rendered = text;
      }

      return rendered;
    };

    this.stop = function (status) {
      this.handler = null;
      this.messages = [];
      this.status = status || 'stopped';
      botkit.logger.debug('Conversation is over with status ' + this.status);
      this.task.conversationEnded(this);
    };

    // was this conversation successful?
    // return true if it was completed
    // otherwise, return false
    // false could indicate a variety of failed states:
    // manually stopped, timed out, etc
    this.successful = function () {

      // if the conversation is still going, it can't be successful yet
      if (this.isActive()) {
        return false;
      }

      if (this.status == 'completed') {
        return true;
      } else {
        return false;
      }

    };

    this.cloneMessage = function (message) {
      // clone this object so as not to modify source
      var outbound = clone(message);

      if (typeof (message.text) == 'string') {
        outbound.text = this.replaceTokens(message.text);
      } else if (message.text) {
        outbound.text = this.replaceTokens(
          message.text[Math.floor(Math.random() * message.text.length)]
        );
      }

      if (outbound.attachments) {
        outbound.attachments = this.replaceAttachmentTokens(outbound.attachments);
      }

      if (outbound.attachment) {

        // pick one variation of the message text at random
        if (outbound.attachment.payload.text && typeof (outbound.attachment.payload.text) != 'string') {
          outbound.attachment.payload.text = this.replaceTokens(
            outbound.attachment.payload.text[
            Math.floor(Math.random() * outbound.attachment.payload.text.length)
            ]
          );
        }
        outbound.attachment = this.replaceAttachmentTokens([outbound.attachment])[0];
      }

      if (this.messages.length && !message.handler) {
        outbound.continue_typing = true;
      }

      if (typeof (message.attachments) == 'function') {
        outbound.attachments = message.attachments(this);
      }

      return outbound;
    };

    this.onTimeout = function (handler) {
      if (typeof (handler) == 'function') {
        this.timeOutHandler = handler;
      } else {
        botkit.logger.debug('Invalid timeout function passed to onTimeout');
      }
    };

    this.tick = function () {
      var now = new Date();

      if (this.isActive()) {
        if (this.processing) {
          // do nothing. The bot is waiting for async process to complete.
        } else if (this.handler) {
          // check timeout!
          // how long since task started?
          var duration = (now.getTime() - this.task.startTime.getTime());
          // how long since last active?
          var lastActive = (now.getTime() - this.lastActive.getTime());

          if (this.task.timeLimit && // has a timelimit
            (duration > this.task.timeLimit) && // timelimit is up
            (lastActive > this.task.timeLimit) // nobody has typed for 60 seconds at least
          ) {
            // if timeoutHandler is set then call it, otherwise follow the normal flow
            // this will not break others code, after the update
            if (this.timeOutHandler) {
              this.timeOutHandler(this);
            } else if (this.hasThread('on_timeout')) {
              this.status = 'ending';
              this.gotoThread('on_timeout');
            } else {
              this.stop('timeout');
            }
          }
          // otherwise do nothing
        } else {
          if (this.messages.length) {

            if (this.sent.length &&
              !this.sent[this.sent.length - 1].sent
            ) {
              return;
            }

            if (this.task.bot.botkit.config.require_delivery &&
              this.sent.length &&
              !this.sent[this.sent.length - 1].delivered
            ) {
              return;
            }

            if (typeof (this.messages[0].timestamp) == 'undefined' ||
              this.messages[0].timestamp <= now.getTime()) {
              var message = this.messages.shift();
              //console.log('HANDLING NEW MESSAGE',message);
              // make sure next message is delayed appropriately
              if (this.messages.length && this.messages[0].delay) {
                this.messages[0].timestamp = now.getTime() + this.messages[0].delay;
              }
              if (message.handler) {
                //console.log(">>>>>> SET HANDLER IN TICK");
                this.handler = message.handler;
              } else {
                this.handler = null;
                //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED");
              }
              if (message.capture_options) {
                this.capture_options = message.capture_options;
              } else {
                this.capture_options = {};
              }

              this.lastActive = new Date();

              // is there any text?
              // or an attachment? (facebook)
              // or multiple attachments (slack)
              // if (message.text || message.attachments || message.attachment) {
              if (message) {

                var outbound = this.cloneMessage(message);
                var that = this;

                outbound.sent_timestamp = new Date().getTime();

                that.sent.push(outbound);
                that.transcript.push(outbound);

                this.task.bot.reply(this.source_message, outbound, function (err, sent_message) {
                  if (err) {
                    botkit.logger.error('An error occurred while sending a message: ', err);

                    // even though an error occured, set sent to true
                    // this will allow the conversation to keep going even if one message fails
                    // TODO: make a message that fails to send _resend_ at least once
                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = err;

                  } else {

                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = sent_message;

                    // if sending via slack's web api, there is no further confirmation
                    // so we can mark the message delivered
                    if (that.task.bot.type == 'slack' && sent_message && sent_message.ts) {
                      that.sent[that.sent.length - 1].delivered = true;
                    }

                    that.trigger('sent', [sent_message]);

                  }
                });
              }
              if (message.action) {
                if (typeof (message.action) == 'function') {
                  message.action(this);
                } else if (message.action == 'repeat') {
                  this.repeat();
                } else if (message.action == 'wait') {
                  this.silentRepeat();
                } else if (message.action == 'stop') {
                  this.stop();
                } else if (message.action == 'timeout') {
                  this.stop('timeout');
                } else if (this.threads[message.action]) {
                  this.gotoThread(message.action);
                }
              }
            } else {
              //console.log('Waiting to send next message...');
            }

            // end immediately instad of waiting til next tick.
            // if it hasn't already been ended by a message action!
            if (this.isActive() && !this.messages.length && !this.handler && !this.processing) {
              this.stop('completed');
            }

          } else if (this.sent.length) { // sent at least 1 message
            this.stop('completed');
          }
        }
      }
    };

    botkit.logger.debug('CREATED A CONVO FOR', this.source_message.user, this.source_message.channel);
    this.gotoThread('default');
  }

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  function Task(bot, message, botkit) {

    this.convos = [];
    this.botkit = botkit;
    this.bot = bot;

    this.events = {};
    this.source_message = message;
    this.status = 'active';
    this.startTime = new Date();

    this.isActive = function () {
      return this.status == 'active';
    };

    this.createConversation = function (message) {
      var convo = new Conversation(this, message);
      convo.id = botkit.convoCount++;
      this.convos.push(convo);
      return convo;
    };

    this.startConversation = function (message) {

      var convo = this.createConversation(message);
      botkit.logger.debug('>   [Start] ', convo.id, ' Conversation with ', message.user, 'in', message.channel);

      convo.activate();
      return convo;
    };

    this.conversationEnded = function (convo) {
      botkit.logger.debug('>   [End] ', convo.id, ' Conversation with ',
        convo.source_message.user, 'in', convo.source_message.channel);
      this.trigger('conversationEnded', [convo]);
      this.botkit.trigger('conversationEnded', [bot, convo]);
      convo.trigger('end', [convo]);
      var actives = 0;
      for (var c = 0; c < this.convos.length; c++) {
        if (this.convos[c].isActive()) {
          actives++;
        }
      }
      if (actives == 0) {
        this.taskEnded();
      }

    };

    this.endImmediately = function (reason) {

      for (var c = 0; c < this.convos.length; c++) {
        if (this.convos[c].isActive()) {
          this.convos[c].stop(reason || 'stopped');
        }
      }

    };

    this.taskEnded = function () {
      botkit.logger.debug('[End] ', this.id, ' Task for ',
        this.source_message.user, 'in', this.source_message.channel);

      this.status = 'completed';
      this.trigger('end', [this]);

    };

    this.on = function (event, cb) {
      botkit.logger.debug('Setting up a handler for', event);
      var events = event.split(/\,/g);
      for (var e in events) {
        if (!this.events[events[e]]) {
          this.events[events[e]] = [];
        }
        this.events[events[e]].push(cb);
      }
      return this;
    };

    this.trigger = function (event, data) {
      if (this.events[event]) {
        for (var e = 0; e < this.events[event].length; e++) {
          var res = this.events[event][e].apply(this, data);
          if (res === false) {
            return;
          }
        }
      }
    };

    this.getResponsesByUser = function () {

      var users = {};

      // go through all conversations
      // extract normalized answers
      for (var c = 0; c < this.convos.length; c++) {

        var user = this.convos[c].source_message.user;
        users[this.convos[c].source_message.user] = {};
        var convo = this.convos[c];
        users[user] = convo.extractResponses();
      }

      return users;

    };

    this.getResponsesBySubject = function () {
      var answers = {};

      // go through all conversations
      // extract normalized answers
      for (var c = 0; c < this.convos.length; c++) {
        var convo = this.convos[c];

        for (var key in convo.responses) {
          if (!answers[key]) {
            answers[key] = {};
          }
          answers[key][convo.source_message.user] = convo.extractResponse(key);
        }
      }

      return answers;
    };

    this.tick = function () {

      for (var c = 0; c < this.convos.length; c++) {
        if (this.convos[c].isActive()) {
          this.convos[c].tick();
        }
      }
    };
  }

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  botkit.storage = {
    teams: {
      get: function (team_id, cb) {
        cb(null, botkit.memory_store.teams[team_id]);
      },
      save: function (team, cb) {
        botkit.logger.warn('Warning: using temporary storage. Data will be lost when process restarts.');
        if (team.id) {
          botkit.memory_store.teams[team.id] = team;
          cb(null, team.id);
        } else {
          cb('No ID specified');
        }
      },
      all: function (cb) {
        cb(null, botkit.memory_store.teams);
      }
    },
    users: {
      get: function (user_id, cb) {
        cb(null, botkit.memory_store.users[user_id]);
      },
      save: function (user, cb) {
        botkit.logger.warn('Warning: using temporary storage. Data will be lost when process restarts.');
        if (user.id) {
          botkit.memory_store.users[user.id] = user;
          cb(null, user.id);
        } else {
          cb('No ID specified');
        }
      },
      all: function (cb) {
        cb(null, botkit.memory_store.users);
      }
    },
    channels: {
      get: function (channel_id, cb) {
        cb(null, botkit.memory_store.channels[channel_id]);
      },
      save: function (channel, cb) {
        botkit.logger.warn('Warning: using temporary storage. Data will be lost when process restarts.');
        if (channel.id) {
          botkit.memory_store.channels[channel.id] = channel;
          cb(null, channel.id);
        } else {
          cb('No ID specified');
        }
      },
      all: function (cb) {
        cb(null, botkit.memory_store.channels);
      }
    }
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  /**
   * hears_regexp - default string matcher uses regular expressions
   *
   * @param  {array}  tests    patterns to match
   * @param  {object} message message object with various fields
   * @return {boolean}        whether or not a pattern was matched
   */
  botkit.hears_regexp = function (tests, message) {
    for (var t = 0; t < tests.length; t++) {
      if (message.text) {

        // the pattern might be a string to match (including regular expression syntax)
        // or it might be a prebuilt regular expression
        var test = null;
        if (typeof (tests[t]) == 'string') {
          try {
            test = new RegExp(tests[t], 'i');
          } catch (err) {
            botkit.logger.error('Error in regular expression: ' + tests[t] + ': ' + err);
            return false;
          }
          if (!test) {
            return false;
          }
        } else {
          test = tests[t];
        }

        let match = message.text.match(test);
        if (match) {
          message.match = match;
          return true;
        }
      }
    }
    return false;
  };

  /**
   * changeEars - change the default matching function
   *
   * @param  {function} new_test a function that accepts (tests, message) and returns a boolean
   */
  botkit.changeEars = function (new_test) {
    botkit.hears_test = new_test;
  };

  botkit.hears = function (keywords, events, middleware_or_cb, cb) {

    // the third parameter is EITHER a callback handler
    // or a middleware function that redefines how the hear works
    var test_function = botkit.hears_test;
    if (cb) {
      test_function = middleware_or_cb;
    } else {
      cb = middleware_or_cb;
    }

    if (typeof (keywords) == 'string') {
      keywords = [keywords];
    }

    if (keywords instanceof RegExp) {
      keywords = [keywords];
    }

    if (typeof (events) == 'string') {
      events = events.split(/\,/g).map(function (str) {
        return str.trim();
      });
    }

    for (var e = 0; e < events.length; e++) {
      (function (keywords, test_function) {
        botkit.on(events[e], function (bot, message) {
          if (test_function && test_function(keywords, message)) {
            botkit.logger.debug('I HEARD', keywords);
            botkit.middleware.heard.run(bot, message, function (err, bot, message) {
              cb.apply(this, [bot, message]);
              botkit.trigger('heard_trigger', [bot, keywords, message]);
            });
            return false;
          }
        }, true);
      })(keywords, test_function);
    }

    return this;
  };

  botkit.on = function (event, cb, is_hearing) {
    botkit.logger.debug('Setting up a handler for', event);
    var events = (typeof (event) == 'string') ? event.split(/\,/g) : event;

    for (var e in events) {
      if (!this.events[events[e]]) {
        this.events[events[e]] = [];
      }
      this.events[events[e]].push({
        callback: cb,
        type: is_hearing ? 'hearing' : 'event'
      });
    }
    return this;
  };

  botkit.trigger = function (event, data) {
    if (this.events[event]) {

      var hearing = this.events[event].filter(function (e) {
        return (e.type == 'hearing');
      });

      var handlers = this.events[event].filter(function (e) {
        return (e.type != 'hearing');
      });

      // first, look for hearing type events
      // these are always handled before normal event handlers
      for (var e = 0; e < hearing.length; e++) {
        var res = hearing[e].callback.apply(this, data);
        if (res === false) {
          return;
        }
      }

      // now, if we haven't already heard something,
      // fire the remaining event handlers
      if (handlers.length) {
        botkit.middleware.triggered.run(data[0], data[1], function (err, bot, message) {
          for (var e = 0; e < handlers.length; e++) {
            var res = handlers[e].callback.apply(this, data);
            if (res === false) {
              return;
            }
          }
        });
      }
    }
  };

  botkit.startConversation = function (bot, message, cb) {
    botkit.startTask(bot, message, function (task, convo) {
      cb(null, convo);
    });
  };

  botkit.createConversation = function (bot, message, cb) {

    var task = new Task(bot, message, this);

    task.id = botkit.taskCount++;

    var convo = task.createConversation(message);

    this.tasks.push(task);

    cb(null, convo);

  };

  botkit.defineBot = function (unit) {
    if (typeof (unit) != 'function') {
      throw new Error('Bot definition must be a constructor function');
    }
    this.worker = unit;
  };

  botkit.spawn = function (config, cb) {

    var worker = new this.worker(this, config);
    // mutate the worker so that we can call middleware
    worker.say = function (message, cb) {
      var platform_message = {};
      botkit.middleware.send.run(worker, message, function (err, worker, message) {
        if (err) {
          botkit.logger.error('An error occured in the send middleware:: ' + err);
        } else {
          botkit.middleware.format.run(worker, message, platform_message, function (err, worker, message, platform_message) {
            if (err) {
              botkit.logger.error('An error occured in the format middleware: ' + err);
            } else {
              worker.send(platform_message, cb);
            }
          });
        }
      });
    };

    // add platform independent convenience methods
    worker.startConversation = function (message, cb) {
      botkit.startConversation(worker, message, cb);
    };

    worker.createConversation = function (message, cb) {
      botkit.createConversation(worker, message, cb);
    };

    botkit.middleware.spawn.run(worker, function (err, worker) {
      if (err) {
        botkit.logger.error('Error in middlware.spawn.run: ' + err);
      } else {
        botkit.trigger('spawned', [worker]);

        if (cb) {
          cb(worker);
        }
      }
    });

    return worker;
  };

  botkit.startTicking = function () {
    if (!botkit.tickInterval) {
      // set up a once a second tick to process messages
      botkit.tickInterval = setInterval(function () {
        botkit.tick();
      }, 1500);
    }
  };

  botkit.shutdown = function () {
    if (botkit.tickInterval) {
      clearInterval(botkit.tickInterval);
    }
  };

  botkit.startTask = function (bot, message, cb) {

    var task = new Task(bot, message, this);

    task.id = botkit.taskCount++;
    botkit.logger.debug('[Start] ', task.id, ' Task for ', message.user, 'in', message.channel);

    var convo = task.startConversation(message);

    this.tasks.push(task);

    if (cb) {
      cb(task, convo);
    } else {
      return task;
    }

  };

  botkit.tick = function () {
    for (let t = 0; t < botkit.tasks.length; t++) {
      botkit.tasks[t].tick();
    }
    for (let t = botkit.tasks.length - 1; t >= 0; t--) {
      if (!botkit.tasks[t].isActive()) {
        botkit.tasks.splice(t, 1);
      }
    }

    this.trigger('tick', []);

  };

  // Provide a fairly simple Express-based webserver
  botkit.setupWebserver = function (port, cb) {

    if (!port) {
      throw new Error('Cannot start webserver without a port');
    }

    var static_dir = process.cwd() + '/public';

    if (botkit.config && botkit.config.webserver && botkit.config.webserver.static_dir)
      static_dir = botkit.config.webserver.static_dir;

    botkit.config.port = port;

    var express = require('express');
    var bodyParser = require('body-parser');

    botkit.webserver = express();
    botkit.webserver.use(bodyParser.json());
    botkit.webserver.use(bodyParser.urlencoded({
      extended: true
    }));
    botkit.webserver.use(express.static(static_dir));

    var server = botkit.webserver.listen(
      botkit.config.port,
      botkit.config.hostname,
      function () {
        botkit.logger.info('** Starting webserver on port ' +
          botkit.config.port);
        if (cb) {
          cb(null, botkit.webserver);
        }
        botkit.trigger('webserver_up', [botkit.webserver]);
      });

    return botkit;
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  /**
   * Define a default worker bot. This function should be customized outside
   * of Botkit and passed in as a parameter by the developer
   **/
  botkit.worker = function (botkit, config) {
    this.botkit = botkit;
    this.config = config;

    this.say = function (message, cb) {
      botkit.logger.debug('SAY:', message);
    };

    this.replyWithQuestion = function (message, question, cb) {

      botkit.startConversation(message, function (convo) {
        convo.ask(question, cb);
      });

    };

    this.reply = function (src, resp) {
      botkit.logger.debug('REPLY:', resp);
    };

    this.findConversation = function (message, cb) {
      botkit.logger.debug('DEFAULT FIND CONVO');
      cb(null);
    };
  };

  /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

  botkit.userAgent = function () {

    if (!botkit.my_user_agent) {
      // set user agent to Botkit
      var ua = 'Botkit/' + botkit.version();

      // add OS info
      ua = ua + ' ' + os.platform() + '/' + os.release();

      // add Node info
      ua = ua + ' ' + 'node/' + process.version.replace('v', '');

      botkit.my_user_agent = ua;
    }

    return botkit.my_user_agent;

  };

  botkit.version = function () {

    if (!botkit.my_version) {
      botkit.my_version = '0.0.0';
    }
    return botkit.my_version;

  };

  botkit.config = configuration;

  /** Default the application to listen to the 0.0.0.0, the default
   * for node's http module. Developers can specify a hostname or IP
   * address to override this.
   **/
  if (!botkit.config.hostname) {
    botkit.config.hostname = '0.0.0.0';
  }

  if (!configuration.logLevel) {
    if (configuration.debug) {
      configuration.logLevel = 'debug';
    } else if (configuration.log === false) {
      configuration.logLevel = 'error';
    } else {
      configuration.logLevel = 'info';
    }
  }

  if (configuration.logger) {
    botkit.logger = configuration.logger;
  } else {
    botkit.logger = logging('abbott-framework:botkit:CoreBot');
  }

  // botkit.log = function () {
  //   botkit.logger.info.apply(botkit.log, arguments);
  // };

  // Object.keys(LogLevels).forEach(function (level) {
  //   botkit.log[level] = botkit.logger.log.bind(botkit.logger, level);
  // });

  // botkit.debug = botkit.logger.debug;

  // if (!botkit.config.disable_startup_messages) {
  //   console.log('Initializing Botkit v' + botkit.version());
  // }

  if (configuration.storage) {
    if (
      configuration.storage.teams &&
      configuration.storage.teams.get &&
      configuration.storage.teams.save &&

      configuration.storage.users &&
      configuration.storage.users.get &&
      configuration.storage.users.save &&

      configuration.storage.channels &&
      configuration.storage.channels.get &&
      configuration.storage.channels.save
    ) {
      botkit.logger.debug('** Using custom storage system.');
      botkit.storage = configuration.storage;
    } else {
      throw new Error('Storage object does not have all required methods!');
    }
  // } else if (configuration.json_file_store) {
  //   botkit.logger.debug('** Using simple storage. Saving data to ' + configuration.json_file_store);
  //   botkit.storage = simple_storage({
  //     path: configuration.json_file_store
  //   });
  } else {
    botkit.logger.debug('** No persistent storage method specified! Data may be lost when process shuts down.');
  }

  // set the default set of ears to use the regular expression matching
  botkit.changeEars(botkit.hears_regexp);

  //enable Botkit Studio
  // studio(botkit);

  return botkit;
}

module.exports = Botkit;


const Botkit = require('../../../botkit/CoreBot');
const logging = require('../../../logging');
const request = require('request');
const crypto = require('crypto');
const bodyParser = require('body-parser');

function Facebookbot(configuration) {

  var api_host = configuration.api_host || 'graph.facebook.com';

  configuration = configuration || {};

  configuration.logger = logging('abbott-framework:botkit:facebook');

  // Create a core botkit bot
  var facebook_botkit = Botkit(configuration);

  if (facebook_botkit.config.require_delivery) {

    facebook_botkit.on('message_delivered', function (bot, message) {

      // get list of mids in this message
      for (var m = 0; m < message.delivery.mids.length; m++) {
        var mid = message.delivery.mids[m];

        // loop through all active conversations this bot is having
        // and mark messages in conversations as delivered = true
        bot.findConversation(message, function (convo) {
          if (convo) {
            for (var s = 0; s < convo.sent.length; s++) {
              if (convo.sent[s].sent_timestamp <= message.delivery.watermark ||
                (convo.sent[s].api_response && convo.sent[s].api_response.mid == mid)) {
                convo.sent[s].delivered = true;
              }
            }
          }
        });
      }

    });

  }

  // For backwards-compatability, support the receive_via_postback config option
  // this causes facebook_postback events to be replicated as message_received events
  // allowing them to be heard without subscribing to additional events
  if (facebook_botkit.config.receive_via_postback) {
    facebook_botkit.on('facebook_postback', function (bot, message) {
      facebook_botkit.trigger('message_received', [bot, message]);
    });
  }

  facebook_botkit.middleware.format.use(function (bot, message, platform_message, next) {

    platform_message.recipient = {};
    platform_message.message = message.sender_action ? undefined : {};

    if (typeof (message.channel) == 'string' && message.channel.match(/\+\d+\(\d\d\d\)\d\d\d\-\d\d\d\d/)) {
      platform_message.recipient.phone_number = message.channel;
    } else {
      platform_message.recipient.id = message.channel;
    }

    if (!message.sender_action) {
      if (message.text) {
        platform_message.message.text = message.text;
      }

      if (message.attachment) {
        platform_message.message.attachment = message.attachment;
      }

      if (message.tag) {
        platform_message.message.tag = message.tag;
      }

      if (message.sticker_id) {
        platform_message.message.sticker_id = message.sticker_id;
      }

      if (message.quick_replies) {

        // sanitize the length of the title to maximum of 20 chars
        var titleLimit = function (title) {
          if (title.length > 20) {
            var newTitle = title.substring(0, 16) + '...';
            return newTitle;
          } else {
            return title;
          }
        };

        platform_message.message.quick_replies = message.quick_replies.map(function (item) {
          var quick_reply = {};
          if (item.content_type === 'text' || !item.content_type) {
            quick_reply = {
              content_type: 'text',
              title: titleLimit(item.title),
              payload: item.payload,
              image_url: item.image_url,
            };
          } else if (item.content_type === 'location') {
            quick_reply = {
              content_type: 'location'
            };
          } else {
            // Future quick replies types
          }
          return quick_reply;
        });
      }
    } else {
      platform_message.sender_action = message.sender_action;
    }

    if (message.sender_action) {
      platform_message.sender_action = message.sender_action;
    }

    if (message.notification_type) {
      platform_message.notification_type = message.notification_type;
    }

    next();

  });

  // customize the bot definition, which will be used when new connections
  // spawn!
  facebook_botkit.defineBot(function (botkit, config) {

    var bot = {
      type: 'fb',
      botkit: botkit,
      config: config || {},
      utterances: botkit.utterances,
    };

    bot.send = function (message, cb) {

      //Add Access Token to outgoing request
      message.access_token = configuration.access_token;

      request({
        method: 'POST',
        json: true,
        headers: {
          'content-type': 'application/json',
        },
        body: message,
        uri: 'https://' + api_host + '/v2.6/me/messages'
      },
        function (err, res, body) {

          if (err) {
            botkit.logger.debug('WEBHOOK ERROR', err);
            return cb && cb(err);
          }

          if (body.error) {
            botkit.logger.debug('API ERROR', body.error);
            return cb && cb(body.error.message);
          }

          botkit.logger.debug('WEBHOOK SUCCESS', body);

          if (cb) {
            cb(null, body);
          }
        });
    };

    bot.startTyping = function (src, cb) {
      var msg = {};
      msg.channel = src.channel;
      msg.sender_action = 'typing_on';
      bot.say(msg, cb);
    };

    bot.stopTyping = function (src, cb) {
      var msg = {};
      msg.channel = src.channel;
      msg.sender_action = 'typing_off';
      bot.say(msg, cb);
    };

    bot.replyWithTyping = function (src, resp, cb) {
      var textLength;

      if (typeof (resp) == 'string') {
        textLength = resp.length;
      } else if (resp.text) {
        textLength = resp.text.length;
      } else {
        textLength = 80; //default attachement text length
      }

      var avgWPM = 85;
      var avgCPM = avgWPM * 7;

      var typingLength = Math.min(Math.floor(textLength / (avgCPM / 60)) * 1000, 5000);

      bot.startTyping(src, function (err) {
        if (err) console.log(err);
        setTimeout(function () {
          bot.reply(src, resp, cb);
        }, typingLength);
      });

    };

    bot.reply = function (src, resp, cb) {
      var msg = {};

      if (typeof (resp) == 'string') {
        msg.text = resp;
      } else {
        msg = resp;
      }

      msg.channel = src.channel;
      msg.to = src.user;

      bot.say(msg, cb);
    };

    bot.findConversation = function (message, cb) {
      botkit.logger.debug('CUSTOM FIND CONVO', message.user, message.channel);
      for (var t = 0; t < botkit.tasks.length; t++) {
        for (var c = 0; c < botkit.tasks[t].convos.length; c++) {
          if (
            botkit.tasks[t].convos[c].isActive() &&
            botkit.tasks[t].convos[c].source_message.user == message.user
          ) {
            botkit.logger.debug('FOUND EXISTING CONVO!');
            cb(botkit.tasks[t].convos[c]);
            return;
          }
        }
      }

      cb();
    };

    // return info about the specific instance of this bot
    // including identity information, and any other info that is relevant
    bot.getInstanceInfo = function (cb) {
      return facebook_botkit.getInstanceInfo(cb);
    };

    bot.getMessageUser = function (message, cb) {
      return new Promise(function (resolve, reject) {
        facebook_botkit.api.user_profile(message.user).then(function (identity) {

          // normalize this into what botkit wants to see
          var profile = {
            id: message.user,
            username: identity.first_name + ' ' + identity.last_name,
            first_name: identity.first_name,
            last_name: identity.last_name,
            full_name: identity.first_name + ' ' + identity.last_name,
            email: null, // no source for this info
            gender: identity.gender,
            timezone_offset: identity.timezone,
          };

          if (cb) {
            cb(null, profile);
          }
          resolve(profile);

        }).catch(function (err) {
          if (cb) {
            cb(err);
          }
          reject(err);
        });
      });

    };

    return bot;
  });

  // return info about the specific instance of this bot
  // including identity information, and any other info that is relevant
  // unlike other platforms, this has to live on the controller
  // so that we can use it before a bot is spawned!
  facebook_botkit.getInstanceInfo = function (cb) {
    return new Promise(function (resolve, reject) {
      var instance = {
        identity: {},
        team: {},
      };

      request.get('https://' + api_host + '/v2.6/me?access_token=' + configuration.access_token,
        {},
        function (err, res, body) {
          if (err) {
            if (cb) cb(err);
            return reject(err);
          } else {

            var identity = null;
            try {
              identity = JSON.parse(body);
            } catch (err01) {
              if (cb) cb(err01);
              return reject(err01);
            }

            // for facebook, the bot and the page have the same identity
            instance.identity.name = identity.name;
            instance.identity.id = identity.id;

            instance.team.name = identity.name;
            instance.team.url = 'http://facebook.com/' + identity.id;
            instance.team.id = identity.id;

            if (cb) cb(null, instance);
            resolve(instance);
          }
        });
    });
  };

  // set up a web route for receiving outgoing webhooks and/or slash commands
  facebook_botkit.createWebhookEndpoints = function (webserver, bot, cb) {

    facebook_botkit.logger.info(
      '** Serving webhook endpoints for Messenger Platform at: ' +
      'http://' + facebook_botkit.config.hostname + ':' + facebook_botkit.config.port + '/facebook/receive');
    webserver.post('/facebook/receive', function (req, res) {
      res.send('ok');
      facebook_botkit.handleWebhookPayload(req, res, bot);
    });

    webserver.get('/facebook/receive', function (req, res) {
      if (req.query['hub.mode'] == 'subscribe') {
        if (req.query['hub.verify_token'] == configuration.verify_token) {
          res.send(req.query['hub.challenge']);
        } else {
          res.send('OK');
        }
      }
    });

    if (cb) {
      cb();
    }

    return facebook_botkit;
  };

  facebook_botkit.handleWebhookPayload = function (req, res, bot) {

    var payload = req.body;

    // facebook may send more than 1 message payload at a time
    // we split these up into multiple message objects for ingestion
    if (payload.entry) {
      for (var e = 0; e < payload.entry.length; e++) {
        if (payload.entry[e].messaging) {
          for (var m = 0; m < payload.entry[e].messaging.length; m++) {
            facebook_botkit.ingest(bot, payload.entry[e].messaging[m], res);
          }
        }
      }
    }

  };

  facebook_botkit.middleware.spawn.use(function (worker, next) {

    // copy the identity that we get when the app initially boots up
    // into the specific bot instance
    worker.identity = facebook_botkit.identity;
    next();

  });

  // universal normalizing steps
  // handle normal messages from users (text, stickers, files, etc count!)
  facebook_botkit.middleware.normalize.use(function normalizeMessage(bot, message, next) {

    // capture the user ID
    message.user = message.sender.id;

    // since there are only 1:1 channels on Facebook, the channel id is set to the user id
    message.channel = message.sender.id;

    // copy over some facebook specific features
    message.page = message.recipient.id;
    next();

  });

  // handle normal messages from users (text, stickers, files, etc count!)
  facebook_botkit.middleware.normalize.use(function handleMessage(bot, message, next) {
    if (message.message) {

      // capture the message text
      message.text = message.message.text;

      // copy over some facebook specific features
      message.seq = message.message.seq;
      message.is_echo = message.message.is_echo;
      message.mid = message.message.mid;
      message.sticker_id = message.message.sticker_id;
      message.attachments = message.message.attachments;
      message.quick_reply = message.message.quick_reply;
      message.nlp = message.message.nlp;
    }

    next();

  });

  // handle postback messages (when a user clicks a button)
  facebook_botkit.middleware.normalize.use(function handlePostback(bot, message, next) {

    if (message.postback) {

      message.text = message.postback.payload;
      message.payload = message.postback.payload;

      message.referral = message.postback.referral;

      message.type = 'facebook_postback';

    }

    next();

  });

  // handle message sub-types
  facebook_botkit.middleware.categorize.use(function handleOptIn(bot, message, next) {

    if (message.optin) {
      message.type = 'facebook_optin';
    }
    if (message.delivery) {
      message.type = 'message_delivered';
    }
    if (message.read) {
      message.type = 'message_read';
    }
    if (message.referral) {
      message.type = 'facebook_referral';
    }
    if (message.account_linking) {
      message.type = 'facebook_account_linking';
    }
    if (message.is_echo) {
      message.type = 'message_echo';
    }

    next();

  });

  facebook_botkit.on('webserver_up', function (webserver) {

    // Validate that requests come from facebook, and abort on validation errors
    if (facebook_botkit.config.validate_requests === true) {
      // Load verify middleware just for post route on our receive webhook, and catch any errors it might throw to prevent the request from being parsed further.
      webserver.post('/facebook/receive', bodyParser.json({ verify: verifyRequest }));
      webserver.use(abortOnValidationError);
    }

  });

  var messenger_profile_api = {
    greeting: function (payload) {
      var message = {
        'greeting': []
      };
      if (Array.isArray(payload)) {
        message.greeting = payload;
      } else {
        message.greeting.push({
          'locale': 'default',
          'text': payload
        });
      }
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_greeting: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('greeting');
    },
    get_greeting: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('greeting', cb);
    },
    get_started: function (payload) {
      var message = {
        'get_started': {
          'payload': payload
        }
      };
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_get_started: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('get_started');
    },
    get_get_started: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('get_started', cb);
    },
    menu: function (payload) {
      var messege = {
        persistent_menu: payload
      };
      facebook_botkit.api.messenger_profile.postAPI(messege);
    },
    delete_menu: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('persistent_menu');
    },
    get_menu: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('persistent_menu', cb);
    },
    account_linking: function (payload) {
      var message = {
        'account_linking_url': payload
      };
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_account_linking: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('account_linking_url');
    },
    get_account_linking: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('account_linking_url', cb);
    },
    domain_whitelist: function (payload) {
      var message = {
        'whitelisted_domains': Array.isArray(payload) ? payload : [payload]
      };
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_domain_whitelist: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('whitelisted_domains');
    },
    get_domain_whitelist: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('whitelisted_domains', cb);
    },
    target_audience: function (payload) {
      var message = {
        'target_audience': payload
      };
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_target_audience: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('target_audience');
    },
    get_target_audience: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('target_audience', cb);
    },
    home_url: function (payload) {
      var message = {
        home_url: payload
      };
      facebook_botkit.api.messenger_profile.postAPI(message);
    },
    delete_home_url: function () {
      facebook_botkit.api.messenger_profile.deleteAPI('home_url');
    },
    get_home_url: function (cb) {
      facebook_botkit.api.messenger_profile.getAPI('home_url', cb);
    },
    postAPI: function (message) {
      request.post('https://' + api_host + '/v2.6/me/messenger_profile?access_token=' + configuration.access_token,
        { form: message },
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not configure messenger profile');
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in messenger profile API call: Could not parse JSON', err01, body);
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.log('ERROR in messenger profile API call: ', results.error.message);
              } else {
                facebook_botkit.logger.debug('Successfully configured messenger profile', body);
              }
            }
          }
        });
    },
    deleteAPI: function (type) {
      var message = {
        'fields': [type]
      };
      request.delete('https://' + api_host + '/v2.6/me/messenger_profile?access_token=' + configuration.access_token,
        { form: message },
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not configure messenger profile');
          } else {
            facebook_botkit.logger.debug('Successfully configured messenger profile', message);
          }
        });
    },
    getAPI: function (fields, cb) {
      request.get('https://' + api_host + '/v2.6/me/messenger_profile?fields=' + fields + '&access_token=' + configuration.access_token,
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not get messenger profile');
            cb(err);
          } else {
            facebook_botkit.logger.debug('Successfully got messenger profile ', body);
            cb(null, body);
          }
        });
    },
    get_messenger_code: function (image_size, cb, ref) {
      var message = {
        'type': 'standard',
        'image_size': image_size || 1000
      };

      if (ref) {
        message.data = { 'ref': ref };
      }

      request.post('https://' + api_host + '/v2.6/me/messenger_codes?access_token=' + configuration.access_token,

        { form: message },
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not configure get messenger code');
            cb(err);
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in messenger code API call: Could not parse JSON', err01, body);
              cb(err01);
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.info('ERROR in messenger code API call: ', results.error.message);
                cb(results.error);
              } else {
                var uri = results.uri;
                facebook_botkit.logger.info('Successfully got messenger code', uri);
                cb(null, uri);
              }
            }
          }
        });
    }
  };

  var attachment_upload_api = {
    upload: function (attachment, cb) {
      var message = {
        message: {
          attachment: attachment
        }
      };

      request.post('https://' + api_host + '/v2.6/me/message_attachments?access_token=' + configuration.access_token,
        { form: message },
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not upload attachment');
            cb(err);
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in attachment upload API call: Could not parse JSON', err01, body);
              cb(err01);
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.info('ERROR in attachment upload API call: ', results.error.message);
                cb(results.error);
              } else {
                var attachment_id = results.attachment_id;
                facebook_botkit.logger.info('Successfully got attachment id ', attachment_id);
                cb(null, attachment_id);
              }
            }
          }
        });
    }

  };

  var tags = {
    get_all: function (cb) {
      request.get('https://' + api_host + '/v2.6/page_message_tags?access_token=' + configuration.access_token,
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not get tags list');
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in page message tags call: Could not parse JSON', err01, body);
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.info('ERROR in page message tags: ', results.error.message);
              } else {
                facebook_botkit.logger.debug('Successfully call page message tags', body);
                cb(results);
              }
            }
          }
        });
    }
  };

  var nlp = {
    enable: function (custom_token) {
      facebook_botkit.api.nlp.postAPI(true, custom_token);
    },
    disable: function (custom_token) {
      facebook_botkit.api.nlp.postAPI(false, custom_token);
    },
    postAPI: function (value, custom_token) {
      var uri = 'https://' + api_host + '/v2.8/me/nlp_configs?nlp_enabled=' + value + '&access_token=' + configuration.access_token;
      if (custom_token) {
        uri += '&custom_token=' + custom_token;
      }
      request.post(uri, {},
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not enable/disable build-in NLP');
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in build-in NLP API call: Could not parse JSON', err01, body);
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.info('ERROR in build-in API call: ', results.error.message);
              } else {
                facebook_botkit.logger.debug('Successfully enable/disable build-in NLP', body);
              }
            }
          }
        });
    }
  };

  var user_profile = function (uid, fields, cb) {
    if (!fields) {
      fields = 'first_name,last_name,timezone,gender,locale';
    }
    return new Promise(function (resolve, reject) {
      var uri = 'https://' + api_host + '/v2.6/' + uid + '?fields=' + fields + '&access_token=' + configuration.access_token;
      request.get(uri, {},
        function (err, res, body) {
          if (err) {
            facebook_botkit.logger.info('Could not get user profile:', err);
            if (cb) {
              cb(err);
            } else {
              reject(err);
            }
            return;
          } else {

            var results = null;
            try {
              results = JSON.parse(body);
            } catch (err01) {
              facebook_botkit.logger.info('ERROR in user profile call: Could not parse JSON', err01, body);
              if (cb) {
                cb(err01);
              } else {
                reject(err01);
              }
              return;
            }

            if (results) {
              if (results.error) {
                facebook_botkit.logger.info('ERROR in user profile API call: ', results.error.message);
                if (cb) {
                  cb(results.error);
                } else {
                  reject(results.error);
                }
              } else {
                if (cb) {
                  cb(null, results);
                } else {
                  resolve(results);
                }
              }
            }
          }
        });
    });
  };

  facebook_botkit.api = {
    'user_profile': user_profile,
    'messenger_profile': messenger_profile_api,
    'thread_settings': messenger_profile_api,
    'attachment_upload': attachment_upload_api,
    'nlp': nlp,
    'tags': tags,
  };

  // Verifies the SHA1 signature of the raw request payload before bodyParser parses it
  // Will abort parsing if signature is invalid, and pass a generic error to response
  function verifyRequest(req, res, buf, encoding) {
    var expected = req.headers['x-hub-signature'];
    var calculated = getSignature(buf);
    if (expected !== calculated) {
      throw new Error('Invalid signature on incoming request');
    } else {
      // facebook_botkit.logger.debug('** X-Hub Verification successful!')
    }
  }

  function getSignature(buf) {
    var hmac = crypto.createHmac('sha1', facebook_botkit.config.app_secret);
    hmac.update(buf, 'utf-8');
    return 'sha1=' + hmac.digest('hex');
  }

  function abortOnValidationError(err, req, res, next) {
    if (err) {
      facebook_botkit.logger.info('** Invalid X-HUB signature on incoming request!');
      facebook_botkit.logger.debug('** X-HUB Validation Error:', err);
      res.status(400).send({
        error: 'Invalid signature.'
      });
    } else {
      next();
    }
  }

  facebook_botkit.getInstanceInfo().then(function (instance) {

    facebook_botkit.identity = instance.identity;

  }).catch(function (err) {

    console.error('Could not load bot identity!');
    throw new Error(err);

  });

  facebook_botkit.startTicking();

  return facebook_botkit;
}

module.exports = Facebookbot;


/**
 * Route
 *
 * @param {String} pattern
 * @param {Function} Handler
 * @param {Object} [observe]
 * @param {Object} [router]
 * @constructor
 * @public
 */
function Route(pattern, Handler, observe, router) {
	this.pattern = pattern;
	this.map = parsePattern(pattern);
	this.regExp = patternToRegExp(pattern);
	this.strictRegExp = patternToStrictRegExp(pattern);
	this.isComponent = !!Handler.extend;
	this.Handler = Handler;
	this.observe = assign({ qs: [], hash: [], state: [] }, observe);
	this.allObserved = this.observe.qs.concat(this.observe.hash, this.observe.state);
	this.router = router || {};
	this.view = null;
}

/**
 * Destroy
 *
 * @returns {Route}
 * @private
 */
Route.prototype.destroy = function () {
	if (this.view) {
		this.view.teardown();
		this.view = null;
	}

	return this;
};

/**
 * Get state
 *
 * @returns {Object}
 * @private
 */
Route.prototype.getState = function () {
	var data = {};

	for (var i = 0, c = this.allObserved.length; i < c; i++) {
		data[this.allObserved[i]] = this.view.get(this.allObserved[i]);
	}

	return {
		qs: pick(data, this.observe.qs),
		hash: pick(data, this.observe.hash),
		state: pick(data, this.observe.state)
	};
};

/**
 * Init
 *
 * @param {Object} uri
 * @param {Object} data
 * @returns {Route}
 * @private
 */
Route.prototype.init = function (uri, data) {
	var _this = this;

	assign(
		data,
		this.parsePath(uri.path),
		parseQS(uri.qs, this.observe.qs),
		parseHash(uri.hash, this.observe.hash)
	);

	// not a component
	if (!this.isComponent) {
		this.Handler({ el: this.router.el, data: data, uri: this.router.uri });
	} else {
		// init new Ractive
		this.view = new this.Handler({
			el: this.router.el,
			data: data
		});

		// observe
		if (this.allObserved.length) {
			this.view.observe(this.allObserved.join(' '), function () {
				if (!_this.updating) {
					_this.router.update();
				}
			}, { init: false });
		}

		// notify Ractive we're done here
		this.view.set('__ready', true);
	}

	return this;
};

/**
 * Match
 *
 * @param {String} request
 * @param {Boolean} strict
 * @returns {Boolean}
 * @private
 */
Route.prototype.match = function (request, strict) {
	return strict
		? this.strictRegExp.test(request)
		: this.regExp.test(request);
};

/**
 * Parse path
 *
 * @param {String} path
 * @returns {Object}
 * @private
 */
Route.prototype.parsePath = function(path) {
	var parsed = path.match(this.regExp);
	var data = {};

	for (var i = 0, c = this.map.length; i < c; i++) {
		if (!isEmpty(parsed[i + 1])) {
			data[this.map[i]] = parseJSON(parsed[i + 1]);
		}
	}

	return data;
};

/**
 * Parse pattern
 *
 * @param {String} pattern
 * @returns {Array}
 * @private
 */
function parsePattern(pattern) {
	return (pattern.match(/\/:\w+/g) || []).map(function (name) {
		return name.substr(2);
	});
}

/**
 * Pattern to RegExp
 *
 * @param pattern
 * @returns {RegExp}
 * @private
 */
function patternToRegExp(pattern) {
	return new RegExp(
		patternToRegExpString(pattern)
			.replace(/^\^(\\\/)?/, '^\\/?')
			.replace(/(\\\/)?\$$/, '\\/?$'),
		'i'
	);
}

/**
 * Pattern to RegExp string
 *
 * @param {String} pattern
 * @returns {String}
 * @private
 */
function patternToRegExpString(pattern) {
	return ('^' + pattern + '$')
		.replace(/\/:\w+(\([^)]+\))?/g, '(?:\/([^/]+)$1)')
		.replace(/\(\?:\/\(\[\^\/]\+\)\(/g, '(?:/(')
		.replace(/\//g, '\\/');
}

/**
 * Pattern to strict RegExp
 *
 * @param {String} pattern
 * @returns {RegExp}
 * @private
 */
function patternToStrictRegExp(pattern) {
	return new RegExp(patternToRegExpString(pattern));
}


var features = module.exports = function(){};

//去除字符串左右空格
function trim(str){
	return str.replace(/(^\s*)|(\s*$)/g, "");
}

//获取某个cookie
function getCookie(cookies,name){
	var match = String(cookies).match(new RegExp(name + '=([^;]+)'));
	if (match){
	   return match[1];
	}
	return false;
}

//同php hexdec方法，16进制转10进制
function hexdec(hex_string) {
  hex_string = (hex_string + '')
    .replace(/[^a-f0-9]/gi, '');
  return parseInt(hex_string, 16);
}

/**
 * switch开关，根据配置on/off控制功能展现关闭
 * @param  {[type]} val [on/off]
 * @param  {[type]} req [description]
 * @param  {[type]} res [description]
 * @return {[type]}     [true/false]
 */
features.switch = function(val,req,res){
    if(val == "on"){
        return true;
    }
    return false;
}

/**
 * 日期开关，在日期范围内的将开启. *代表无限大或者无限小
 * @param  {[type]} val ["2014-04-03 23:10:00"]
 * @param  {[type]} req [description]
 * @param  {[type]} res [description]
 * @return {[type]}     [true/false]
 */
features.date = function(val,req,res){
    if(val){
    	var times = String(val).split("|");
    	var startStamp = 0, endStamp = 0,nowStamp = +new Date;
    	if(times.length == 2){
    		if(trim(times[0]) != "*"){
    			startStamp = +new Date(times[0]); //此处暂时用new Date，对日期字符串有格式要求
    		}
            //第二个*代表无限大
    		if(trim(times[1]) == "*"){
    			endStamp = 2145953471000;
    		}else{
    			endStamp = +new Date(times[1]);
    		}
            //日期范围内的功能开启
    		if(startStamp <  nowStamp && nowStamp < endStamp){
    			return true;
    		}else{
    			return false;
    		}
    	}else{
    		return false;
    	}
    }
    return false;
}

/**
 * 比例开关，0-1范围内配置，根据baiduID进行抽样
 * @param  {[type]} val [description]
 * @param  {[type]} req [description]
 * @param  {[type]} res [description]
 * @return {[type]}     [description]
 */
features.percentage = function(val,req,res){
	if(!req || !req.headers || !val){
		return false;
	}
    //从cookie中读取baiduID
	var baiduId  = getCookie(req.headers['cookie'],"BAIDUID");
	//test
	//baiduId = "93DCA8B25F1F793BA1D4F2B42CD1D879:FG=1";

	var sample = val * 100 , num = Math.random()*100;
	if(baiduId){
        //baiduid后6位是随机数，转为0-100整数
		num = hexdec(baiduId.replace(':FG=1','').substr(-6)) % 100;
	}
	return num < sample;
}


'use strict';

var child_process = require('child_process');

/**
 * This is a DockerCmd representing "docker" command line
 * @constructor
 */
function DockerCmd() {
}

/**
 * @callback DockerCmd~callback
 * @param {number} dockerProcessExitCode - the docker process exit code (0 if all was OK)
 */

/**
 * Execute the given commandName with the given dockerOptions and commandOptions.
 * @param {string} commandName
 * @param {Options} commandOptions
 * @param {Object} dockerOptions
 * @param {DockerCmd~callback} callback
 */
DockerCmd.prototype.executeCommand = function(commandName, commandOptions, dockerOptions, callback) {
    // put all options in an array to give to "spawn" later
    var cmdOptions = ['docker'];

    // first the docker options to pass before the docker command
    appendOptions(cmdOptions, dockerOptions);
    // then the docker command
    cmdOptions.push(commandName);
    // and finally the command options with potentially final args (using the '_' field)
    appendOptions(cmdOptions, commandOptions);

    var dockerProcess = child_process.spawn('/usr/bin/env', cmdOptions, {stdio: 'inherit'});
    dockerProcess.on('close', callback);
};

/**
 * @param {string} commandName
 * @return {function(this:DockerCmd, Options, Object, DockerCmd~callback)}
 * @private
 */
DockerCmd.prototype._createDefaultCommand = function(commandName) {
    var self = this;
    /**
     * @param {Options} commandOptions
     * @param {Object} dockerOptions
     * @param {DockerCmd~callback} callback
     */
    return function(commandOptions, dockerOptions, callback) {
        self.executeCommand(commandName, commandOptions, dockerOptions, callback);
    }
};

/// Declare all the docker commands
[
    'attach',
    'build',
    'commit',
    'cp',
    'diff',
    'events',
    'export',
    'history',
    'images',
    'import',
    'info',
    'inspect',
    'kill',
    'load',
    'login',
    'logout',
    'logs',
    'port',
    'pause',
    'ps',
    'pull',
    'push',
    'restart',
    'rm',
    'run',
    'save',
    'search',
    'start',
    'stop',
    'tag',
    'top',
    'unpause',
    'version',
    'wait'
].forEach(function (commandName) {
        DockerCmd.prototype[commandName] = DockerCmd.prototype._createDefaultCommand(commandName);
    });

/**
 * Append each option from the given fromOptions to the given
 * options array, flattening them to pass them later as parameters to a
 * sub call process.
 * @param {string[]} options
 * @param {Options} fromOptions
 */
function appendOptions(options, fromOptions) {
    function pushOption(optionName, optionValue) {
        var valueDefined = optionValue !== null && optionValue !== undefined;
        if (optionName.length === 1) {
            // simple letter option
            options.push('-' + optionName);
            if (valueDefined) {
                options.push(optionValue);
            }
        } else {
            // full option name
            options.push('--' + optionName + (valueDefined ? '=' + optionValue : ''));
        }
    }
    for (var optionName in fromOptions) {
        if (fromOptions.hasOwnProperty(optionName) && optionName !== '_') {
            var optionValue = fromOptions[optionName];
            if (Array.isArray(optionValue)) {
                // we have multiple values for the same option, let's iterate on each
                optionValue.forEach(function (iOptionValue) {
                    pushOption(optionName, iOptionValue);
                });
            } else {
                pushOption(optionName, optionValue);
            }
        }
    }
    // now append the "_" which are not "options" but args
    if (fromOptions && fromOptions._) {
        [].concat(fromOptions._).forEach(function(arg) {
           options.push(arg);
        });
    }
}

module.exports = DockerCmd;


var request                 = require('request')
  , async                   = require('async')
  ;

var config                  = require('../config')
  , DEFAULT_TRACKER_SERVER  = config.DEFAULT_TRACKER_SERVER
  , DEFAULT_TRACKER_PORT    = config.DEFAULT_TRACKER_PORT
  ;

/******************************************************************************
 * Performs the given request on the KISSmetrics tracker host.
 *
 * @param pathname {String} The path section of the URL, that comes after the
 * host and before the query, including the initial slash if present.
 *
 * @param query_params {Object}
 *
 * @param callback {Function} A callback of the form `function(res)`.
 *
 */
function request_kissmetrics_client(pathname, query_params, callback) {

  var query_string  = ['_k=', this.key, '&'].join('')
    , uri
    , v
    ;

  query_params['_t'] = (query_params['_d'] ? query_params['_t'] : Date.now());

  for(var k in query_params) {

    v = query_params[k];

    if (query_string !== '') {
      query_string += '&';
    }

    query_string += [k, v].join('=');

  }

  uri = encodeURI(['http', ':', '//', this.host, ':', this.port, pathname, '?', query_string].join(''));

  // should encode the = sign in the user id
  var regex = new RegExp("&_p=" + query_params["_p"] + "&");
  var uri = uri.replace(regex, "&_p=" + query_params["_p"].replace("=", "%3D") + "&");

  request({ uri: uri }, function (err, res) {

    if (err) {

      console.log('xxx KISSmetrics error');
      console.log('    uri    : ' + uri);
      console.log('    error  : ' + err.message);

    } else if (res.statusCode !== 200) {
      err = new Error('KISSmetrics error ---> RECEIVED WRONG STATUS CODE [' + res.statusCode + ']');
    }

    callback(err, res);

  });

}

kissmetrics_client.prototype.request = request_kissmetrics_client;

/******************************************************************************
 * Sets properties on a person without recording an event by making a request.
 *
 * @param person {String} The identity of the person.
 *
 * This is limited to 255 chars and all commas (,), and colons (:) will
 * automatically be changed to spaces (KISSmetrics will convert foo:bar,baz to
 * foo bar baz).
 *
 * @param properties {Object} An object containing the properties to be set on
 * `person`.
 *
 * @param callback {Function} A function of the form `function(err)`.
 *
 */
function set_kissmetrics_client(person, properties, callback) {

  var query_params = new Object(properties)
    ;

  query_params['_p'] = person;

  this.request('/s', query_params, callback);

}

kissmetrics_client.prototype.set        = set_kissmetrics_client;
kissmetrics_client.prototype.properties = set_kissmetrics_client;

/******************************************************************************
 * Aliases the user identified by `person` with `aliases`.
 *
 * @param person {String} The identity of the person.
 *
 * This is limited to 255 chars and all commas (,), and colons (:) will
 * automatically be changed to spaces (KISSmetrics will convert foo:bar,baz to
 * foo bar baz).
 *
 * @param alias | aliases {String|Array} The alias to apply to the person.
 *
 * This is limited to 255 chars and all commas (,), and colons (:) will
 * automatically be changed to spaces (KISSmetrics will convert foo:bar,baz to
 * foo bar baz).
 *
 * Can either be a string or an array of string if multiple alias are supplied.
 *
 * @param callback {Function} A callback of the form `function(err)`.
 *
 */
function alias_kissmetrics_client(person, aliases, callback) {

  var aliases = (Array.isArray(aliases) ? aliases : [aliases])
    , that    = this
    ;

  async.forEach(

      aliases

    , function(alias, callback) {

        var query_params  = {}
          ;

        query_params['_p'] = person;
        query_params['_n'] = alias;

        that.request('/a', query_params, callback);

      }

    , callback

  );

}

kissmetrics_client.prototype.alias = alias_kissmetrics_client;

/******************************************************************************
 * Records `event` for `person. Also sets `properties` on the person if
 * specified.
 *
 * @param person {String} The identity of the person doing the event.
 *
 * This is limited to 255 chars and all commas (,), and colons (:) will
 * automatically be changed to spaces (KISSmetrics will convert foo:bar,baz to
 * foo bar baz).
 *
 * @param event {String} The name of the event you want to record.
 *
 * This is limited to 255 chars and all commas (,), and colons (:) will
 * automatically be changed to spaces (KISSmetrics will convert foo:bar,baz to
 * foo bar baz).
 *
 * @param properties {Object} (optional) An object containing the properties to
 * be set on `person`.
 *
 * @param callback {Function} A function of the form `function(err)`.
 */
function record_kissmetrics_client() {

  var args          = Array.prototype.slice.call(arguments)

    , callback      = args.pop()

    , person        = args.shift()
    , event         = args.shift()
    , properties    = args.shift() || {}

    , query_params  = new Object(properties)
    ;

  query_params['_p'] = person;
  query_params['_n'] = event;

  this.request('/e', query_params, callback);

}

kissmetrics_client.prototype.record = record_kissmetrics_client;
kissmetrics_client.prototype.event  = record_kissmetrics_client;

/******************************************************************************
 * KISSmetrics REST client constructor.
 *
 * @param options {Object} The options object defining the REST client to
 * instantiate. Its possible parameters are :
 *
 *  @param key {String} your KISSmetrics API key.
 *
 *  @param host {String} (optional) The tracker host to which the client must be
 *  connected to. Defaults to 'trk.kissmetrics.com'
 *
 *  @param port {Number} (optional) The port of the tracker to connect to.
 *  Defaults to port 80
 *
 */
function kissmetrics_client(options) {

  var options   = options || {}
    ;

  this.host = options.host || DEFAULT_TRACKER_SERVER;
  this.port = options.port || DEFAULT_TRACKER_PORT;

  this.key  = options.key;

}

module.exports = kissmetrics_client;