/* global Twitch */
import Service from 'ember-service';
import Mixin from 'ember-metal/mixin';
import injectService from 'ember-service/inject';
import computed from 'ember-computed';
import RSVP from 'rsvp';
import run from 'ember-runloop';
import $ from 'jquery';

import {
  TIMINGS as BITS_TIMINGS,
  MAX_BITS_MESSAGE_LENGTH,
  SEND_WITH_DELAY_MINIMUM,
  LOCALSTORAGE_KEYS as BITS_STORAGE_KEYS,
  MAX_BITS_EMOTE,
  CHEER_PREFIX
} from 'web-client/utilities/bits/constants-config';
import { parseTotalBits } from 'web-client/utilities/bits/parse-total-bits';
import { AD_ERROR_TYPES } from 'web-client/components/bits/watch-ad-error/component';
import {
  getHashtagFromMessage,
  isValidHashtagMessage
} from 'web-client/utilities/bits/cheer-message';

/*
  The following Bits-related mixins are created (as opposed to defining them in the controller directly) to:
    * Identify and give a name to groups of computed properties that are closely related to a single function
    * Allow for unit testing on these specific properties, until the functionality moves to a smaller component,
      where it can be tested as part of the component.

  They are not intended to be used elsewhere.
*/

export const AsyncErrorHandlerMixin = Mixin.create({
  _asyncErrorClearer: null,
  bitsAsyncErrorStatus: null,
  bitsAsyncErrorMessage: null,
  showAsyncError: computed('bitsAsyncErrorMessage', 'bitsAsyncErrorStatus', {
    get() {
      return !!this.get('bitsAsyncErrorStatus');
    },

    /*
      error must be either false(y) or an object:
      { status: 'insufficient_balance',
        message: 'You do not have enough Bits to send th` message' }
    */
    set(key, error) {
      run.cancel(this._asyncErrorClearer); // Cancel previous
      if (error) {
        this.set('bitsAsyncErrorMessage', error.message);
        this.set('bitsAsyncErrorStatus', error.status);

        this._asyncErrorClearer = run.later(() => {
          this.set('showAsyncError', false);
        }, BITS_TIMINGS.ERROR_DISPLAY_TIMEOUT);
      } else {
        this._clearAsyncError();
        this._asyncErrorClearer = null;
      }

      return !!this.get('bitsAsyncErrorStatus');
    }
  }),
  _clearAsyncError() {
    this.set('bitsAsyncErrorMessage', null);
    this.set('bitsAsyncErrorStatus', null);
  },
  willDestroy() {
    run.cancel(this._asyncErrorClearer);
    this._super(...arguments);
  }
});

/**
* The only type of bad attempt currently supported (i.e., the only type of
* bad attempt we provide feedback for) is when the user attempts to
* send cheer without a number, i.e. missingSendAmount.
*
* @class BadAttemptHandlerMixin
*/
export const BadAttemptHandlerMixin = Mixin.create({
  _badAttemptClearer: null,
  _isBadCheerAttempt: false,
  isBadCheerAttempt: computed({
    get() {
      return this._isBadCheerAttempt;
    },
    set(key, badAttemptMade) {
      run.cancel(this._badAttemptClearer);
      if (badAttemptMade) {
        this._badAttemptClearer = run.later(() => {
          this.set('isBadCheerAttempt', false);
        }, BITS_TIMINGS.ERROR_DISPLAY_TIMEOUT);
      } else {
        this._badAttemptClearer = null;
      }

      this._isBadCheerAttempt = badAttemptMade;
      return badAttemptMade;
    }
  }),
  willDestroy() {
    this._super(...arguments);
    run.cancel(this._badAttemptClearer);
  }
});

