/*
 * NOTICE:  This file is owned by the chat and messaging team (#chat-and-messaging on slack)
 * If you make changes to this file or any of its chat-affecting dependencies, you must do the following two things:
 * 1)  Test chat and whispers in a staging environment
 * 2)  Get a PR approved by a member of the chat and messaging team
 *
 * Thanks!
 */

/* globals Twitch */

import config from 'web-client/config/environment';
import { Stats } from 'client-event-reporter';
import ENV from '../config/environment';
import injectService from 'ember-service/inject';
import Service from 'ember-service';
import Evented from 'ember-evented';
import on from 'ember-evented/on';
import $ from 'jquery';
import run from 'ember-runloop';
import { assert } from 'ember-metal/utils';

export default Service.extend(Evented, {
  pubsub: injectService('whispers-pubsub'),
  imApi: injectService('im-api'),

  setupVariables: on('init', function() {
    // If a bunch of messages are being dropped server-side then there
    // could be a memory leak
    this.set('nonces', {});

    this.set('stats', Stats.getInstance(ENV.environment, "web-client.services.whispers"));
  }),

  setupService() {
    let pubsub = this.get("pubsub");
    return pubsub.setupService().then(() => {
      if (this.isDestroyed) { return; }
      pubsub.on("whisper", (message) => {
        if (this.isDestroyed) { return; }
        let nonce = message.nonce;
        if (nonce) {
          let nonces = this.get("nonces");
          let isLocallyRendered = nonces.hasOwnProperty(nonce);
          if (isLocallyRendered) {
            nonces[nonce].receivedOnPubsub = true;
            return;
          }
        }

        this.trigger("whisper", message);
      });

      pubsub.on("thread", (thread) => { this.trigger("thread", thread); });
      pubsub.on("threads",  (threads) => { this.trigger("threads", threads); });
      pubsub.on("reconnecting", () => { this.trigger("reconnecting"); });
    });
  },

  publishMessage(message, options) {
    options = options || {};
    let toLogin = options.toLogin;
    let nonce = options.nonce;

    if (!toLogin) { throw new Error("Missing options.toLogin"); }
    if (!nonce) { throw new Error("Missing options.nonce"); }

    // Keep track of nonces for de-duping rendering
    this.get("nonces")[nonce] = { receivedOnPubsub: false };

    // Send to im-api
    let publishOptions = { nonce: nonce };

    if (options.toId) {
      publishOptions.toId = options.toId;
    } else {
      publishOptions.toLogin = toLogin;
    }

    let publishPromise = this.get("imApi").publishMessage(message, publishOptions);

    this._recordWhisperSentReflection(publishPromise, nonce);

    if (!options.disableRenderMetric) {
      let destination = options.isEmbedChat ? 'popout' : 'thread';
      this._scheduleRecordLocalRenderMetric(nonce, destination);
    }

    return publishPromise;
  },

  triggerChatWhisper(message) {
    assert(`Missing body`, !!message.body);
    assert(`Missing toLogin`, !!message.toLogin);
    assert(`Missing nonce`, !!message.nonce);

    this.trigger('chat_whisper', message);
  },

  // Track if the request is ever made and if there is a pubsub reflection message
  _recordWhisperSentReflection(promise, nonce) {
    let succeeded = false;
    promise.then(() => {
      if (this.isDestroyed) { return; }
      succeeded = true;
    });

    this.runTask(() => {
      let nonces = this.get("nonces");
      let stats = this.get("stats");
      if (nonces[nonce].receivedOnPubsub) {
        delete nonces[nonce];

        stats.logCounter("whisper-sent-ack", 1);
        if (succeeded) {
          stats.logCounter("whisper-sent-ack-expected", 1);
        }
      } else {
        stats.logCounter("whisper-sent-ack", 0);
        if (succeeded) {
          stats.logCounter("whisper-sent-ack-expected", 0);
        }
      }
    }, config.delay.whisper.stats);
  },

  // This exists to be mocked in tests
  //
  //   "Assertion Failed: You have turned on testing mode, which disabled the run-loop's autorun.
  //    You will need to wrap any code with asynchronous side-effects in a run"
  _scheduleRecordLocalRenderMetric(nonce, destination) {
    // Rendering is not synchronous so check after rendering
    run.schedule("afterRender", () => {
      this._recordLocalRenderMetric(nonce, destination);
    });
  },

  // Poll for when the whisper message is rendered in the DOM. The
  // rendering can happen very fast if the thread is already open. The
  // rendering is slow if the thread is closed; this happens when
  // a whisper is sent via `/w` in the chat window
  _recordLocalRenderMetric(nonce, destination, tries=0) {
    if (tries > 8) {
      this.get("stats").logCounter("local-render", 0);
      this.get("stats").logCounter(`local-render.${destination}`, 0);
    } else {
      let isRendered = this._isMessageRendered(nonce);
      if (isRendered) {
        this.get("stats").logCounter("local-render", 1);
        this.get("stats").logCounter(`local-render.${destination}`, 1);
      } else {
        this.runTask(() => {
          this._recordLocalRenderMetric(nonce, destination, tries + 1);
        }, config.delay.whisper.renderStats);
      }
    }
  },

  _isMessageRendered(nonce) {
    // Checks the conversation windows and chat popout
    return $(".conversation-chat-lines, .chat-lines").find(`[data-nonce="${nonce}"]`).length > 0;
  },

  _twitchUserLogin() {
    return Twitch.user.login();
  }
});
