import DataSourceService from '@baserow/modules/builder/services/dataSource'
import PublishedBuilderService from '@baserow/modules/builder/services/publishedBuilder'
import { ELEMENT_EVENTS } from '../enums'

const state = {}

const updateContext = {
  updateTimeout: null,
  promiseResolve: null,
  lastUpdatedValues: null,
  valuesToUpdate: {},
}

const mutations = {
  ADD_ITEM(state, { page, dataSource, beforeId = null }) {
    if (beforeId === null) {
      page.dataSources.push(dataSource)
    } else {
      const insertionIndex = page.dataSources.findIndex(
        (e) => e.id === beforeId
      )
      page.dataSources.splice(insertionIndex, 0, dataSource)
    }
  },
  UPDATE_ITEM(state, { page, dataSource: dataSourceToUpdate, values }) {
    const index = page.dataSources.findIndex(
      (dataSource) => dataSource.id === dataSourceToUpdate.id
    )
    page.dataSources.splice(index, 1, {
      ...page.dataSources[index],
      ...values,
    })
  },
  FULL_UPDATE_ITEM(state, { page, dataSource: dataSourceToUpdate, values }) {
    const index = page.dataSources.findIndex(
      (dataSource) => dataSource.id === dataSourceToUpdate.id
    )
    page.dataSources.splice(index, 1, {
      ...values,
    })
  },
  DELETE_ITEM(state, { page, dataSourceId }) {
    const index = page.dataSources.findIndex(
      (dataSource) => dataSource.id === dataSourceId
    )
    if (index > -1) {
      page.dataSources.splice(index, 1)
    }
  },
  MOVE_ITEM(state, { page, index, oldIndex }) {
    page.dataSources.splice(index, 0, page.dataSources.splice(oldIndex, 1)[0])
  },
  CLEAR_ITEMS(state, { page }) {
    page.dataSources = []
  },
  SET_LOADING(state, { page, value }) {
    page._.dataSourceLoading = value
  },
}

