import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'

// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
  if (!item) { return false }
  const oldItem = obj[item.id]
  if (oldItem) {
    // We already have this, so only merge the new info.
    mergeWith(oldItem, item, mergeArrayLength)
    return { item: oldItem, new: false }
  } else {
    // This is a new item, prepare it
    arr.push(item)
    obj[item.id] = item
    return { item, new: true }
  }
}

const mergeArrayLength = (oldValue, newValue) => {
  if (isArray(oldValue) && isArray(newValue)) {
    oldValue.length = newValue.length
    return mergeWith(oldValue, newValue, mergeArrayLength)
  }
}

const getNotificationPermission = () => {
  const Notification = window.Notification

  if (!Notification) return Promise.resolve(null)
  if (Notification.permission === 'default') return Notification.requestPermission()
  return Promise.resolve(Notification.permission)
}

const blockUser = (store, id) => {
  return store.rootState.api.backendInteractor.blockUser({ id })
    .then((relationship) => {
      store.commit('updateUserRelationship', [relationship])
      store.commit('addBlockId', id)
      store.commit('removeStatus', { timeline: 'friends', userId: id })
      store.commit('removeStatus', { timeline: 'public', userId: id })
      store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
    })
}

const unblockUser = (store, id) => {
  return store.rootState.api.backendInteractor.unblockUser({ id })
    .then((relationship) => store.commit('updateUserRelationship', [relationship]))
}

const removeUserFromFollowers = (store, id) => {
  return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
    .then((relationship) => store.commit('updateUserRelationship', [relationship]))
}

const editUserNote = (store, { id, comment }) => {
  return store.rootState.api.backendInteractor.editUserNote({ id, comment })
    .then((relationship) => store.commit('updateUserRelationship', [relationship]))
}

const muteUser = (store, args) => {
  const id = typeof args === 'object' ? args.id : args
  const expiresIn = typeof args === 'object' ? args.expiresIn : 0

  const predictedRelationship = store.state.relationships[id] || { id }
  predictedRelationship.muting = true
  store.commit('updateUserRelationship', [predictedRelationship])
  store.commit('addMuteId', id)

  return store.rootState.api.backendInteractor.muteUser({ id, expiresIn })
    .then((relationship) => {
      store.commit('updateUserRelationship', [relationship])
      store.commit('addMuteId', id)
    })
}

const unmuteUser = (store, id) => {
  const predictedRelationship = store.state.relationships[id] || { id }
  predictedRelationship.muting = false
  store.commit('updateUserRelationship', [predictedRelationship])

  return store.rootState.api.backendInteractor.unmuteUser({ id })
    .then((relationship) => store.commit('updateUserRelationship', [relationship]))
}

const hideReblogs = (store, userId) => {
  return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
    .then((relationship) => {
      store.commit('updateUserRelationship', [relationship])
    })
}

const showReblogs = (store, userId) => {
  return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
    .then((relationship) => store.commit('updateUserRelationship', [relationship]))
}

const muteDomain = (store, domain) => {
  return store.rootState.api.backendInteractor.muteDomain({ domain })
    .then(() => store.commit('addDomainMute', domain))
}

const unmuteDomain = (store, domain) => {
  return store.rootState.api.backendInteractor.unmuteDomain({ domain })
    .then(() => store.commit('removeDomainMute', domain))
}

