'use strict';

const debug = require('debug')('wechat-JSSDK');

const isEmpty = require('lodash.isempty');
const urlParser = require('url');

const utils = require('./utils');
const config = require('./config');

const Store = require('./store/Store');
const FileStore = require('./store/FileStore');

const wxConfig = config.getDefaultConfiguration();

class JSSDK {
  /**
   * Pass custom wechat config for the instance
   * @constructor
   * @param {object=} options
   * @see ./config.js
   * @return {JSSDK} JSSDK instance
   */
  constructor(options) {
    config.checkPassedConfiguration(options);
    this.refreshedTimes = 0;
    this.wechatConfig = isEmpty(options)
      ? /* istanbul ignore next  */ wxConfig
      : Object.assign({}, wxConfig, options);

    //no custom store provided, using default FileStore
    /* istanbul ignore if  */
    if (!options.store || !(options.store instanceof Store)) {
      debug('[JSSDK]Store not provided, using default FileStore...');
      this.store = new FileStore(options.storeOptions);
    } else {
      this.store = options.store;
    }

    //clear the counter every 2 hour
    setInterval(
      () => (this.refreshedTimes = 0),
      options.clearCountInterval || 1000 * 7200
    );
  }

  /**
   * Check if token is expired starting from the modify date
   * @param modifyDate
   * @static
   * @return {boolean}
   */
  static isTokenExpired(modifyDate) {
    return utils.isExpired(modifyDate);
  }

  /**
   * Create NonceStr before generating the signature
   * @static
   * @return {string}
   */
  static createNonceStr() {
    return utils.nonceStr();
  }

  /**
   * Filter the signature for the client
   * @param {object} originalSignatureObj original signature information
   * @return {object} filtered signature object
   */
  filterSignature(originalSignatureObj) {
    if (!originalSignatureObj) {
      return {};
    }
    return {
      appId: this.wechatConfig.appId,
      timestamp: originalSignatureObj.timestamp,
      nonceStr: originalSignatureObj.nonceStr,
      signature: originalSignatureObj.signature,
      url: originalSignatureObj.url,
    };
  }

  /**
   * Remove hash from the url, wechat signature does not need it
   * @param {string} url original url
   * @static
   * @return {string}
   */
  static normalizeUrl(url) {
    const temp = urlParser.parse(url);
    const hashIndex = url.indexOf(temp.hash);
    //remove hash from url
    return hashIndex > 0 ? url.substring(0, hashIndex) : url;
  }

  /**
   * Generate the url signature with the provided info
   * @param {string} url current url
   * @param {string} accessToken
   * @param {string} ticket js ticket
   * @static
   * @returns {object} generated wechat signature info
   */
  static generateSignature(url, accessToken, ticket) {
    const ret = {
      jsapi_ticket: ticket,
      nonceStr: JSSDK.createNonceStr(),
      timestamp: utils.timestamp(),
      url: JSSDK.normalizeUrl(url),
    };
    const originalStr = utils.paramsToString(ret);
    ret.signature = utils.genSHA1(originalStr);
    ret.accessToken = accessToken;
    return ret;
  }

  /**
   * Need to verify before you are a wechat developer
   * @param {object} query url query sent by the wechat server to do the validation
   * @return {boolean}
   */
  verifySignature(query) {
    const keys = [
      this.wechatConfig.wechatToken,
      query['timestamp'],
      query['nonce'],
    ];
    let str = keys.sort().join('');
    str = utils.genSHA1(str);
    return str === query.signature;
  }

  /**
   * Send request to get wechat access token
   * @return {Promise}
   */
  getAccessToken() {
    const cfg = this.wechatConfig;
    return utils.getGlobalAccessToken(
      cfg.appId,
      cfg.appSecret,
      cfg.accessTokenUrl
    );
  }

  /**
   * Get wechat ticket with the accessToken
   * @param {string} accessToken token received from the @see getAccessToken above
   * @return {Promise}
   */
  getJsApiTicket(accessToken) {
    const params = {
      access_token: accessToken,
      type: 'jsapi',
    };
    return utils
      .sendWechatRequest(this.wechatConfig.ticketUrl, {
        query: params,
      })
      .then(data => data)
      .catch(reason => {
        debug('get ticket failed!');
        return Promise.reject(reason);
      });
  }

  /**
   * Update the global token or js_ticket, we should cache this to prevent requesting too often
   * @param {string} token
   * @param {string} ticket
   * @return {Promise} resolved with the updated globalToken object
   */
  updateAccessTokenOrTicketGlobally(token, ticket) {
    const info = { modifyDate: new Date() };
    token && (info.accessToken = token);
    ticket && (info.jsapi_ticket = ticket);
    return this.store.updateGlobalToken(info);
  }