const actions = {
  forceCreate({ commit }, { page, dataSource, beforeId = null }) {
    commit('ADD_ITEM', { page, dataSource, beforeId })
  },
  forceUpdate({ commit, dispatch }, { page, dataSource, values }) {
    commit('UPDATE_ITEM', { page, dataSource, values })
  },
  forceDelete({ commit, dispatch }, { page, dataSourceId }) {
    // Remove related content first
    dispatch(
      'dataSourceContent/clearDataSourceContent',
      { page, dataSourceId },
      { root: true }
    )
    //
    dispatch(
      'element/emitElementEvent',
      { event: ELEMENT_EVENTS.DATA_SOURCE_REMOVED, page, dataSourceId },
      { root: true }
    )
    commit('DELETE_ITEM', { page, dataSourceId })
  },
  forceMove({ commit, getters }, { page, dataSourceId, beforeDataSourceId }) {
    const currentOrder = getters
      .getPageDataSources(page)
      .map((dataSource) => dataSource.id)

    const oldIndex = currentOrder.findIndex((id) => id === dataSourceId)
    const index = beforeDataSourceId
      ? currentOrder.findIndex((id) => id === beforeDataSourceId)
      : getters.getPageDataSources(page).length

    // If the dataSource is before the beforeDataSource we must decrease the target index by
    // one to compensate the removed dataSource.
    if (oldIndex < index) {
      commit('MOVE_ITEM', { page, index: index - 1, oldIndex })
    } else {
      commit('MOVE_ITEM', { page, index, oldIndex })
    }
  },
  async create({ commit, dispatch }, { page, values, beforeId }) {
    commit('SET_LOADING', { page, value: true })
    const { data: dataSource } = await DataSourceService(this.$client).create(
      page.id,
      values,
      beforeId
    )

    await dispatch('forceCreate', { page, dataSource, beforeId })
    commit('SET_LOADING', { page, value: false })
  },
  async update({ commit, dispatch }, { page, dataSourceId, values }) {
    const dataSourcesOfPage = getters.getPageDataSources(page)
    const dataSource = dataSourcesOfPage.find(
      (dataSource) => dataSource.id === dataSourceId
    )
    const oldValues = {}
    const newValues = {}
    Object.keys(values).forEach((name) => {
      if (Object.prototype.hasOwnProperty.call(dataSource, name)) {
        oldValues[name] = dataSource[name]
        newValues[name] = values[name]
      }
    })

    await dispatch('forceUpdate', { page, dataSource, values: newValues })

    commit('SET_LOADING', { page, value: true })
    try {
      await DataSourceService(this.$client).update(dataSource.id, values)

      dispatch(
        'element/emitElementEvent',
        {
          event: ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE,
          page,
          dataSourceId: dataSource.id,
        },
        { root: true }
      )
    } catch (error) {
      await dispatch('forceUpdate', { page, dataSource, values: oldValues })
      throw error
    }
    commit('SET_LOADING', { page, value: false })
  },

  async debouncedUpdate(
    { dispatch, getters, commit },
    { page, dataSourceId, values }
  ) {
    const dataSourcesOfPage = getters.getPageDataSources(page)
    const dataSource = dataSourcesOfPage.find(
      (dataSource) => dataSource.id === dataSourceId
    )
    const oldValues = {}
    Object.keys(values).forEach((name) => {
      if (Object.prototype.hasOwnProperty.call(dataSource, name)) {
        oldValues[name] = dataSource[name]
        // Accumulate the changed values to send all the ongoing changes with the
        // final request
        updateContext.valuesToUpdate[name] = values[name]
      }
    })

    // If we have a dataSource type, fetch it from the service type registry
    // then call the registry's `beforeUpdate` hook to optionally manipulate
    // the values prior to performing an update.
    if (dataSource.type !== null) {
      const dataSourceType = this.$registry.get('service', dataSource.type)
      updateContext.valuesToUpdate = dataSourceType.beforeUpdate(
        updateContext.valuesToUpdate,
        oldValues
      )
    }

    await dispatch('forceUpdate', {
      page,
      dataSource,
      values: updateContext.valuesToUpdate,
    })

    return new Promise((resolve, reject) => {
      const fire = async () => {
        const toUpdate = updateContext.valuesToUpdate
        updateContext.valuesToUpdate = {}
        commit('SET_LOADING', { page, value: true })
        try {
          const { data } = await DataSourceService(this.$client).update(
            dataSource.id,
            toUpdate
          )
          await commit('FULL_UPDATE_ITEM', { page, dataSource, values: data })
          dispatch(
            'element/emitElementEvent',
            {
              event: ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE,
              page,
              dataSourceId: dataSource.id,
            },
            { root: true }
          )
          updateContext.lastUpdatedValues = null
          resolve()
        } catch (error) {
          // Revert to old values on error
          await dispatch('forceUpdate', {
            page,
            dataSource,
            values: updateContext.lastUpdatedValues,
          })
          updateContext.lastUpdatedValues = null
          reject(error)
        }
        commit('SET_LOADING', { page, value: false })
      }

      if (updateContext.promiseResolve) {
        updateContext.promiseResolve()
        updateContext.promiseResolve = null
      }

      clearTimeout(updateContext.updateTimeout)

      if (!updateContext.lastUpdatedValues) {
        updateContext.lastUpdatedValues = oldValues
      }

      updateContext.updateTimeout = setTimeout(fire, 500)
      updateContext.promiseResolve = resolve
    })
  },
  async delete({ commit, dispatch, getters }, { page, dataSourceId }) {
    const dataSourcesOfPage = getters.getPageDataSources(page)
    const dataSourceIndex = dataSourcesOfPage.findIndex(
      (dataSource) => dataSource.id === dataSourceId
    )
    const dataSourceToDelete = dataSourcesOfPage[dataSourceIndex]
    const beforeId =
      dataSourceIndex !== dataSourcesOfPage.length - 1
        ? dataSourcesOfPage[dataSourceIndex + 1].id
        : null

    await dispatch('forceDelete', { page, dataSourceId })

    commit('SET_LOADING', { page, value: true })
    try {
      await DataSourceService(this.$client).delete(dataSourceId)
    } catch (error) {
      await dispatch('forceCreate', {
        page,
        dataSource: dataSourceToDelete,
        beforeId,
      })
      throw error
    }
    // After deleting the data source, find all collection elements
    // which use this data source, and clear their element content.
    const dataSourceCollectionElements = page.elements.filter((element) => {
      return element.data_source_id === dataSourceToDelete.id
    })
    dataSourceCollectionElements.map(async (collectionElement) => {
      await dispatch(
        'elementContent/clearElementContent',
        {
          element: collectionElement,
        },
        { root: true }
      )
    })
    commit('SET_LOADING', { page, value: false })
  },
  async fetch({ dispatch, commit }, { page }) {
    commit('SET_LOADING', { page, value: true })
    dispatch(
      'dataSourceContent/clearDataSourceContents',
      { page },
      { root: true }
    )
    const { data: dataSources } = await DataSourceService(
      this.$client
    ).fetchAll(page.id)

    commit('CLEAR_ITEMS', { page })
    await Promise.all(
      dataSources.map((dataSource) =>
        dispatch('forceCreate', { page, dataSource })
      )
    )
    commit('SET_LOADING', { page, value: false })

    return dataSources
  },
  async fetchPublished({ dispatch, commit }, { page }) {
    commit('SET_LOADING', { page, value: true })
    dispatch(
      'dataSourceContent/clearDataSourceContents',
      { page },
      { root: true }
    )

    const { data: dataSources } = await PublishedBuilderService(
      this.$client
    ).fetchDataSources(page.id)

    commit('CLEAR_ITEMS', { page })
    await Promise.all(
      dataSources.map((dataSource) =>
        dispatch('forceCreate', { page, dataSource })
      )
    )
    commit('SET_LOADING', { page, value: false })

    return dataSources
  },
  async move({ dispatch }, { page, dataSourceId, beforeDataSourceId }) {
    await dispatch('forceMove', { page, dataSourceId, beforeDataSourceId })

    try {
      await DataSourceService(this.$client).move(
        dataSourceId,
        beforeDataSourceId
      )
    } catch (error) {
      await dispatch('forceMove', {
        page,
        dataSourceId: beforeDataSourceId,
        beforeDataSourceId: dataSourceId,
      })
      throw error
    }
  },
  async duplicate({ commit, getters, dispatch }, { page, dataSourceId }) {
    const dataSourcesOfPage = getters.getPageDataSources(page)
    const dataSource = dataSourcesOfPage.find((e) => e.id === dataSourceId)
    commit('SET_LOADING', { page, value: true })
    await dispatch('create', {
      page,
      dataSourceType: dataSource.type,
      beforeId: dataSource.id,
    })
    commit('SET_LOADING', { page, value: false })
  },
}

const getters = {
  getPageDataSources: (state) => (page) => {
    return page.dataSources
  },
  getPageDataSourceById: (state) => (page, id) => {
    return page.dataSources.find((dataSource) => dataSource.id === id)
  },
  getLoading: (state) => (page) => {
    return page._.dataSourceLoading
  },
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
}