/* globals Twitch, moment */
import Service from 'ember-service';
import RSVP from 'rsvp';
import set from 'ember-metal/set';
import injectService from 'ember-service/inject';
import { A as emberA } from 'ember-array/utils';
import EmberObject from 'ember-object';
import { assign } from 'ember-platform';
import PubsubDriver from 'pubsub-js-client/PubsubDriver';
import { mergeReactions } from 'web-client/utilities/reactions';

const URL_REGEX = /(?:game:(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*))|(?:https?:\/\/)?(?:[-a-zA-Z0-9@:%_\+~#=]+\.)+[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*)/g;

export const RECENT_FEED_ID = '_';
export const NEWSFEED_ID = 'newsfeed';

export default Service.extend({
  globals: injectService(),
  store: injectService(),
  api: injectService(),
  emotes: injectService('user-emotes'),
  session: injectService(),
  paginator: injectService(),
  tracking: injectService(),

  pubsubFeeds: null,
  postCreationInFlight: false,
  meAsFeedUser: null,
  urlRegex: URL_REGEX,
  _hasNewsFeed: null,

  init() {
    this._super();
    this.set('pubsubFeeds', []);
    this.loadMeAsFeedUser();
    this.initializeDriver();
    this._feedPostPromises = {};
  },

  hasNewsFeed() {
    let hasNewsFeed = this.get('_hasNewsFeed');
    let { isAuthenticated } = this.get('session');

    if (!isAuthenticated) {
      return RSVP.resolve(false);
    }

    if (hasNewsFeed !== null) {
      return RSVP.resolve(hasNewsFeed);
    }

    return this.checkFeedBeta().then(userHasNewsFeed => {
      this.set('_hasNewsFeed', userHasNewsFeed);
      return userHasNewsFeed;
    });
  },

  checkFeedBeta() {
  // Requesting feed with limit=-1 to determine membership in newsfeed beta
    return this.get('api')
      .authRequest('get', 'feed/posts', {limit: -1}, {version: 4})
      .then(resp => {
        return resp._disabled === false;
      }, () => {
        return false;
      });
  },

  toggleChannelFeed(setChannelFeedEnabled, userLogin) {
    let url = `/kraken/channels/${userLogin}`;
    let data = {
      channel : {
        "channel_feed_enabled": setChannelFeedEnabled
      }
    };

    return this.get('api').authRequest('put', url, data);
  },

  getRecentFeed() {
    return this.getFeedForUser(RECENT_FEED_ID);
  },

  loadMoreFeed(feed) {
    if (feed && feed.get('cursor')) {
      return feed.get('page').fetchNextPage();
    }

    return RSVP.resolve(feed);
  },

  loadMoreForRecentFeed(feed) {
    return this.loadMoreFeed(feed, RECENT_FEED_ID);
  },

  getNewsfeed() {
    return this.getFeedForUser(NEWSFEED_ID);
  },

  getFeedForUser(id) {
    let feedPostPromise = this._feedPostPromises[id];
    if (feedPostPromise) {
      return feedPostPromise;
    }

    let store = this.get('store');
    let feed = store.peekRecord('feed', id);
    if (!feed) {
      feed = store.createRecord('feed', { id });
    } else if (feed.get('page')) {
      return RSVP.resolve(feed);
    }

    let page = this.get('paginator').paginatorFor({
      model: feed,
      relationshipName: 'allPosts',
      pageFactory: 'feed-post'
    });
    feed.set('page', page);

    let promise = page.fetchNextPage().then(() => {
      return feed;
    }, (err) => {
      // Status 403 implies the user has not opted into channel feed
      if (err.status === 403) {
        store.deleteRecord(feed);
      }
      throw err;
    }).finally(() => {
      delete this._feedPostPromises[id];
    });

    // On the feeds backend we load the 10 most recent posts and filter out any
    // deleted posts. If the last 10 posts were all deleted then this response
    // will include a cursor but will not include any posts. In this scenario
    // fetch more posts from the backend until we either have at least one post
    // or the cursor is empty.
    promise.then(function ensureSomePostsLoaded() {
      if (feed.get('cursor') && !feed.get('posts').length) {
        return page.fetchNextPage()
            .then(ensureSomePostsLoaded);
      }
      return RSVP.resolve(feed);
    });

    this._feedPostPromises[id] = promise;
    return promise;
  },

  loadMoreForChannelFeed(login, feed) {
    return this.loadMoreFeed(feed, login);
  },

  loadMoreComments(postId) {
    let store = this.get('store');
    let post = store.peekRecord('post', postId);
    let channelName = post.get('user.login');
    let cursor = post.get('commentsCursor');

    if (cursor) {
      return store.query('comment', {type : 'queryComments', channelName, postId, cursor}).then((comments) => {
        if (this.isDestroyed) { return; }
        post.get('comments').pushObjects(comments);
        post.set('commentsCursor', comments.get('meta.cursor'));
      });
    }
  },

  getPost(postId, login) {
    let store = this.get('store');
    // For requesting a single post by id and login.
    let post = store.peekRecord('post', postId);
    if (!post) {
      return store.queryRecord(
        'post',
        {
          feed: login,
          id: postId
        }
      );
    }
    return RSVP.resolve(post);
  },

  setReactionForMessage(message, emote) {
    let type = message.constructor.modelName;
    let me = this.get('meAsFeedUser');
    let reactions = message.get('reactions');

    let messageReaction = reactions.findBy('emoteId', emote.id);
    if (!messageReaction) {
      messageReaction = this._addReactionToMessage(message, emote);
    }

    let isEndorsing = messageReaction.userIds[0] ? false : true;

    // Do the local mutation to the model
    if (isEndorsing) {
      set(messageReaction, 'count', messageReaction.count + 1);
      set(messageReaction, 'userIds', [me.get('id')]);
    } else {
      set(messageReaction, 'count', messageReaction.count - 1 || null);
      set(messageReaction, 'userIds', emberA([]));
    }

    if (messageReaction.count === null) {
      reactions.removeObject(messageReaction);
    }

    let url;
    let payload = {"emote_id": emote.id};

    if (type === 'post') {
      let user = message.get('user.login');
      let postId = message.get('id');
      url = `feed/${user}/posts/${postId}/reactions`;
    } else {
      let user = message.get('post.user.login');
      let commentId = message.get('id');
      let postId = message.get('post.id');
      url = `feed/${user}/posts/${postId}/comments/${commentId}/reactions`;
    }

    if (isEndorsing) {
      return this.get('api').authRequest('post', url, payload);
    }
    return this.get('api').authRequest('del', url, payload);
  },

  createPost(params) {
    let store = this.get('store');
    let emotes = this.get('emotes');
    let user = this.get('meAsFeedUser');
    let newPost = store.createRecord("post", params);

    this.set('postCreationInFlight', true);
    newPost.set('user', user);
    newPost.set('reactions', emberA([]));
    newPost.set('commentsTotal', 0);

    let body = params.body;
    body = body.split('\n').filter(x => x.length).join('\n');
    emotes.parseEmotes(body).then((emotesInString) => {
      newPost.set('emotes', emotesInString);
    });

    let newsFeed = store.peekRecord('feed', NEWSFEED_ID);
    let channelFeed = store.peekRecord('feed', user.get('login'));
    if (channelFeed) {
      channelFeed.get('posts').unshiftObject(newPost);
    }
    if (newsFeed) {
      newsFeed.get('posts').unshiftObject(newPost);
    }

    return newPost.save().then(
      (post) => {
        this.set('postCreationInFlight', false);
        return post;
      }, (error) => {
        if (newsFeed) {
          newsFeed.get('posts').removeObject(newPost);
        }
        if (channelFeed) {
          channelFeed.get('posts').removeObject(newPost);
        }
        this.set('postCreationInFlight', false);
        throw error;
      }
    );
  },

  createComment(params) {
    let store = this.get('store');
    let emotes = this.get('emotes');
    let user = this.get('meAsFeedUser');
    let newComment = store.createRecord('comment', params);

    this.set('postCreationInFlight', true);
    newComment.set('user', user);
    newComment.set('reactions', emberA([]));

    let body = params.body;
    body = body.split('\n').filter(x => x.length).join('\n');
    emotes.parseEmotes(body).then((emotesInString) => {
      newComment.set('emotes', emotesInString);
    });

    let parentPost = store.peekRecord('post', params.postId);
    parentPost.set('commentsTotal', parentPost.get('commentsTotal') + 1);
    parentPost.get('comments').unshiftObject(newComment);
    newComment.set('channelName', parentPost.get('user.login'));

    return newComment.save().then(
      (comment) => {
        comment.set('localCreatedAt', moment());
        this.set('postCreationInFlight', false);
        return comment;
      }, (error) => {
        parentPost.get('comments').removeObject(newComment);
        parentPost.set('commentsTotal', parentPost.get('commentsTotal') - 1);
        this.set('postCreationInFlight', false);
        throw error;
      }
    );
  },

  deleteMessage(message) {
    let type = message.constructor.modelName;
    let post = message.get('post');

    return message.destroyRecord().then((deletedMessage) => {
      if (type === 'comment') {
        let commentsTotal = post.get('commentsTotal');
        post.set('commentsTotal', commentsTotal - 1);
      }
      return deletedMessage;
    });
  },

  deleteUserCommentsFromPost(comment) {
    let post = comment.get('post');
    let comments = post.get('comments');
    let commentsTotal = post.get('commentsTotal');
    let deletedComments = [];

    for (let i = 0; i < comments.length; i++) {
      let indexComment = comments.objectAt(i);
      if (indexComment.get('user.id') === comment.get('user.id')) {
        deletedComments.push(indexComment);
      }
    }
    comments.removeObjects(deletedComments);
    post.set('commentsTotal', commentsTotal - deletedComments.length);

    return this.get('store').query('comment', {
      type : 'deleteUserCommentsFromPost',
      comment,
      post
    }).then((response) => {
      post.set('commentsTotal', commentsTotal - response.content.length);
      return response;
    }, (e) => {
      if (this.isDestroyed) { throw e; }
      post.set('commentsTotal', commentsTotal);
      comments.addObjects(deletedComments);
      let sortedComments = comments.toArray().sort((a, b) => {
        return a.get('createdAt') < b.get('createdAt');
      });
      post.set('comments', sortedComments);
      throw e;
    });
  },

  reportMessage(params) {
    let message = params.message;
    let messageId = message.get('id');
    let type = message.constructor.modelName;
    let url;
    let payload = {
      reason: params.reportReason,
      reported_user: params.channelName
    };

    if (type === 'post') {
      url = `feed/${params.channelName}/posts/${messageId}/report`;
      payload.post_id = messageId;
    } else {
      let postId = message.get('post.id');
      url = `feed/${params.channelName}/posts/${postId}/comments/${messageId}/report`;
      payload.comment_id = messageId;
    }

    return this.get('api').authRequest('post', url, payload);
  },

  banUser(user, channelName) {
    let url = `/api/channels/${channelName}/chat_ban_user`;
    let payload = { ban_user: user };
    return this.get('api').authRequest('post', url, payload);
  },

  loadMeAsFeedUser() {
    let { isAuthenticated, userData, userModel } = this.get('session');
    let userLogo = this.get('session').get('userLogo');
    if (!isAuthenticated) { return; }

    let store = this.get('store');
    let me = store.peekRecord('feed-user', userData.id);

    if (!me) {
      me = store.createRecord('feed-user', {
        id: userData.id,
        login: userData.login,
        displayName: userModel.get('displayName'),
        isStaff: userData.is_staff,
        twitterConnected: userData.twitter_connected,
        profileImage: userLogo
      });
    } else {
      me.set('twitterConnected', userData.twitter_connected);
    }

    this.set('meAsFeedUser', me);
  },

  _addReactionToMessage(message, emote) {
    let reactions = message.get('reactions');
    let reaction = EmberObject.create({
      emoteId: emote.id,
      emoteName: emote.name,
      count: 0,
      userIds: emberA([])
    });

    reactions.pushObject(reaction);
    return reaction;
  },

  setLocalStorageValue(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },

  getLocalStorageValue(key) {
    try {
      return JSON.parse(localStorage.getItem(key));
    }
    catch(err) {
      console.error(`Error retrieving "${key}" from localStorage`);
    }
  },

  _parseMessage(payload) {
    try {
      return JSON.parse(payload);
    } catch (err) {
      console.error("Error parsing message from pubsub", err);
      return {};
    }
  },

  _generateMessageHandler(feed) {
    return (payload) => {
      let message = this._parseMessage(payload);
      this._processMessage(feed, message);
    };
  },

  bindLiveFeed(feed) {
    let liveFeeds = this.get('pubsubFeeds');
    let topic = feed.get('topic');

    if (topic && liveFeeds[topic] === undefined) {
      let message = this._generateMessageHandler(feed);
      liveFeeds[topic] = message;

      let auth = this.get('session.userData.oauth_chat_token');

      let pubsub = this.get('pubsubDriver');
      pubsub.Listen({
        topic, auth, message,
        success: () => {},
        failure: (err) => {
          console.error("Failed to bind to live feed", topic, err);
          this.unbindLiveFeed(feed);
        }
      });

      this._getChannelPermissions(feed.get('id'), true);
    }
  },

  unbindLiveFeed(feed) {
    let liveFeeds = this.get('pubsubFeeds');
    let feedTopic = feed.get('topic');
    if (feedTopic && liveFeeds[feedTopic]) {
      let pubsub = this.get('pubsubDriver');
      pubsub.Unlisten({
        topic: feedTopic,
        success: () => {
          delete liveFeeds[feedTopic];
        },
        failure: (err) => {
          console.error("Failed to unbind to live feed", feedTopic, err);
        },
        message: liveFeeds[feedTopic]
      });
      this.set('permissions', null);
    }
  },

  initializeDriver() {
    let pubsub = this._pubsub();

    this.set('pubsubDriver', pubsub);
  },

  _getChannelPermissions(channelLogin, reFetch=false) {
    let url = `/kraken/feed/${channelLogin}/posts/permissions`;
    let channelPermissions = this.get('permissions');

    return new RSVP.Promise((resolve, reject) => {
      if (channelPermissions && !reFetch) {
        resolve(channelPermissions);
        return;
      }

      this.get('api').authRequest('get', url).then((resp) => {
        if (!this.isDestroyed) {
          this.set('permissions', resp);
        }
        return resp;
      }).then(resolve, reject);
    });
  },

  _processMessage(feed, message) {
    let store = this.get('store');
    switch (message.kind) {
      case "new-post": {
        if (this.get('postCreationInFlight')) {
          return;
        }

        let channelLogin = feed.get('id');

        this._getChannelPermissions(channelLogin).then(
          (permissionsContext) => {
            return this._parsePost(message.payload, permissionsContext);
          }
        ).then(
          (post) => {
            if (feed && post) {
              let posts = feed.get('posts');
              posts.unshiftObject(post);
            }
          }
        );
        break;
      }
      case "delete-post": {
        let postId = message.payload;
        if (feed) {
          let post = store.peekRecord('post', postId);
          if (post) {
            let posts = feed.get('posts');
            posts.removeObject(post);
            store.deleteRecord(post);
          }
        }
        break;
      }
      case "update-reactions": {
        let {comments, posts} = message.payload || {};
        Object.keys(comments || {}).forEach(commentID => {
          let comment = store.peekRecord('comment', commentID);
          comment.set('reactions', mergeReactions(comment.get('reactions'), comments[commentID]));
        });
        Object.keys(posts || {}).forEach(postID => {
          let post = store.peekRecord('post', postID);
          post.set('reactions', mergeReactions(post.get('reactions'), posts[postID]));
        });
        break;
      }
      default:
        console.warn("Unhandled feed pubsub message of type:", message.kind);
    }
  },

  _parsePost(postPayload, permissionContext={can_reply:false}) {
    let userId = postPayload.user_id;
    let store = this.get('store');
    let existingPost = store.peekRecord('post', postPayload.id);
    if (existingPost) {
      // Potentially merge post here, but return null because there is no new post.
      return RSVP.resolve(null);
    }

    postPayload.permissions = permissionContext;
    delete postPayload.user;
    store.pushPayload('post', postPayload);
    let post = store.peekRecord('post', postPayload.id);
    post.set('commentsTotal', 0);

    return store.findRecord('feed-user', userId).then((user) => {
      post.set('user', user);
      return post;
    });
  },

  _pubsub() {
    let environment = this.get('globals.feedsPubsubEnv');
    return PubsubDriver.getInstance(environment);
  },

  trackEvent(type, context, data = {}) {
    let login = this.get('session.userData.login');
    let deviceId = Twitch.idsForMixpanel.getOrCreateUniqueId();
    let params = {
      device_id: deviceId,
      login
    };
    assign(data, context);
    assign(params, data);

    this.get('tracking').trackEvent({
      event: type,
      services: ['spade'],
      data: params
    });
  }
});