  /**
   * Get new access token and ticket from wechat server, and update that to cache
   * @param {boolean=} force force update, by default it will only get at most 5 times within 2 hours,
   *        cause the wechat server limits the access_token requests number
   * @return {Promise}
   */
  getGlobalTokenAndTicket(force) {
    force || this.refreshedTimes++;
    /* istanbul ignore if  */
    if (!force && this.refreshedTimes > 5) {
      return Promise.reject(
        new Error('maximum manual refresh threshold reached!')
      );
    }
    let accessToken = '';
    return this.getAccessToken()
      .then(result => {
        accessToken = result.access_token;
        return accessToken;
      })
      .catch(reason => {
        debug('get new global token failed!');
        return Promise.reject(reason);
      })
      .then(receivedAccessToken => {
        return this.getJsApiTicket(receivedAccessToken);
      })
      .then(ticketResult => {
        return this.updateAccessTokenOrTicketGlobally(
          accessToken,
          ticketResult.ticket
        );
      })
      .catch(ticketReason => {
        debug('get new global ticket failed!');
        debug(ticketReason);
        return Promise.reject(ticketReason);
      });
  }

  /**
   * Get or generate global token info for signature generating process
   * @return {Promise}
   */
  prepareGlobalToken() {
    return this.store.getGlobalToken().then(globalToken => {
      if (
        !globalToken ||
        !globalToken.accessToken ||
        JSSDK.isTokenExpired(globalToken.modifyDate)
      ) {
        debug(
          'global access token was expired, getting new global access token and ticket now...'
        );
        return this.getGlobalTokenAndTicket(true);
      }
      debug('global ticket exists, use cached access token');
      return Promise.resolve(globalToken);
    });
  }

  /**
   * Save or update the signature
   * @param {object} info signature information to save
   * @return {Promise}
   */
  saveSignature(info) {
    const signature = Object.assign({}, info);
    signature.createDate = new Date();
    signature.modifyDate = signature.createDate;
    return this.store
      .isSignatureExisting(signature.url)
      .then(existing => {
        if (existing) {
          debug('wechat url signature existed, try updating the signature...');
          return this.updateSignature(signature.url, signature);
        }
        return this.store.saveSignature(signature.signatureName, signature);
      })
      .then(sig => {
        debug('create/update wechat signature finished');
        return Promise.resolve(sig);
      });
  }

  /**
   * Update the signature for existing url
   * @param {string} url signature of url need to update
   * @param {object} info update info need to be updated to the existing url signature info
   * @return {Promise}
   */
  updateSignature(url, info) {
    url = JSSDK.normalizeUrl(url);
    info.modifyDate = new Date();
    delete info.createDate;
    delete info.url; //prevent changing the original url
    delete info.signatureName; //prevent changing the original name
    return this.store.updateSignature(url, info).then(sig => {
      debug('update wechat signature finished');
      return Promise.resolve(sig);
    });
  }

  /**
   * Get the signature from cache or create a new one
   * @param {string} url
   * @param {boolean=} forceNewSignature if true, generate a new signature rather than getting from cache
   * @return {Promise}
   */
  getSignature(url, forceNewSignature) {
    url = JSSDK.normalizeUrl(url);
    return this.store.getSignature(url).then(signature => {
      if (
        !forceNewSignature &&
        signature &&
        !JSSDK.isTokenExpired(signature.modifyDate)
      ) {
        signature = this.filterSignature(signature);
        return Promise.resolve(signature);
      }
      return this.createSignature(url);
    });
  }

  /**
   * Create a new signature now, and save to store
   * @param {string} url signature will be created for the url
   * @return {Promise} resolved with filtered signature results
   */
  createSignature(url) {
    return this.prepareGlobalToken()
      .then(data => {
        const ret = JSSDK.generateSignature(
          url,
          data.accessToken,
          data.jsapi_ticket
        );
        ret.signatureName = ret.url;
        return this.saveSignature(ret);
      })
      .then(sig => this.filterSignature(sig));
  }

  /**
   * Just get url signature from cache
   * @param {string} url
   * @return {Promise} filtered signature info
   */
  getCachedSignature(url) {
    url = JSSDK.normalizeUrl(url);
    return this.store.getSignature(url).then(signature => {
      return Promise.resolve(this.filterSignature(signature));
    });
  }
}

module.exports = JSSDK;
