import PubsubDriver from 'pubsub-js-client/PubsubDriver';
import injectService from 'ember-service/inject';
import RSVP from 'rsvp';
import Service from 'ember-service';
import Ember from 'ember';
import Evented from 'ember-evented';
import { A as emberA } from 'ember-array/utils';
import run from 'ember-runloop';
import computed from 'ember-computed';

const { Logger } = Ember;

const PUBSUB_TOPIC_PREFIX = "onsite-notifications";

export const PUBSUB_CREATE_NOTIFICATION = "create-notification";
export const PUBSUB_UPDATE_NOTIFICATION = "update-notification";
export const PUBSUB_READ_NOTIFICATIONS = "read-notifications";
export const PUBSUB_DELETE_NOTIFICATION = "delete-notification";
export const PUBSUB_CONNECT_TIMEOUT = 3000; // This is the TP90 of WebSocket connect timings on the "PubSub - Client Stats" dashboard

const MAX_UNREAD_IDS = 100;

const NEW_NOTIFICATION_EVENT = "_new-notification";

const API_VERSION = 5;

export default Service.extend(Evented, {
  api: injectService(),
  session: injectService(),
  store: injectService(),

  hasViewedCenter: false,
  hasMorePages: true,
  loadingMore: false,

  connectTimeout: PUBSUB_CONNECT_TIMEOUT,

  isEmpty: computed('hasMorePages', 'notifications.[]', function() {
    return !this.get('hasMorePages') && this.get('notifications').length === 0;
  }),

  userId: computed.readOnly('session.userData.id'),

  init() {
    this._super(...arguments);
    this.set('notifications', emberA());
    this.set('summary', null);
  },

  willDestroy() {
    this._super(...arguments);
    this._unbindPubsub();
  },

  setupService() {
    this.set('hasViewedCenter', false);
    this._bindPubsub();
  },

  getSummary() {
    return this.get('store').findRecord('onsite-notification-summary', 'user');
  },

  _queryNotifications(cursor) {
    let params = {};
    if (cursor) {
      params.cursor = cursor;
    }

    return this.get('store').query('onsite-notification', params);
  },

  loadMoreNotifications() {
    if (!this.get('hasMorePages')) {
      return RSVP.resolve();
    }

    // do not allow parallel loads
    let promise = this.get('_loadingMorePromise');
    if (promise) {
      return promise;
    }

    this.set('loadingMore', true);
    let cursor = this.get('_lastCursor');

    promise = this._queryNotifications(cursor).then((notifications) => {
      if (this.isDestroyed) { return; }
      let meta = notifications.get('meta');
      this.set('_lastCursor', meta.cursor);
      this.set('hasMorePages', !!meta.cursor);

      this.get('notifications').pushObjects(notifications.toArray());
    }).finally(() => {
      if (this.isDestroyed) { return; }
      this.set('_loadingMorePromise', null);
      this.set('loadingMore', false);
    });

    this.set('_loadingMorePromise', promise);

    return promise;
  },

  dismissNotification(notification) {
    this._removeNotification(notification);
    let url = `users/${this.get('userId')}/notifications/onsite/${notification.get('id')}`;
    return this.get("api").authRequest("delete", url, null, { version: API_VERSION });
  },

  _removeNotification(notification) {
    this.get('notifications').removeObject(notification);
  },

  markAllRead() {
    let notifications = this.get('notifications');
    if (notifications.length === 0) {
      return;
    }

    let unreadNotifications = [];
    notifications.forEach(notification => {
      if (!notification.get('read')) {
        unreadNotifications.push(notification);
      }
    });

    if (unreadNotifications.length === 0) {
      return;
    }

    for (let i = 0; i < unreadNotifications.length; i += MAX_UNREAD_IDS) {
      let chunk = unreadNotifications.slice(i, i + MAX_UNREAD_IDS);
      this.setNotificationsRead(chunk);
    }
  },

  setNotificationsRead(notifications) {
    if (!notifications) {
      throw new Error("Missing notifications");
    }

    if (notifications.length === 0) {
      throw new Error("Must have more than 0 notifications");
    }

    if (notifications.length > 100) {
      throw new Error("Must have less than 100 notifications");
    }

    let notificationIds = notifications.map(n => n.get('id'));

    let requestParams = {
      notification_ids: notificationIds
    };

    // Optimistically mark as read
    notifications.forEach((notif) => {
      // Use `pushPayload` to update the ember data model, so the
      // model is not marked as dirty. If it's dirty, then subsequent
      // `pushPayload` calls via pubsub will fail
      this.get('store').pushPayload('onsite-notification', {
        id: notif.get('id'),
        read: true
      });
    });

    let url = `users/${this.get('userId')}/notifications/onsite/read`;
    return this.get("api").authRequest("put", url, requestParams, { version: API_VERSION });
  },

  setNotificationsCenterViewed() {
    let url = `users/${this.get('userId')}/notifications/onsite/viewed`;
    let requestParams = null;

    let summary = this.get('summary');
    if (summary) {
      // Use `pushPayload` to update the ember data model, so the
      // model is not marked as dirty. If it's dirty, then subsequent
      // `pushPayload` calls via pubsub will fail
      this.get('store').pushPayload('onsite-notification-summary', {
        id: summary.get('id'),
        unseen_view_count: 0
      });
    }

    return this.get("api").authRequest("put", url, requestParams, { version: API_VERSION });
  },

  //
  // Pubsub
  //

  _bindPubsub() {
    this.get('session').getCurrentUser().then((userData) => {
      let messageHandler = this.get('_messageHandler');
      if (messageHandler) {
        return;
      }

      let pubsub = this._pubsub();
      let topic = this._pubsubTopic();
      messageHandler = run.bind(this, this._onPubsubMessage);
      // keep track of this function for unbinding
      this.set('_messageHandler', messageHandler);

      // Attempt to connect to pubsub and then fetch the summary to handle
      // race conditions.
      //
      // Example: Onboarding notification is sent right after `getSummary()`. If we do not
      // wait for pubsub to connect, then the onboarding notification could be dropped.
      let pubsubPromise = new RSVP.Promise((resolve) => {
        this.runTask(() => {
          resolve();
        }, this.get('connectTimeout'));

        pubsub.Listen({
          topic: topic,
          auth: userData.chat_oauth_token,
          message: messageHandler,
          success: resolve,
          failure: (err) => {
            Logger.error("Failed to bind onsite notifications pubsub", topic, err);
            resolve();
          }
        });
      });

      pubsubPromise.then(() => {
        this.getSummary().then((summary) => {
          // Check if the summary has already been set by a pubsub message
          if (this.isDestroyed || this.get('summary')) { return; }
          this.set('summary', summary);
        });
      });
    });
  },

  _unbindPubsub() {
    let messageHandler = this.get('_messageHandler', messageHandler);
    if (!messageHandler) {
      return;
    }

    let pubsub = this._pubsub();
    let topic = this._pubsubTopic();

    pubsub.Unlisten({
      topic: topic,
      message: messageHandler,
      success: () => {},
      failure: (err) => {
        Logger.error("Failed to unbind onsite notifications pubsub", topic, err);
      }
    });
  },

  _handleCreatedOrUpdatedNotification(notificationPayload, persistent) {
    this.get('store').pushPayload('onsite-notification', notificationPayload);
    let notification = this.get('store').peekRecord('onsite-notification', notificationPayload.id);

    this.trigger(NEW_NOTIFICATION_EVENT, notification);

    // If the center has been opened, then prepend the notification, otherwise the
    // notification will be loaded when the center is opened
    if (this.get('hasViewedCenter')) {
      if (!persistent) {
        return;
      }

      let notifications = this.get('notifications');
      notifications.removeObject(notification); // handle duplicate aggregates
      notifications.unshiftObject(notification); // prepend
    }
  },

  bindNewNotification(context, handler) {
    this.on(NEW_NOTIFICATION_EVENT, context, handler);
  },

  unbindNewNotification(context, handler) {
    this.off(NEW_NOTIFICATION_EVENT, context, handler);
  },

  _handleReadNotifications(ids) {
    ids.forEach((id) => {
      let notification = this.get('store').peekRecord('onsite-notification', id);
      if (notification) {
        this.get('store').pushPayload('onsite-notification', {
          id: id,
          read: true
        });
      }
    });
  },

  _handleDeleteNotification(id) {
    let notification = this.get('store').peekRecord('onsite-notification', id);
    if (notification) {
      this._removeNotification(notification);
    }
  },

  _onPubsubMessage(payload) {
    if (this.isDestroyed) { return; }

    let response = JSON.parse(payload);
    let data = response.data;

    if (data.summary) {
      // Update the unseen view count
      this.get('store').pushPayload('onsite-notification-summary', data.summary);

      // Always overwrite the GET summary API call
      let summary = this.get('store').peekRecord('onsite-notification-summary', 'user');
      this.set('summary', summary);
    }

    switch (response.type) {
      case PUBSUB_CREATE_NOTIFICATION:
      case PUBSUB_UPDATE_NOTIFICATION:
        this._handleCreatedOrUpdatedNotification(data.notification, data.persistent);
        break;
      case PUBSUB_READ_NOTIFICATIONS:
        this._handleReadNotifications(data.notification_ids);
        break;
      case PUBSUB_DELETE_NOTIFICATION:
        this._handleDeleteNotification(data.notification_id);
        break;
    }
  },

  _pubsubTopic() {
    return `${PUBSUB_TOPIC_PREFIX}.${this.get('userId')}`;
  },

  _pubsub() {
    return PubsubDriver.getInstance("production");
  }
});
