var AWS = require('../core');
require('../metadata_service');

/**
 * Represents credentials received from the metadata service on an EC2 instance.
 *
 * By default, this class will connect to the metadata service using
 * {AWS.MetadataService} and attempt to load any available credentials. If it
 * can connect, and credentials are available, these will be used with zero
 * configuration.
 *
 * This credentials class will by default timeout after 1 second of inactivity
 * and retry 3 times.
 * If your requests to the EC2 metadata service are timing out, you can increase
 * these values by configuring them directly:
 *
 * ```javascript
 * AWS.config.credentials = new AWS.EC2MetadataCredentials({
 *   httpOptions: { timeout: 5000 }, // 5 second timeout
 *   maxRetries: 10, // retry 10 times
 *   retryDelayOptions: { base: 200 }, // see AWS.Config for information
 *   logger: console // see AWS.Config for information
 *   ec2MetadataV1Disabled: false // whether to block IMDS v1 fallback.
 * });
 * ```
 *
 * If your requests are timing out in connecting to the metadata service, such
 * as when testing on a development machine, you can use the connectTimeout
 * option, specified in milliseconds, which also defaults to 1 second.
 *
 * If the requests failed or returns expired credentials, it will
 * extend the expiration of current credential, with a warning message. For more
 * information, please go to:
 * https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html
 *
 * @!attribute originalExpiration
 *   @return [Date] The optional original expiration of the current credential.
 *   In case of AWS outage, the EC2 metadata will extend expiration of the
 *   existing credential.
 *
 * @see AWS.Config.retryDelayOptions
 * @see AWS.Config.logger
 *
 * @!macro nobrowser
 */
AWS.EC2MetadataCredentials = AWS.util.inherit(AWS.Credentials, {
  constructor: function EC2MetadataCredentials(options) {
    AWS.Credentials.call(this);

    options = options ? AWS.util.copy(options) : {};
    options = AWS.util.merge(
      {maxRetries: this.defaultMaxRetries}, options);
    if (!options.httpOptions) options.httpOptions = {};
    options.httpOptions = AWS.util.merge(
      {timeout: this.defaultTimeout,
        connectTimeout: this.defaultConnectTimeout},
       options.httpOptions);

    this.metadataService = new AWS.MetadataService(options);
    this.logger = options.logger || AWS.config && AWS.config.logger;
  },

  /**
   * @api private
   */
  defaultTimeout: 1000,

   /**
   * @api private
   */
  defaultConnectTimeout: 1000,

  /**
   * @api private
   */
  defaultMaxRetries: 3,

  /**
   * The original expiration of the current credential. In case of AWS
   * outage, the EC2 metadata will extend expiration of the existing
   * credential.
   */
  originalExpiration: undefined,

  /**
   * Loads the credentials from the instance metadata service
   *
   * @callback callback function(err)
   *   Called when the instance metadata service responds (or fails). When
   *   this callback is called with no error, it means that the credentials
   *   information has been loaded into the object (as the `accessKeyId`,
   *   `secretAccessKey`, and `sessionToken` properties).
   *   @param err [Error] if an error occurred, this value will be filled
   * @see get
   */
  refresh: function refresh(callback) {
    this.coalesceRefresh(callback || AWS.util.fn.callback);
  },

  /**
   * @api private
   * @param callback
   */
  load: function load(callback) {
    var self = this;
    self.metadataService.loadCredentials(function(err, creds) {
      if (err) {
        if (self.hasLoadedCredentials()) {
          self.extendExpirationIfExpired();
          callback();
        } else {
          callback(err);
        }
      } else {
        self.setCredentials(creds);
        self.extendExpirationIfExpired();
        callback();
      }
    });
  },

  /**
   * Whether this credential has been loaded.
   * @api private
   */
  hasLoadedCredentials: function hasLoadedCredentials() {
    return this.AccessKeyId && this.secretAccessKey;
  },

  /**
   * if expired, extend the expiration by 15 minutes base plus a jitter of 5
   * minutes range.
   * @api private
   */
  extendExpirationIfExpired: function extendExpirationIfExpired() {
    if (this.needsRefresh()) {
      this.originalExpiration = this.originalExpiration || this.expireTime;
      this.expired = false;
      var nextTimeout = 15 * 60 + Math.floor(Math.random() * 5 * 60);
      var currentTime = AWS.util.date.getDate().getTime();
      this.expireTime = new Date(currentTime + nextTimeout * 1000);
      // TODO: add doc link;
      this.logger.warn('Attempting credential expiration extension due to a '
          + 'credential service availability issue. A refresh of these '
          + 'credentials will be attempted again at ' + this.expireTime
          + '\nFor more information, please visit: https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html');
    }
  },

  /**
   * Update the credential with new credential responded from EC2 metadata
   * service.
   * @api private
   */
  setCredentials: function setCredentials(creds) {
    var currentTime = AWS.util.date.getDate().getTime();
    var expireTime = new Date(creds.Expiration);
    this.expired = currentTime >= expireTime ? true : false;
    this.metadata = creds;
    this.accessKeyId = creds.AccessKeyId;
    this.secretAccessKey = creds.SecretAccessKey;
    this.sessionToken = creds.Token;
    this.expireTime = expireTime;
  }
});
