// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

/*!
 * Module Dependencies.
 */

var g = require('../../lib/globalize');
var loopback = require('../../lib/loopback');
var assert = require('assert');
var uid = require('uid2');
var DEFAULT_TOKEN_LEN = 64;

/**
 * Token based authentication and access control.
 *
 * **Default ACLs**
 *
 *  - DENY EVERYONE `*`
 *  - ALLOW EVERYONE create
 *
 * @property {String} id Generated token ID.
 * @property {Number} ttl Time to live in seconds, 2 weeks by default.
 * @property {Date} created When the token was created.
 * @property {Object} settings Extends the `Model.settings` object.
 * @property {Number} settings.accessTokenIdLength Length of the base64-encoded string access token. Default value is 64.
 * Increase the length for a more secure access token.
 *
 * @class AccessToken
 * @inherits {PersistedModel}
 */

module.exports = function(AccessToken) {
  // Workaround for https://github.com/strongloop/loopback/issues/292
  AccessToken.definition.rawProperties.created.default =
  AccessToken.definition.properties.created.default = function() {
    return new Date();
  };

  /**
   * Anonymous Token
   *
   * ```js
   * assert(AccessToken.ANONYMOUS.id === '$anonymous');
   * ```
   */

  AccessToken.ANONYMOUS = new AccessToken({ id: '$anonymous' });

  /**
   * Create a cryptographically random access token id.
   *
   * @callback {Function} callback
   * @param {Error} err
   * @param {String} token
   */

  AccessToken.createAccessTokenId = function(fn) {
    uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
      if (err) {
        fn(err);
      } else {
        fn(null, guid);
      }
    });
  };

  /*!
   * Hook to create accessToken id.
   */
  AccessToken.observe('before save', function(ctx, next) {
    if (!ctx.instance || ctx.instance.id) {
      // We are running a partial update or the instance already has an id
      return next();
    }

    AccessToken.createAccessTokenId(function(err, id) {
      if (err) return next(err);
      ctx.instance.id = id;
      next();
    });
  });

  /**
   * Find a token for the given `ServerRequest`.
   *
   * @param {ServerRequest} req
   * @param {Object} [options] Options for finding the token
   * @callback {Function} callback
   * @param {Error} err
   * @param {AccessToken} token
   */

  AccessToken.findForRequest = function(req, options, cb) {
    if (cb === undefined && typeof options === 'function') {
      cb = options;
      options = {};
    }

    var id = tokenIdForRequest(req, options);

    if (id) {
      this.findById(id, function(err, token) {
        if (err) {
          cb(err);
        } else if (token) {
          token.validate(function(err, isValid) {
            if (err) {
              cb(err);
            } else if (isValid) {
              cb(null, token);
            } else {
              var e = new Error(g.f('Invalid Access Token'));
              e.status = e.statusCode = 401;
              e.code = 'INVALID_TOKEN';
              cb(e);
            }
          });
        } else {
          cb();
        }
      });
    } else {
      process.nextTick(function() {
        cb();
      });
    }
  };

  /**
   * Validate the token.
   *
   * @callback {Function} callback
   * @param {Error} err
   * @param {Boolean} isValid
   */

  AccessToken.prototype.validate = function(cb) {
    try {
      assert(
          this.created && typeof this.created.getTime === 'function',
        'token.created must be a valid Date'
      );
      assert(this.ttl !== 0, 'token.ttl must be not be 0');
      assert(this.ttl, 'token.ttl must exist');
      assert(this.ttl >= -1, 'token.ttl must be >= -1');

      var AccessToken = this.constructor;
      var userRelation = AccessToken.relations.user; // may not be set up
      var User = userRelation && userRelation.modelTo;

      var now = Date.now();
      var created = this.created.getTime();
      var elapsedSeconds = (now - created) / 1000;
      var secondsToLive = this.ttl;
      var eternalTokensAllowed = !!(User && User.settings.allowEternalTokens);
      var isEternalToken = secondsToLive === -1;
      var isValid = isEternalToken ?
        eternalTokensAllowed :
        elapsedSeconds < secondsToLive;

      if (isValid) {
        cb(null, isValid);
      } else {
        this.destroy(function(err) {
          cb(err, isValid);
        });
      }
    } catch (e) {
      cb(e);
    }
  };

  function tokenIdForRequest(req, options) {
    var params = options.params || [];
    var headers = options.headers || [];
    var cookies = options.cookies || [];
    var i = 0;
    var length, id;

    // https://github.com/strongloop/loopback/issues/1326
    if (options.searchDefaultTokenKeys !== false) {
      params = params.concat(['access_token']);
      headers = headers.concat(['X-Access-Token', 'authorization']);
      cookies = cookies.concat(['access_token', 'authorization']);
    }

    for (length = params.length; i < length; i++) {
      var param = params[i];
      // replacement for deprecated req.param()
      id = req.params && req.params[param] !== undefined ? req.params[param] :
        req.body && req.body[param] !== undefined ? req.body[param] :
        req.query && req.query[param] !== undefined ? req.query[param] :
        undefined;

      if (typeof id === 'string') {
        return id;
      }
    }

    for (i = 0, length = headers.length; i < length; i++) {
      id = req.header(headers[i]);

      if (typeof id === 'string') {
        // Add support for oAuth 2.0 bearer token
        // http://tools.ietf.org/html/rfc6750
        if (id.indexOf('Bearer ') === 0) {
          id = id.substring(7);
          // Decode from base64
          var buf = new Buffer(id, 'base64');
          id = buf.toString('utf8');
        } else if (/^Basic /i.test(id)) {
          id = id.substring(6);
          id = (new Buffer(id, 'base64')).toString('utf8');
          // The spec says the string is user:pass, so if we see both parts
          // we will assume the longer of the two is the token, so we will
          // extract "a2b2c3" from:
          //   "a2b2c3"
          //   "a2b2c3:"   (curl http://a2b2c3@localhost:3000/)
          //   "token:a2b2c3" (curl http://token:a2b2c3@localhost:3000/)
          //   ":a2b2c3"
          var parts = /^([^:]*):(.*)$/.exec(id);
          if (parts) {
            id = parts[2].length > parts[1].length ? parts[2] : parts[1];
          }
        }
        return id;
      }
    }

    if (req.signedCookies) {
      for (i = 0, length = cookies.length; i < length; i++) {
        id = req.signedCookies[cookies[i]];

        if (typeof id === 'string') {
          return id;
        }
      }
    }
    return null;
  }
};