export const mutations = {
  tagUser (state, { user: { id }, tag }) {
    const user = state.usersObject[id]
    const tags = user.tags || []
    const newTags = tags.concat([tag])
    user.tags = newTags
  },
  untagUser (state, { user: { id }, tag }) {
    const user = state.usersObject[id]
    const tags = user.tags || []
    const newTags = tags.filter(t => t !== tag)
    user.tags = newTags
  },
  updateRight (state, { user: { id }, right, value }) {
    const user = state.usersObject[id]
    const newRights = user.rights
    newRights[right] = value
    user.rights = newRights
  },
  updateActivationStatus (state, { user: { id }, deactivated }) {
    const user = state.usersObject[id]
    user.deactivated = deactivated
  },
  setCurrentUser (state, user) {
    state.lastLoginName = user.screen_name
    state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
  },
  clearCurrentUser (state) {
    state.currentUser = false
    state.lastLoginName = false
  },
  beginLogin (state) {
    state.loggingIn = true
  },
  endLogin (state) {
    state.loggingIn = false
  },
  saveFriendIds (state, { id, friendIds }) {
    const user = state.usersObject[id]
    user.friendIds = uniq(concat(user.friendIds || [], friendIds))
  },
  saveFollowerIds (state, { id, followerIds }) {
    const user = state.usersObject[id]
    user.followerIds = uniq(concat(user.followerIds || [], followerIds))
  },
  // Because frontend doesn't have a reason to keep these stuff in memory
  // outside of viewing someones user profile.
  clearFriends (state, userId) {
    const user = state.usersObject[userId]
    if (user) {
      user.friendIds = []
    }
  },
  clearFollowers (state, userId) {
    const user = state.usersObject[userId]
    if (user) {
      user.followerIds = []
    }
  },
  addNewUsers (state, users) {
    each(users, (user) => {
      if (user.relationship) {
        state.relationships[user.relationship.id] = user.relationship
      }
      const res = mergeOrAdd(state.users, state.usersObject, user)
      const item = res.item
      if (res.new && item.screen_name && !item.screen_name.includes('@')) {
        state.usersByNameObject[item.screen_name.toLowerCase()] = item
      }
    })
  },
  updateUserRelationship (state, relationships) {
    relationships.forEach((relationship) => {
      state.relationships[relationship.id] = relationship
    })
  },
  updateUserInLists (state, { id, inLists }) {
    state.usersObject[id].inLists = inLists
  },
  saveBlockIds (state, blockIds) {
    state.currentUser.blockIds = blockIds
  },
  addBlockId (state, blockId) {
    if (state.currentUser.blockIds.indexOf(blockId) === -1) {
      state.currentUser.blockIds.push(blockId)
    }
  },
  setBlockIdsMaxId (state, blockIdsMaxId) {
    state.currentUser.blockIdsMaxId = blockIdsMaxId
  },
  saveMuteIds (state, muteIds) {
    state.currentUser.muteIds = muteIds
  },
  setMuteIdsMaxId (state, muteIdsMaxId) {
    state.currentUser.muteIdsMaxId = muteIdsMaxId
  },
  addMuteId (state, muteId) {
    if (state.currentUser.muteIds.indexOf(muteId) === -1) {
      state.currentUser.muteIds.push(muteId)
    }
  },
  saveDomainMutes (state, domainMutes) {
    state.currentUser.domainMutes = domainMutes
  },
  addDomainMute (state, domain) {
    if (state.currentUser.domainMutes.indexOf(domain) === -1) {
      state.currentUser.domainMutes.push(domain)
    }
  },
  removeDomainMute (state, domain) {
    const index = state.currentUser.domainMutes.indexOf(domain)
    if (index !== -1) {
      state.currentUser.domainMutes.splice(index, 1)
    }
  },
  setPinnedToUser (state, status) {
    const user = state.usersObject[status.user.id]
    user.pinnedStatusIds = user.pinnedStatusIds || []
    const index = user.pinnedStatusIds.indexOf(status.id)

    if (status.pinned && index === -1) {
      user.pinnedStatusIds.push(status.id)
    } else if (!status.pinned && index !== -1) {
      user.pinnedStatusIds.splice(index, 1)
    }
  },
  setUserForStatus (state, status) {
    status.user = state.usersObject[status.user.id]
  },
  setUserForNotification (state, notification) {
    if (notification.type !== 'follow') {
      notification.action.user = state.usersObject[notification.action.user.id]
    }
    notification.from_profile = state.usersObject[notification.from_profile.id]
  },
  setColor (state, { user: { id }, highlighted }) {
    const user = state.usersObject[id]
    user.highlight = highlighted
  },
  signUpPending (state) {
    state.signUpPending = true
    state.signUpErrors = []
    state.signUpNotice = {}
  },
  signUpSuccess (state) {
    state.signUpPending = false
  },
  signUpFailure (state, errors) {
    state.signUpPending = false
    state.signUpErrors = errors
    state.signUpNotice = {}
  },
  signUpNotice (state, notice) {
    state.signUpPending = false
    state.signUpErrors = []
    state.signUpNotice = notice
  }
}

