const debug = require('debug')('ali-oss:sts');
const crypto = require('crypto');
const querystring = require('querystring');
const copy = require('copy-to');
const AgentKeepalive = require('agentkeepalive');
const is = require('is-type-of');
const ms = require('humanize-ms');
const urllib = require('urllib');

const globalHttpAgent = new AgentKeepalive();

function STS(options) {
  if (!(this instanceof STS)) {
    return new STS(options);
  }

  if (!options || !options.accessKeyId || !options.accessKeySecret) {
    throw new Error('require accessKeyId, accessKeySecret');
  }

  this.options = {
    endpoint: options.endpoint || 'https://sts.aliyuncs.com',
    format: 'JSON',
    apiVersion: '2015-04-01',
    sigMethod: 'HMAC-SHA1',
    sigVersion: '1.0',
    timeout: '60s'
  };
  copy(options).to(this.options);

  // support custom agent and urllib client
  if (this.options.urllib) {
    this.urllib = this.options.urllib;
  } else {
    this.urllib = urllib;
    this.agent = this.options.agent || globalHttpAgent;
  }
}

module.exports = STS;

const proto = STS.prototype;

/**
 * STS opertaions
 */

proto.assumeRole = async function assumeRole(role, policy, expiration, session, options) {
  const opts = this.options;
  const params = {
    Action: 'AssumeRole',
    RoleArn: role,
    RoleSessionName: session || 'app',
    DurationSeconds: expiration || 3600,

    Format: opts.format,
    Version: opts.apiVersion,
    AccessKeyId: opts.accessKeyId,
    SignatureMethod: opts.sigMethod,
    SignatureVersion: opts.sigVersion,
    SignatureNonce: Math.random(),
    Timestamp: new Date().toISOString()
  };

  if (policy) {
    let policyStr;
    if (is.string(policy)) {
      try {
        policyStr = JSON.stringify(JSON.parse(policy));
      } catch (err) {
        throw new Error(`Policy string is not a valid JSON: ${err.message}`);
      }
    } else {
      policyStr = JSON.stringify(policy);
    }
    params.Policy = policyStr;
  }

  const signature = this._getSignature('POST', params, opts.accessKeySecret);
  params.Signature = signature;

  const reqUrl = opts.endpoint;
  const reqParams = {
    agent: this.agent,
    timeout: ms((options && options.timeout) || opts.timeout),
    method: 'POST',
    content: querystring.stringify(params),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    ctx: options && options.ctx
  };

  const result = await this.urllib.request(reqUrl, reqParams);
  debug('response %s %s, got %s, headers: %j', reqParams.method, reqUrl, result.status, result.headers);

  if (Math.floor(result.status / 100) !== 2) {
    const err = await this._requestError(result);
    err.params = reqParams;
    throw err;
  }
  result.data = JSON.parse(result.data);

  return {
    res: result.res,
    credentials: result.data.Credentials
  };
};

proto._requestError = async function _requestError(result) {
  const err = new Error();
  err.status = result.status;

  try {
    const resp = (await JSON.parse(result.data)) || {};
    err.code = resp.Code;
    err.message = `${resp.Code}: ${resp.Message}`;
    err.requestId = resp.RequestId;
  } catch (e) {
    err.message = `UnknownError: ${String(result.data)}`;
  }

  return err;
};

proto._getSignature = function _getSignature(method, params, key) {
  const that = this;
  const canoQuery = Object.keys(params)
    .sort()
    .map(k => `${that._escape(k)}=${that._escape(params[k])}`)
    .join('&');

  const stringToSign = `${method.toUpperCase()}&${this._escape('/')}&${this._escape(canoQuery)}`;

  debug('string to sign: %s', stringToSign);

  let signature = crypto.createHmac('sha1', `${key}&`);
  signature = signature.update(stringToSign).digest('base64');

  debug('signature: %s', signature);

  return signature;
};

/**
 * Since `encodeURIComponent` doesn't encode '*', which causes
 * 'SignatureDoesNotMatch'. We need do it ourselves.
 */
proto._escape = function _escape(str) {
  return encodeURIComponent(str)
    .replace(/!/g, '%21')
    .replace(/'/g, '%27')
    .replace(/\(/g, '%28')
    .replace(/\)/g, '%29')
    .replace(/\*/g, '%2A');
};
