var AWS = require('../core');
var path = require('path');
var crypto = require('crypto');
var iniLoader = AWS.util.iniLoader;

/**
 *  Represents credentials from sso.getRoleCredentials API for
 * `sso_*` values defined in shared credentials file.
 *
 * ## Using SSO credentials
 *
 * The credentials file must specify the information below to use sso:
 *
 *     [profile sso-profile]
 *     sso_account_id = 012345678901
 *     sso_region = **-****-*
 *     sso_role_name = SampleRole
 *     sso_start_url = https://d-******.awsapps.com/start
 *
 * or using the session format:
 *
 *     [profile sso-token]
 *     sso_session = prod
 *     sso_account_id = 012345678901
 *     sso_role_name = SampleRole
 *
 *     [sso-session prod]
 *     sso_region = **-****-*
 *     sso_start_url = https://d-******.awsapps.com/start
 *
 * This information will be automatically added to your shared credentials file by running
 * `aws configure sso`.
 *
 * ## Using custom profiles
 *
 * The SDK supports loading credentials for separate profiles. This can be done
 * in two ways:
 *
 * 1. Set the `AWS_PROFILE` environment variable in your process prior to
 *    loading the SDK.
 * 2. Directly load the AWS.SsoCredentials provider:
 *
 * ```javascript
 * var creds = new AWS.SsoCredentials({profile: 'myprofile'});
 * AWS.config.credentials = creds;
 * ```
 *
 * @!macro nobrowser
 */
AWS.SsoCredentials = AWS.util.inherit(AWS.Credentials, {
  /**
   * Creates a new SsoCredentials object.
   *
   * @param options [map] a set of options
   * @option options profile [String] (AWS_PROFILE env var or 'default')
   *   the name of the profile to load.
   * @option options filename [String] ('~/.aws/credentials' or defined by
   *   AWS_SHARED_CREDENTIALS_FILE process env var)
   *   the filename to use when loading credentials.
   * @option options callback [Function] (err) Credentials are eagerly loaded
   *   by the constructor. When the callback is called with no error, the
   *   credentials have been loaded successfully.
   */
  constructor: function SsoCredentials(options) {
    AWS.Credentials.call(this);

    options = options || {};
    this.errorCode = 'SsoCredentialsProviderFailure';
    this.expired = true;

    this.filename = options.filename;
    this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
    this.service = options.ssoClient;
    this.httpOptions = options.httpOptions || null;
    this.get(options.callback || AWS.util.fn.noop);
  },

  /**
   * @api private
   */
  load: function load(callback) {
    var self = this;

    try {
      var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader, this.filename);
      var profile = profiles[this.profile] || {};

      if (Object.keys(profile).length === 0) {
        throw AWS.util.error(
          new Error('Profile ' + this.profile + ' not found'),
          { code: self.errorCode }
        );
      }

      if (profile.sso_session) {
        if (!profile.sso_account_id || !profile.sso_role_name) {
          throw AWS.util.error(
            new Error('Profile ' + this.profile + ' with session ' + profile.sso_session +
              ' does not have valid SSO credentials. Required parameters "sso_account_id", "sso_session", ' +
              '"sso_role_name". Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html'),
            { code: self.errorCode }
          );
        }
      } else {
        if (!profile.sso_start_url || !profile.sso_account_id || !profile.sso_region || !profile.sso_role_name) {
          throw AWS.util.error(
            new Error('Profile ' + this.profile + ' does not have valid SSO credentials. Required parameters "sso_account_id", "sso_region", ' +
            '"sso_role_name", "sso_start_url". Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html'),
            { code: self.errorCode }
          );
        }
      }

      this.getToken(this.profile, profile, function (err, token) {
        if (err) {
          return callback(err);
        }
        var request = {
          accessToken: token,
          accountId: profile.sso_account_id,
          roleName: profile.sso_role_name,
        };

        if (!self.service || self.service.config.region !== profile.sso_region) {
          self.service = new AWS.SSO({
            region: profile.sso_region,
            httpOptions: self.httpOptions,
          });
        }

        self.service.getRoleCredentials(request, function(err, data) {
          if (err || !data || !data.roleCredentials) {
            callback(AWS.util.error(
              err || new Error('Please log in using "aws sso login"'),
              { code: self.errorCode }
            ), null);
          } else if (!data.roleCredentials.accessKeyId || !data.roleCredentials.secretAccessKey || !data.roleCredentials.sessionToken || !data.roleCredentials.expiration) {
            throw AWS.util.error(new Error(
              'SSO returns an invalid temporary credential.'
            ));
          } else {
            self.expired = false;
            self.accessKeyId = data.roleCredentials.accessKeyId;
            self.secretAccessKey = data.roleCredentials.secretAccessKey;
            self.sessionToken = data.roleCredentials.sessionToken;
            self.expireTime = new Date(data.roleCredentials.expiration);
            callback(null);
          }
        });
      });
    } catch (err) {
      callback(err);
    }
  },

  /**
   * @private
   * Uses legacy file system retrieval or if sso-session is set,
   * use the SSOTokenProvider.
   *
   * @param {string} profileName - name of the profile.
   * @param {object} profile - profile data containing sso_session or sso_start_url etc.
   * @param {function} callback - called with (err, (string) token).
   *
   * @returns {void}
   */
  getToken: function getToken(profileName, profile, callback) {
    var self = this;

    if (profile.sso_session) {
      var _iniLoader = AWS.util.iniLoader;
      var ssoSessions = _iniLoader.loadSsoSessionsFrom();
      var ssoSession = ssoSessions[profile.sso_session];
      Object.assign(profile, ssoSession);

      var ssoTokenProvider = new AWS.SSOTokenProvider({
        profile: profileName,
      });
      ssoTokenProvider.get(function (err) {
        if (err) {
          return callback(err);
        }
        return callback(null, ssoTokenProvider.token);
      });
      return;
    }

    try {
      /**
       * The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
       * This is needed because server side may have invalidated the token before the defined expiration date.
       */
      var EXPIRE_WINDOW_MS = 15 * 60 * 1000;
      var hasher = crypto.createHash('sha1');
      var fileName = hasher.update(profile.sso_start_url).digest('hex') + '.json';
      var cachePath = path.join(
        iniLoader.getHomeDir(),
        '.aws',
        'sso',
        'cache',
        fileName
      );
      var cacheFile = AWS.util.readFileSync(cachePath);
      var cacheContent = null;
      if (cacheFile) {
        cacheContent = JSON.parse(cacheFile);
      }
      if (!cacheContent) {
        throw AWS.util.error(
          new Error('Cached credentials not found under ' + this.profile + ' profile. Please make sure you log in with aws sso login first'),
          { code: self.errorCode }
        );
      }

      if (!cacheContent.startUrl || !cacheContent.region || !cacheContent.accessToken || !cacheContent.expiresAt) {
        throw AWS.util.error(
          new Error('Cached credentials are missing required properties. Try running aws sso login.')
        );
      }

      if (new Date(cacheContent.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) {
        throw AWS.util.error(new Error(
          'The SSO session associated with this profile has expired. To refresh this SSO session run aws sso login with the corresponding profile.'
        ));
      }

      return callback(null, cacheContent.accessToken);
    } catch (err) {
      return callback(err, null);
    }
  },

  /**
   * Loads the credentials from the AWS SSO process
   *
   * @callback callback function(err)
   *   Called after the AWS SSO process has been executed. 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) {
    iniLoader.clearCachedFiles();
    this.coalesceRefresh(callback || AWS.util.fn.callback);
  },
});