export const getters = {
  findUser: state => query => {
    return state.usersObject[query]
  },
  findUserByName: state => query => {
    return state.usersByNameObject[query.toLowerCase()]
  },
  findUserByUrl: state => query => {
    return state.users
      .find(u => u.statusnet_profile_url &&
            u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
  },
  relationship: state => id => {
    const rel = id && state.relationships[id]
    return rel || { id, loading: true }
  }
}

export const defaultState = {
  loggingIn: false,
  lastLoginName: false,
  currentUser: false,
  users: [],
  usersObject: {},
  usersByNameObject: {},
  signUpPending: false,
  signUpErrors: [],
  signUpNotice: {},
  relationships: {}
}

const users = {
  state: defaultState,
  mutations,
  getters,
  actions: {
    fetchUserIfMissing (store, id) {
      if (!store.getters.findUser(id)) {
        store.dispatch('fetchUser', id)
      }
    },
    fetchUser (store, id) {
      return store.rootState.api.backendInteractor.fetchUser({ id })
        .then((user) => {
          store.commit('addNewUsers', [user])
          return user
        })
    },
    fetchUserByName (store, name) {
      return store.rootState.api.backendInteractor.fetchUserByName({ name })
        .then((user) => {
          store.commit('addNewUsers', [user])
          return user
        })
    },
    fetchUserRelationship (store, id) {
      if (store.state.currentUser) {
        store.rootState.api.backendInteractor.fetchUserRelationship({ id })
          .then((relationships) => store.commit('updateUserRelationship', relationships))
      }
    },
    fetchUserInLists (store, id) {
      if (store.state.currentUser) {
        store.rootState.api.backendInteractor.fetchUserInLists({ id })
          .then((inLists) => store.commit('updateUserInLists', { id, inLists }))
      }
    },
    fetchBlocks (store, args) {
      const { reset } = args || {}

      const maxId = store.state.currentUser.blockIdsMaxId
      return store.rootState.api.backendInteractor.fetchBlocks({ maxId })
        .then((blocks) => {
          if (reset) {
            store.commit('saveBlockIds', map(blocks, 'id'))
          } else {
            map(blocks, 'id').map(id => store.commit('addBlockId', id))
          }
          if (blocks.length) {
            store.commit('setBlockIdsMaxId', last(blocks).id)
          }
          store.commit('addNewUsers', blocks)
          return blocks
        })
    },
    blockUser (store, id) {
      return blockUser(store, id)
    },
    unblockUser (store, id) {
      return unblockUser(store, id)
    },
    removeUserFromFollowers (store, id) {
      return removeUserFromFollowers(store, id)
    },
    blockUsers (store, ids = []) {
      return Promise.all(ids.map(id => blockUser(store, id)))
    },
    unblockUsers (store, ids = []) {
      return Promise.all(ids.map(id => unblockUser(store, id)))
    },
    editUserNote (store, args) {
      return editUserNote(store, args)
    },
    fetchMutes (store, args) {
      const { reset } = args || {}

      const maxId = store.state.currentUser.muteIdsMaxId
      return store.rootState.api.backendInteractor.fetchMutes({ maxId })
        .then((mutes) => {
          if (reset) {
            store.commit('saveMuteIds', map(mutes, 'id'))
          } else {
            map(mutes, 'id').map(id => store.commit('addMuteId', id))
          }
          if (mutes.length) {
            store.commit('setMuteIdsMaxId', last(mutes).id)
          }
          store.commit('addNewUsers', mutes)
          return mutes
        })
    },
    muteUser (store, id) {
      return muteUser(store, id)
    },
    unmuteUser (store, id) {
      return unmuteUser(store, id)
    },
    hideReblogs (store, id) {
      return hideReblogs(store, id)
    },
    showReblogs (store, id) {
      return showReblogs(store, id)
    },
    muteUsers (store, ids = []) {
      return Promise.all(ids.map(id => muteUser(store, id)))
    },
    unmuteUsers (store, ids = []) {
      return Promise.all(ids.map(id => unmuteUser(store, id)))
    },
    fetchDomainMutes (store) {
      return store.rootState.api.backendInteractor.fetchDomainMutes()
        .then((domainMutes) => {
          store.commit('saveDomainMutes', domainMutes)
          return domainMutes
        })
    },
    muteDomain (store, domain) {
      return muteDomain(store, domain)
    },
    unmuteDomain (store, domain) {
      return unmuteDomain(store, domain)
    },
    muteDomains (store, domains = []) {
      return Promise.all(domains.map(domain => muteDomain(store, domain)))
    },
    unmuteDomains (store, domain = []) {
      return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
    },
    fetchFriends ({ rootState, commit }, id) {
      const user = rootState.users.usersObject[id]
      const maxId = last(user.friendIds)
      return rootState.api.backendInteractor.fetchFriends({ id, maxId })
        .then((friends) => {
          commit('addNewUsers', friends)
          commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
          return friends
        })
    },
    fetchFollowers ({ rootState, commit }, id) {
      const user = rootState.users.usersObject[id]
      const maxId = last(user.followerIds)
      return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
        .then((followers) => {
          commit('addNewUsers', followers)
          commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
          return followers
        })
    },
    clearFriends ({ commit }, userId) {
      commit('clearFriends', userId)
    },
    clearFollowers ({ commit }, userId) {
      commit('clearFollowers', userId)
    },
    subscribeUser ({ rootState, commit }, id) {
      return rootState.api.backendInteractor.followUser({ id, notify: true })
        .then((relationship) => commit('updateUserRelationship', [relationship]))
    },
    unsubscribeUser ({ rootState, commit }, id) {
      return rootState.api.backendInteractor.followUser({ id, notify: false })
        .then((relationship) => commit('updateUserRelationship', [relationship]))
    },
    toggleActivationStatus ({ rootState, commit }, { user }) {
      const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
      api({ user })
        .then((user) => { const deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
    },
    registerPushNotifications (store) {
      const token = store.state.currentUser.credentials
      const vapidPublicKey = store.rootState.instance.vapidPublicKey
      const isEnabled = store.rootState.config.webPushNotifications
      const notificationVisibility = store.rootState.config.notificationVisibility

      registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
    },
    unregisterPushNotifications (store) {
      const token = store.state.currentUser.credentials

      unregisterPushNotifications(token)
    },
    addNewUsers ({ commit }, users) {
      commit('addNewUsers', users)
    },
    addNewStatuses (store, { statuses }) {
      const users = map(statuses, 'user')
      const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
      store.commit('addNewUsers', users)
      store.commit('addNewUsers', retweetedUsers)

      each(statuses, (status) => {
        // Reconnect users to statuses
        store.commit('setUserForStatus', status)
        // Set pinned statuses to user
        store.commit('setPinnedToUser', status)
      })
      each(compact(map(statuses, 'retweeted_status')), (status) => {
        // Reconnect users to retweets
        store.commit('setUserForStatus', status)
        // Set pinned retweets to user
        store.commit('setPinnedToUser', status)
      })
    },
    addNewNotifications (store, { notifications }) {
      const users = map(notifications, 'from_profile')
      const targetUsers = map(notifications, 'target').filter(_ => _)
      const notificationIds = notifications.map(_ => _.id)
      store.commit('addNewUsers', users)
      store.commit('addNewUsers', targetUsers)

      const notificationsObject = store.rootState.notifications.idStore
      const relevantNotifications = Object.entries(notificationsObject)
        .filter(([k, val]) => notificationIds.includes(k))
        .map(([k, val]) => val)

      // Reconnect users to notifications
      each(relevantNotifications, (notification) => {
        store.commit('setUserForNotification', notification)
      })
    },
    searchUsers ({ rootState, commit }, { query }) {
      return rootState.api.backendInteractor.searchUsers({ query })
        .then((users) => {
          commit('addNewUsers', users)
          return users
        })
    },
    async signUp (store, userInfo) {
      store.commit('signUpPending')

      const rootState = store.rootState

      try {
        const data = await rootState.api.backendInteractor.register(
          { params: { ...userInfo } }
        )

        if (data.access_token) {
          store.commit('signUpSuccess')
          store.commit('setToken', data.access_token)
          store.dispatch('loginUser', data.access_token)
          return 'ok'
        } else { // Request succeeded, but user cannot login yet.
          store.commit('signUpNotice', data)
          return 'request_sent'
        }
      } catch (e) {
        const errors = e.message
        store.commit('signUpFailure', errors)
        throw e
      }
    },
    async getCaptcha (store) {
      return store.rootState.api.backendInteractor.getCaptcha()
    },

    logout (store) {
      const { oauth, instance } = store.rootState

      const data = {
        ...oauth,
        commit: store.commit,
        instance: instance.server
      }

      return oauthApi.getOrCreateApp(data)
        .then((app) => {
          const params = {
            app,
            instance: data.instance,
            token: oauth.userToken
          }

          return oauthApi.revokeToken(params)
        })
        .then(() => {
          store.commit('clearCurrentUser')
          store.dispatch('disconnectFromSocket')
          store.commit('clearToken')
          store.dispatch('stopFetchingTimeline', 'friends')
          store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
          store.dispatch('stopFetchingNotifications')
          store.dispatch('stopFetchingLists')
          store.dispatch('stopFetchingBookmarkFolders')
          store.dispatch('stopFetchingFollowRequests')
          store.commit('clearNotifications')
          store.commit('resetStatuses')
          store.dispatch('resetChats')
          store.dispatch('setLastTimeline', 'public-timeline')
          store.dispatch('setLayoutWidth', windowWidth())
          store.dispatch('setLayoutHeight', windowHeight())
          store.commit('clearServerSideStorage')
        })
    },
    loginUser (store, accessToken) {
      return new Promise((resolve, reject) => {
        const commit = store.commit
        const dispatch = store.dispatch
        commit('beginLogin')
        store.rootState.api.backendInteractor.verifyCredentials(accessToken)
          .then((data) => {
            if (!data.error) {
              const user = data
              // user.credentials = userCredentials
              user.credentials = accessToken
              user.blockIds = []
              user.muteIds = []
              user.domainMutes = []
              commit('setCurrentUser', user)
              commit('setServerSideStorage', user)
              commit('addNewUsers', [user])

              dispatch('fetchEmoji')

              getNotificationPermission()
                .then(permission => commit('setNotificationPermission', permission))

              // Set our new backend interactor
              commit('setBackendInteractor', backendInteractorService(accessToken))
              dispatch('pushServerSideStorage')

              if (user.token) {
                dispatch('setWsToken', user.token)

                // Initialize the shout socket.
                dispatch('initializeSocket')
              }

              const startPolling = () => {
                // Start getting fresh posts.
                dispatch('startFetchingTimeline', { timeline: 'friends' })

                // Start fetching notifications
                dispatch('startFetchingNotifications')

                // Start fetching chats
                dispatch('startFetchingChats')
              }

              dispatch('startFetchingLists')
              dispatch('startFetchingBookmarkFolders')

              if (user.locked) {
                dispatch('startFetchingFollowRequests')
              }

              if (store.getters.mergedConfig.useStreamingApi) {
                dispatch('fetchTimeline', { timeline: 'friends', since: null })
                dispatch('fetchNotifications', { since: null })
                dispatch('enableMastoSockets', true).catch((error) => {
                  console.error('Failed initializing MastoAPI Streaming socket', error)
                }).then(() => {
                  dispatch('fetchChats', { latest: true })
                  setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
                })
              } else {
                startPolling()
              }

              // Get user mutes
              dispatch('fetchMutes')

              dispatch('setLayoutWidth', windowWidth())
              dispatch('setLayoutHeight', windowHeight())

              // Fetch our friends
              store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
                .then((friends) => commit('addNewUsers', friends))
            } else {
              const response = data.error
              // Authentication failed
              commit('endLogin')

              // remove authentication token on client/authentication errors
              if ([400, 401, 403, 422].includes(response.status)) {
                commit('clearToken')
              }

              if (response.status === 401) {
                reject(new Error('Wrong username or password'))
              } else {
                reject(new Error('An error occurred, please try again'))
              }
            }
            commit('endLogin')
            resolve()
          })
          .catch((error) => {
            console.error(error)
            commit('endLogin')
            reject(new Error('Failed to connect to server, try again'))
          })
      })
    }
  }
}

export default users