/* Warning: Mixin expects messageToSend and _cheer() */
export const DelayedSendHandlerMixin = Mixin.create({
  _delayedSendClearer: null,
  delayedBitsMessageToSend: '',
  delayedBitsSendAmount: 0,
  delayedBitsSend: computed('delayedBitsMessageToSend', {
    get() {
      return !!this.get('delayedBitsMessageToSend');
    },
    set(key, isDelayed) {
      run.cancel(this._delayedSendClearer);
      if (isDelayed) {
        this.set('delayedBitsMessageToSend', this.get('messageToSend'));
        this.set('delayedBitsSendAmount', this.get('totalBitsInMessage'));
        this._delayedSendClearer = run.later(() => {
          this._cheer(this.get('delayedBitsMessageToSend'));
          this.set('delayedBitsSend', false);
        }, BITS_TIMINGS.UNDO_PROMPT_DURATION);
      } else {
        this.set('delayedBitsMessageToSend', '');
        this.set('delayedBitsSendAmount', 0);
        this._delayedSendClearer = null;
      }

      return !!this.get('delayedBitsMessageToSend');
    }
  }),
  willDestroy() {
    this._super(...arguments);
    run.cancel(this._delayedSendClearer);
  }
});

export default Service.extend(AsyncErrorHandlerMixin, BadAttemptHandlerMixin, DelayedSendHandlerMixin,{
  globals: injectService(),
  session: injectService(),
  tracking: injectService(),
  badges: injectService(),

  bits: injectService(),
  bitsTags: injectService(),
  bitsAdsEnabled: injectService(),
  bitsEmotes: injectService(),
  bitsPinnedCheers: injectService(),

  channelId: '',
  channelName: '',
  channelDisplayName: '',

  isEnabled: false,
  isPromoEnabled: false,
  isSendingBits: false,
  isBuyingBits: false,

  isSenderIneligible: null,
  isRecipientIneligible: null,

  isFetchingPrices: false,
  isFetchingBits: false,
  isBalanceUnavailable: false,

  caretIndex: 0,
  startingCaretIndex: 0,

  _wasTooltipHidden: true,
  isTooltipActive: false,
  isHelperShown: false,

  needsToSeeFTUE: false,
  needsToSeeAdsFTUE: false,
  needsToSeeRecipientCanEnableFTUE: false,

  minimumBits: 1,
  minimumBitsEmote: 1,
  messageToSend: '',
  purchaseProducts: [],

  adReward: 0,
  isShowingAdReward: false,
  isShowingAdsForBitsError: false,
  adsForBitsErrorType: '',
  disableWatchAd: false,

  selectedAnimatedEmotePrefix: '',

  prefix: CHEER_PREFIX,

  init() {
    this._super(...arguments);
    this._onSuccessfulCheer = () => {};
    this._onFailedCheer = () => {};
    this._onAuthError = () => {};
    this._onTooltipShow = () => {};
    this._onAnimatedEmoteSelection = () => {};
  },

  setupService({ channelId, channelName, channelDisplayName, onSuccessfulCheer, onFailedCheer, onAuthError, onTooltipShow, onAnimatedEmoteSelection }) {
    this.set('channelId', channelId);
    this.set('channelName', channelName);
    this.set('channelDisplayName', channelDisplayName);

    this._onSuccessfulCheer = onSuccessfulCheer;
    this._onFailedCheer = onFailedCheer;
    this._onAuthError = onAuthError;
    this._onTooltipShow = onTooltipShow;
    this._onAnimatedEmoteSelection = onAnimatedEmoteSelection;

    let isEnabled = this.get('globals.isBitsEnabled');
    this.set('isEnabled', isEnabled);

    if (!isEnabled) {
      return;
    }

    this.get('bitsTags').setupService(this.get('channelId'));
    this._calcRecipientEligibility(this._getPromoKeys());
  },

  reset() {
    this._wasTooltipHidden = true;

    this.set('isRecipientIneligible', null);
    this.set('isSenderIneligible', null);
    this.set('minimumBits', 1);
    this.set('minimumBitsEmote', 1);
    this.set('needsToSeeFTUE', false);
    this.set('needsToSeeAdsFTUE', false);
    this.set('needsToSeeRecipientCanEnableFTUE', false);
    this.set('isTooltipActive', false);
    this.set('isHelperShown', false);
    this.set('messageToSend', '');
    this.set('prefix', CHEER_PREFIX);

    this.dismissBuyMenu();
    this._resetBitsAdReward();
    this._resetBitsAdError();
  },

  _getPromoKeys() {
    return {
      seenPromoKey: Twitch.storage.get(this.get('seenPromoKey')),
      seenAdsForBitsPromoKey: Twitch.storage.get(this.get('seenAdsForBitsPromoKey')),
      seenRecipientCanEnablePromoKey: Twitch.storage.get(this.get('seenRecipientCanEnablePromoKey'))
    };
  },

  notLoggedIn: computed.not('session.isAuthenticated'),
  bitsProducts: computed.readOnly('globals.bitsProducts'),
  balance: computed.readOnly('bits.balance'),
  channelTotal: computed.readOnly('bits.channelTotal'),
  hasZeroBits: computed.equal('balance', 0),
  enabledBadgeTiers: null,
  isTagEnabledChannel: computed.readOnly('bitsTags.isTagEnabledChannel'),
  allBitsTagsNames: computed.readOnly('bitsTags.allTagNames'),

  selectedAnimatedEmote: computed('selectedAnimatedEmotePrefix', function() {
    return this.get('bitsEmotes').getPrefixData(this.get('selectedAnimatedEmotePrefix'));
  }),

  ftueType: computed('isTagEnabledChannel', function() {
    return this.get('isTagEnabledChannel') ? 'evo' : 'default';
  }),

  // TODO: Namespacing localstorage based on user ID should happen at a higher level
  localStorageKeySuffix: computed('session.isAuthenticated', 'session.userData.id', function () {
    let isLoggedIn = this.get('session.isAuthenticated');
    let id = this.get('session.userData.id');

    return isLoggedIn ? `.${id}` : '';
  }),

  seenPromoKey: computed('isTagEnabledChannel', 'localStorageKeySuffix', function () {
    let prefix = this.get('isTagEnabledChannel') ? BITS_STORAGE_KEYS.SEEN_EVO_PROMO_PREFIX : BITS_STORAGE_KEYS.SEEN_PROMO_PREFIX;
    let suffix = this.get('localStorageKeySuffix');
    return `${prefix}${suffix}`;
  }),

  seenAdsForBitsPromoKey: computed('localStorageKeySuffix', function () {
    let prefix = BITS_STORAGE_KEYS.SEEN_ADS_PROMO_PREFIX;
    let suffix = this.get('localStorageKeySuffix');
    return `${prefix}${suffix}`;
  }),

  seenRecipientCanEnablePromoKey: computed('localStorageKeySuffix', function () {
    let prefix = BITS_STORAGE_KEYS.SEEN_ELIGIBLE_NOTIFICATION_PREFIX;
    let suffix = this.get('localStorageKeySuffix');
    return `${prefix}${suffix}`;
  }),

  isFetching: computed.or('isFetchingPrices', 'isFetchingBits'),
  isMaxMessageLengthExceeded: computed.gt('messageToSend.length', MAX_BITS_MESSAGE_LENGTH),

  isHashtagMessage: computed('messageToSend', function () {
    return isValidHashtagMessage(this.get('messageToSend'));
  }),

  selectedHashtag: computed('isHashtagMessage', 'messageToSend', function () {
    return this.get('isHashtagMessage') ? getHashtagFromMessage(this.get('messageToSend')) : '';
  }),
  selectedHashtagValue: computed('selectedHashtag', function () {
    return this.get('selectedHashtag') ? this.get('selectedHashtag').substring(1) : '';
  }),
  isSanctionedHashtagMessage: computed('isHashtagMessage', 'selectedHashtagValue', 'allBitsTagsNames', function () {
    if (!this.get('isHashtagMessage') || !this.get('selectedHashtagValue')) {
      return false;
    }
    return this.get('allBitsTagsNames').contains(this.get('selectedHashtagValue'));
  }),

  totalBitsInMessage: computed('bitsEmotes.isLoaded', 'messageToSend', function () {
    if (!this.get('bitsEmotes.isLoaded')) {
      return 0;
    }

    return parseTotalBits(this.get('messageToSend'), this.get('bitsEmotes.regexes'));
  }),

  isCheerMessage: computed('bitsEmotes.isLoaded', 'messageToSend', function () {
    if (!this.get('bitsEmotes.isLoaded')) {
      return false;
    }

    let isCheerMessage = this._isValidCheerMessage(this.get('messageToSend'));
    if (isCheerMessage) {
      if (this.get('needsToSeeFTUE')) {
        this.dismissFTUE();
      } else if(this.get('needsToSeeAdsFTUE')) {
        this.dismissAdsFTUE();
      }
    }

    return isCheerMessage;
  }),

  words: computed('messageToSend', function () {
    return this.get('messageToSend').split(/\s+/);
  }),

  validCheerWords: computed('bitsEmotes.isLoaded', 'words', function () {
    if (!this.get('bitsEmotes.isLoaded')) {
      return [];
    }

    return this.get('words').filter((word) =>
      this._matchRegexValid(word)).map((word) => {
      return {
        'word': word,
        'value': parseInt(word.match(/\d+/), 10)
      };
    });
  }),

  isSamePrefix: computed('bitsEmotes.isLoaded', 'words', function () {
    if (!this.get('bitsEmotes.isLoaded')) {
      return false;
    }

    let regexes = this.get('bitsEmotes.regexes');
    let words = this.get('words');
    let prefixCount = 0;

    regexes.forEach((regex) => {
      if (words.some((word) => word.match(regex.valid))) {
        prefixCount += 1;
      }
    });

    return prefixCount === 1 ? true : false;
  }),

  isCheerZero: computed('bitsEmotes.isLoaded', 'words', function () {
    if (!this.get('bitsEmotes.isLoaded')) {
      return false;
    }

    let regexes = this.get('bitsEmotes.regexes');
    return this.get('words').some((word) =>
      regexes.some((regex) => regex.zeroValue.test(word)));
  }),

  isCheerBelowMinimum: computed('isCheerMessage', 'totalBitsInMessage', 'minimumBits', function() {
    return this.get('isCheerMessage') && this.get('totalBitsInMessage') < this.get('minimumBits');
  }),

  numBitsToMinimum: computed('isCheerBelowMinimum', 'totalBitsInMessage', 'minimumBits', function() {
    if (!this.get('isCheerBelowMinimum')) {
      return 0;
    }

    return this.get('minimumBits') - this.get('totalBitsInMessage');
  }),

  isCheerAboveMax: computed('validCheerWords', function () {
    return this.get('validCheerWords').some((wordObj) => {
      return wordObj.value > MAX_BITS_EMOTE;
    });
  }),

  isCheerBelowMinEmote: computed('validCheerWords', 'minimumBitsEmote', function () {
    return this.get('validCheerWords').some((wordObj) => {
      return wordObj.value < this.get('minimumBitsEmote');
    });
  }),

  validCheerCount: computed('validCheerWords', function () {
    return this.get('validCheerWords').length;
  }),

  balanceExceededAmount: computed('totalBitsInMessage', 'balance', function () {
    let bitsBalance = this.get('balance');
    let totalBitsInMessage = this.get('totalBitsInMessage');

    if (bitsBalance === null || bitsBalance === undefined) {
      return 0;
    }
    return totalBitsInMessage - bitsBalance;
  }),
  isBalanceExceeded: computed.gt('balanceExceededAmount', 0),
  shouldShowBitsTotal: computed.gt('validCheerCount', 1),

  canBitsBeVisible: computed.or('isTooltipActive', 'isCheerMessage', 'needsToSeeFTUE', 'needsToSeeAdsFTUE', 'needsToSeeRecipientCanEnableFTUE', 'isSendingBits', 'isBuyingBits'),

  isTooltipHidden: computed('canBitsBeVisible', 'isEnabled', function () {
    if (!this.get('isEnabled')) {
      return true;
    }

    let previousWasTooltipHidden = this._wasTooltipHidden;
    let canBitsBeVisible = this.get('canBitsBeVisible');
    this._wasTooltipHidden = !canBitsBeVisible;

    // if the tooltip hidden state was changed and tooltip should be visible, the tooltip WAS opened, so invoke callback
    if (this._wasTooltipHidden !== previousWasTooltipHidden && canBitsBeVisible) {
      this._onTooltipOpened();
    }
    return !canBitsBeVisible;
  }),

  isOwnChannel: computed('channelName', 'session.userData.login', function () {
    return this.get('channelName') === this.get('session.userData.login');
  }),

  isUnsendableCheerMessage: computed.or('isSenderIneligible',
                                        'isRecipientIneligible',
                                        'isBalanceUnavailable',
                                        'isFetchingBits',
                                        'isFetchingPrices',
                                        'isSendingBits',
                                        'isBalanceExceeded',
                                        'isCheerBelowMinimum',
                                        'isCheerBelowMinEmote',
                                        'isCheerAboveMax',
                                        'isCheerZero',
                                        'isMaxMessageLengthExceeded',
                                        'delayedBitsMessageToSend'),

  isSendableCheerMessage: computed.not('isUnsendableCheerMessage'),

  hideInventory: computed.or('needsToSeeFTUE',
                             'needsToSeeAdsFTUE',
                             'needsToSeeRecipientCanEnableFTUE',
                             'notLoggedIn',
                             'isOwnChannel',
                             'isSenderIneligible',
                             'isRecipientIneligible',
                             'isBalanceUnavailable',
                             'delayedBitsMessageToSend',
                             'isFetching',
                             'isSendingBits',
                             'isBuyingBits',
                             'isShowingAdReward',
                             'isShowingAdsForBitsError'),

  showInventory: computed.not('hideInventory'),
  hasPurpleFooter: computed.or('delayedBitsMessageToSend', 'showInventory'),

  showMinBitsPinned: computed('bitsPinnedCheers.isEnabled', 'bitsPinnedCheers.channelMinimum', 'numBitsToMinimum', function () {
    return this.get('bitsPinnedCheers.isEnabled') && (this.get('bitsPinnedCheers.channelMinimum') > this.get('numBitsToMinimum'));
  }),

  numBitsToPinMinimum: computed('isCheerMessage', 'showMinBitsPinned', 'totalBitsInMessage', 'bitsPinnedCheers.channelMinimum', function() {
    if (this.get('isCheerMessage') && this.get('showMinBitsPinned') &&
      (this.get('totalBitsInMessage') < this.get('bitsPinnedCheers.channelMinimum'))) {

      return this.get('bitsPinnedCheers.channelMinimum') - this.get('totalBitsInMessage');
    }
    return 0;
  }),

  updateMessageToSend(messageToSend) {
    this.set('messageToSend', messageToSend);
  },

  completeFTUE() {
    this.dismissFTUE();
    this.set('isTooltipActive', true);
    this.openBuyMenu();
  },

  dismissFTUE() {
    Twitch.storage.set(this.get('seenPromoKey'), true);
    this.set('needsToSeeFTUE', false);
  },

  completeAdsFTUE() {
    this.dismissAdsFTUE();
    this.set('isTooltipActive', true);
    this.openBuyMenu();
  },

  dismissAdsFTUE() {
    Twitch.storage.set(this.get('seenAdsForBitsPromoKey'), true);
    this.set('needsToSeeAdsFTUE', false);
  },

  completeRecipientCanEnableFTUE() {
    window.open('http://link.twitch.tv/fgtgb');
    this.dismissRecipientCanEnableFTUE();
  },

  dismissRecipientCanEnableFTUE() {
    Twitch.storage.set(this.get('seenRecipientCanEnablePromoKey'), true);

    // hide both tooltip and helper
    this.set('isHelperShown', false);
    this.set('needsToSeeRecipientCanEnableFTUE', false);
  },

  showAdReward(amount) {
    this.set('isBuyingBits', false);
    this.set('isShowingAdReward', true);
    this.set('adReward', amount);
  },

  showAdUnknownError() {
    this._showAdError(AD_ERROR_TYPES.UNKNOWN);
  },
  showAdExitedEarly() {
    this._showAdError(AD_ERROR_TYPES.EXIT_EARLY);
  },
  showAdLimitReached() {
    this._showAdError(AD_ERROR_TYPES.LIMIT_REACHED);
  },
  showAdErrorAdblock() {
    this._showAdError(AD_ERROR_TYPES.ADBLOCK);
  },

  hideAdError() {
    this._resetBitsAdError();
    this.openBuyMenu();
  },

  appendToChatMessage(animatedEmote) {
    this._onAnimatedEmoteSelection(animatedEmote);
  },

  toggleTooltip() {
    if (this.get('needsToSeeFTUE')) {
      this.dismissFTUE();
    }
    if (this.get('needsToSeeAdsFTUE')) {
      this.dismissAdsFTUE();
    }
    if (this.get('needsToSeeRecipientCanEnableFTUE')) {
      this.dismissRecipientCanEnableFTUE();
    }

    if (!this.get('isTooltipActive')) {
      this.set('startingCaretIndex', this.get('caretIndex'));
    }

    this.dismissBuyMenu();
    this._resetBitsAdReward();
    this._resetBitsAdError();
    this.toggleProperty('isTooltipActive');

    let userName = this.get('session.userData.login');
    let balance = this.get('balance');
    let actionName = this.get('isTooltipActive') ? 'menu_open' : 'menu_close';
    this.get('tracking').trackBitsCardInteractions(actionName, userName, balance);

    $('.js-chat-container .chat_text_input').focus();
  },

  dismissTooltip() {
    this.set('isTooltipActive', false);
    this.set('delayedBitsSend', false);
    this.set('isBadCheerAttempt', false);
    this.set('showAsyncError', false);

    this.dismissBuyMenu();
    this._resetBitsAdReward();
    this._resetBitsAdError();
  },

  dismissBuyMenu() {
    this.set('isBuyingBits', false);
  },

  reloadTooltip() {
    this.dismissTooltip();
    this.set('isTooltipActive', true);
  },

  forceLoadBalance() {
    this.set('bits.balance', null);
    this._loadBalance();
  },

  openBuyMenu() {
    this.set('isFetchingPrices', true);

    let bitsProducts = this.get('bitsProducts');
    let asins = bitsProducts.mapBy('asin');
    if (this.get('isPromoEnabled')) {
      asins.push(this.get('bits.promoProduct'));
    }

    this.get('bits').getPrices(asins).then((prices) => {
      if (this.isDestroyed) { return; }
      let products = this._createBitsProducts(bitsProducts, prices);
      asins = products.map((product) => { return product.asin; });
      this.get('tracking').trackBitsCardInteractions('buy_main', this.get('session.userData.login'), this.get('balance'), null, null, asins.sort().join());
      this.set('purchaseProducts', products);
      this.set('isBuyingBits', true);
    }).catch(e => {
      if (e.status === 401 && !this.isDestroyed) {
        // Silence known error, unauthenticated
        this._onAuthError();
      } else {
        throw e;
      }
    }).finally(() => {
      if (this.isDestroyed) { return; }
      this.set('isFetchingPrices', false);
    });
  },

  setCaretIndex(index) {
    if (this.get('startingCaretIndex') > index) {
      this.set('isTooltipActive', false);
    }
    this.set('caretIndex', index);
  },

  sendMessage(messageToSend) {
    if (this.get('isSendableCheerMessage')) {
      this.set('isBuyingBits', false);
      if (this.get('totalBitsInMessage') >= SEND_WITH_DELAY_MINIMUM) {
        this.set('delayedBitsSend', true);
      } else {
        this._cheer(messageToSend);
      }
    }
  },

  forceSendMessage() {
    this._cheer(this.get('delayedBitsMessageToSend'));
    this.set('delayedBitsSend', false);
  },

  cancelDelayedSend() {
    this.set('delayedBitsSend', false);
  },

  _cheer(messageToSend) {
    this.set('isSendingBits', true);

    this.get('bits').sendBits(this.get('channelId'), messageToSend).then(() => {
      if (this.isDestroyed) { return; }
      this._onSuccessfulCheer();
    }, (xhr) => {
      if (this.isDestroyed) { return; }
      this._cheerRejectionHandler(xhr);
    }).finally(() => {
      if (this.isDestroyed) { return; }
      this.set('isSendingBits', false);
    });
  },

  _createBitsProducts(bitsProducts, prices) {
    let lookup = {};
    prices.forEach(price => {
      lookup[price.asin] = `\$${price.us_price}`;
    });

    let filteredProducts = bitsProducts.filter((product) => { return lookup[product.asin]; });
    return filteredProducts.map((product) => {
      let promo = false;
      let asin = product.asin;

      if (this.get('isPromoEnabled') && product.amount === this.get('bits.promoAmount') && this._bitsBundles[this.get('bits.promoProduct')]) {
        promo = true;
        asin = this.get('bits.promoProduct');
      }

      return {
        amount: product.amount,
        asin: asin,
        price: lookup[product.asin],
        discount: product.discount,
        show: product.discount > 0,
        promo: promo
      };
    });
  },

  _onTooltipOpened() {
    let isSenderIneligible = this.get('isSenderIneligible');
    if (isSenderIneligible === null) {
      let userId = this.get('session.userData.id');
      if (userId) {
        this.get('bits').loadSenderEligibility(userId).then(isEligible => {
          if (this.isDestroyed) { return; }
          this.set('isSenderIneligible', !isEligible);
        });
      }
    } else if (isSenderIneligible) {
      return;
    }

    run.throttle(this, this._loadBalance, BITS_TIMINGS.GET_BALANCE_RATE_LIMIT);
    this._onTooltipShow();
  },

  _calcRecipientEligibility({ seenPromoKey, seenAdsForBitsPromoKey, seenRecipientCanEnablePromoKey }) {
    if (this.get('isRecipientIneligible')) {
      this.set('needsToSeeFTUE', false);
      this.set('needsToSeeAdsFTUE', false);
      this.set('isHelperShown', false);
      return;
    }

    let eligibilityPromise = this.get('bits').loadRecipientEligibility(this.get('channelId'));
    let bitsForAdsEnabledPromise = this.get('bitsAdsEnabled').isEnabled(this.get('channelName'));

    return RSVP.all([eligibilityPromise, bitsForAdsEnabledPromise]).then(([eligibilityResponse, isBitsForAdsEnabled]) => {
      if (this.isDestroyed) { return; }

      // initialize pinned cheers and hashtags (not dependent on being logged in)
      this.get('bitsPinnedCheers').setupService({
        channelId: this.get('channelId'),
        channelName: this.get('channelName'),
        isChannelEnabled: eligibilityResponse.pinnedCheersEnabled,
        channelMinimum: eligibilityResponse.pinnedCheersMinimum,
        channelCheerTimeout: eligibilityResponse.pinnedCheersTimeout
      });

      if (this.get('hashtags') && this.get('isTagEnabledChannel')) {
        this.get('bitsTags').setTags(this.get('hashtags'));
      }

      // the following logic only applies to authenticated users
      if (this.get('notLoggedIn')) {
        return;
      }

      let isRecipientIneligible = !eligibilityResponse.eligible;
      let needsToSeeFTUE = !seenPromoKey && !isRecipientIneligible;

      this.set('needsToSeeFTUE', needsToSeeFTUE);
      this.set('isRecipientIneligible', isRecipientIneligible);
      this.set('minimumBits', eligibilityResponse.minBits);
      this.set('minimumBitsEmote', eligibilityResponse.minBitsEmote);
      this.set('enabledBadgeTiers', eligibilityResponse.enabledBadgeTiers);
      this.set('hashtags', eligibilityResponse.hashtags);

      // this can get overridden if recipient eligible FTUE needs to be shown
      this.set('isHelperShown', !isRecipientIneligible);

      if (!isRecipientIneligible && isBitsForAdsEnabled && !needsToSeeFTUE && !seenAdsForBitsPromoKey) {
        this.get('bits').bitsAdsAvailable(this.get('session.userData.id')).then(response => {
          if (this.isDestroyed) { return; }
          let needsToSeeAdsFTUE = response && response.error === undefined;
          if (!needsToSeeAdsFTUE) {
            this._trackAdsPromoEvent('suppressed');
          }

          this.set('needsToSeeAdsFTUE', needsToSeeAdsFTUE);
        }).catch(() => {
          if (this.isDestroyed) { return; }
          this._trackAdsPromoEvent('error');
          this.set('needsToSeeAdsFTUE',  true);
        });
      } else {
        this.set('needsToSeeAdsFTUE',  false);
      }

      // if partnered recipient is ineligible and they're in their own chat room, check if they can enable bits
      let needToCheckRecipientCanEnable = !seenRecipientCanEnablePromoKey && isRecipientIneligible && this.get('isOwnChannel') && this.get('session.userData.is_partner');

      if (needToCheckRecipientCanEnable) {
        this.set('needsToSeeRecipientCanEnableFTUE', true);
        this.set('isHelperShown', true);
      } else {
        this.set('needsToSeeRecipientCanEnableFTUE', false);
      }
    }).catch(() => {
      if (this.isDestroyed) { return; }

      this.set('isRecipientIneligible', true);
      this.set('needsToSeeFTUE', false);
      this.set('needsToSeeAdsFTUE', false);
      this.set('isHelperShown', false);
    });
  },

  _loadBalance() {
    let bitsBalance = this.get('balance');
    if (bitsBalance === null || bitsBalance === undefined) {
      this.set('isFetchingBits', true);
    }

    this._loadingBalancePromise = this.get('bits').loadBalance(this.get('channelId'));
    this._getGlobalBadgesPromise = this.get('badges').requestGlobalBadges('1');

    RSVP.all([this._loadingBalancePromise, this._getGlobalBadgesPromise]).then(([loadingBalanceResponse, getGlobalBadgesResponse]) => {
      if (this.isDestroyed) { return; }
      this.set('isBalanceUnavailable', false);
      this._bitsBundles = loadingBalanceResponse.bundles;
      this.set('highestBitsBadge', loadingBalanceResponse.highestEntitledBadge);
      this.set('globalBadges', getGlobalBadgesResponse);
    }).catch(() => {
      if (this.isDestroyed) { return; }
      this.set('isBalanceUnavailable', true);
    }).finally(() => {
      if (this.isDestroyed) { return; }
      this.set('isFetchingBits', false);
      this._loadingBalancePromise = null;
      this._getGlobalBadgesPromise = null;
    });
  },

  _resetBitsAdReward() {
    this.set('isShowingAdReward', false);
    this.set('adReward', 0);
  },

  _resetBitsAdError() {
    this.set('isShowingAdsForBitsError', false);
    this.set('adsForBitsErrorType', '');
  },

  _showAdError(type) {
    this.set('isBuyingBits', false);
    this.set('isShowingAdsForBitsError', true);
    this.set('adsForBitsErrorType', type);

    this._resetBitsAdReward();
  },

  _cheerRejectionHandler(xhr) {
    if (xhr.status === 401) {
      this._onAuthError();
      this._onFailedCheer();
    } else {
      let {status, message} = xhr.responseJSON;
      this.set('showAsyncError', {status, message});

      // Attempt to correct state
      if (status === 'insufficient_balance') {
        this.get('bits').loadBalance();
      }
    }
  },

  _trackAdsPromoEvent(action) {
    let session = this.get('session');
    let login = '';
    let loginId = '';

    if (session.isAuthenticated && session.userData) {
      login = session.userData.name;
      loginId = session.userData.id;
    }

    let eventData = {
      client_time: new Date().getTime(),
      device_id: Twitch.idsForMixpanel.getOrCreateUniqueId(),
      login: login,
      login_id: loginId,
      channel: this.get('channelName'),
      channel_id: this.get('channelId'),
      action: action
    };

    this.get('tracking').trackEvent({
      event: 'bits_user_onboarding_ads',
      data: eventData
    });
  },

  _hasUnsupportedChatCommand(words = ['']) {
    let command = words[0];

    if (command[0] !== '/') {
      return false;
    }

    // Match substrings of /me, in case user is still typing
    return command !== '/me'.slice(0, command.length);
  },

  _isValidCheerMessage(message = '') {
    let words = message.split(/\s+/);
    let isCheerMessage = !this._hasUnsupportedChatCommand(words) &&
      words.some((word) => this._matchRegexValid(word));

    return isCheerMessage;
  },

  _matchRegexValid(word) {
    let regexes = this.get('bitsEmotes.regexes');
    for (let i = 0; i < regexes.length; i++) {
      if (word.match(regexes[i].valid)) {
        this.set('prefix', regexes[i].prefix);
        return true;
      }
    }

    return false;
  }
});
