/*
eslint
@typescript-eslint/explicit-function-return-type: 0,
@typescript-eslint/no-explicit-any: 0,
no-var: 0
*/
import { serializeError } from 'serialize-error'
import { reactive, isReactive } from 'vue'
import { updateOriginal } from './utils.vue3'
import {
  models,
  ServiceState,
  assignTempId,
  getId,
  getQueryInfo,
  PendingServiceMethodName,
  PendingIdServiceMethodName,
} from '@feathersjs/vuex-commons'
import _omit from 'lodash/omit'
import _get from 'lodash/get'
import _isObject from 'lodash/isObject'
import { Id } from '@feathersjs/feathers'

export default function makeServiceMutations() {
  function addItems(state, items) {
    const { serverAlias, idField, tempIdField, modelName } = state
    const Model = _get(models, [serverAlias, modelName])
    const BaseModel = _get(models, [serverAlias, 'BaseModel'])

    for (let item of items) {
      const id = getId(item, idField)
      const isTemp = id === null || id === undefined

      // If the response contains a real id, remove isTemp
      if (id != null) {
        delete item.__isTemp
      }

      if (Model && !(item instanceof BaseModel) && !(item instanceof Model)) {
        item = new Model(item, { commit: false })
      }

      if (isTemp) {
        let tempId = item[tempIdField]
        if (tempId == null) {
          tempId = assignTempId(state, item)
        }
        item.__isTemp = true
        state.tempsById[tempId] = item
      } else {
        // Only add the id if it's not already in the `ids` list.
        if (!state.ids.includes(id)) {
          state.ids.push(id)
        }
        state.keyedById[id] = item
      }
    }
  }

  function updateItems(state, items) {
    const { idField, replaceItems, addOnUpsert, serverAlias, modelName } = state
    const Model = _get(models, [serverAlias, modelName])
    const BaseModel = _get(models, [state.serverAlias, 'BaseModel'])

    for (let item of items) {
      const id = getId(item, idField)

      // If the response contains a real id, remove isTemp
      if (id != null) {
        delete item.__isTemp
      }

      // Update the record
      if (id !== null && id !== undefined) {
        if (state.ids.includes(id)) {
          // Completely replace the item
          if (replaceItems) {
            if (Model && !(item instanceof Model)) {
              item = new Model(item)
            }
            state.keyedById[id] = item
            // Merge in changes
          } else {
            /**
             * If we have a Model class, calling new Model(incomingData) will call update
             * the original record with the accessors and setupInstance data.
             * This means that date objects and relationships will be preserved.
             *
             * If there's no Model class, just call updateOriginal on the incoming data.
             */
            if (Model && !(item instanceof BaseModel) && !(item instanceof Model)) {
              item = new Model(item)
            } else {
              const original = state.keyedById[id]
              updateOriginal(original, item)
            }
          }

          // if addOnUpsert then add the record into the state, else discard it.
        } else if (addOnUpsert) {
          state.ids.push(id)
          state.keyedById[id] = item
        }
        continue
      }
    }
  }

  function mergeInstance(state, item) {
    const { idField } = state
    const id = getId(item, idField)
    const existingItem = state.keyedById[id]
    if (existingItem) {
      merge(existingItem, item)
    }
  }

  function merge(state, { dest, source }) {
    merge(dest, source)
  }

  return {
    addItems,
    updateItems(state, items) {
      if (!Array.isArray(items)) {
        throw new Error('You must provide an array to the `updateItems` mutation.')
      }
      updateItems(state, items)
    },
    mergeInstance,
    merge,
    addItem(state, item) {
      addItems(state, [item])
    },
    updateItem(state, item) {
      updateItems(state, [item])
    },

    // Promotes temp to "real" item:
    // - adds _id to temp
    // - removes __isTemp flag
    // - migrates temp from tempsById to keyedById
    updateTemp(state, { id, tempId }) {
      const temp = state.tempsById[tempId]
      if (temp) {
        temp[state.idField] = id
        delete temp.__isTemp
        delete state.tempsbyId[tempId]
        // If an item already exists in the store from the `created` event firing
        // it will be replaced here
        state.keyedById[id] = temp
        // Only add the id if it's not already in the `ids` list.
        if (!state.ids.includes(id)) {
          state.ids.push(id)
        }
      }

      // Add _id to temp's clone as well if it exists
      const Model = _get(models, [state.serverAlias, state.modelName])
      const tempClone = Model && Model.copiesById && Model.copiesById[tempId]
      if (tempClone) {
        tempClone[state.idField] = id
        Model.copiesById[id] = tempClone
        delete tempClone.__isTemp
      }
    },

    removeItem(state, item) {
      const { idField } = state
      const idToBeRemoved = _isObject(item) ? getId(item, idField) : item
      const isIdOk = idToBeRemoved !== null && idToBeRemoved !== undefined
      const index = state.ids.findIndex(i => i === idToBeRemoved)

      const Model = _get(models, `[${state.serverAlias}][${state.modelName}]`)
      const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById

      if (isIdOk && index !== null && index !== undefined) {
        state.ids = state.ids.filter((id, ind) => id && ind !== index)
        delete state.keyedById[idToBeRemoved]
        if (copiesById.hasOwnProperty(idToBeRemoved)) {
          delete copiesById[idToBeRemoved]
        }
      }
    },

    // Removes temp records
    removeTemps(state, tempIds) {
      tempIds.forEach(id => {
        const temp = state.tempsById[id]
        if (temp) {
          if (temp[state.idField]) {
            delete temp.__isTemp
          }
        }
      })
      state.tempsById = _omit(state.tempsById, tempIds)
    },

    removeItems(state, items) {
      const { idField } = state

      if (!Array.isArray(items)) {
        throw new Error('You must provide an array to the `removeItems` mutation.')
      }
      // Make sure we have an array of ids. Assume all are the same.
      const containsObjects = items[0] && _isObject(items[0])
      const idsToRemove = containsObjects ? items.map(item => getId(item, idField)) : items
      const mapOfIdsToRemove = idsToRemove.reduce((map, id) => {
        map[id] = true
        return map
      }, {})

      const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath])
      const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById

      idsToRemove.forEach(id => {
        delete state.keyedById[id]
        if (copiesById.hasOwnProperty(id)) {
          delete copiesById[id]
        }
      })

      // Get indexes to remove from the ids array.
      const mapOfIndexesToRemove = state.ids.reduce((map, id, index) => {
        if (mapOfIdsToRemove[id]) {
          map[index] = true
        }
        return map
      }, {})
      // Remove highest indexes first, so the indexes don't change
      const indexesInReverseOrder = Object.keys(mapOfIndexesToRemove).sort((a, b) => {
        if (a < b) {
          return 1
        } else if (a > b) {
          return -1
        } else {
          return 0
        }
      })
      indexesInReverseOrder.forEach(indexInIdsArray => {
        state.ids = state.ids.filter((id, index) => index !== indexInIdsArray)
      })
    },

    clearAll(state) {
      state.ids = []
      state.keyedById = {}

      if (state.keepCopiesInStore) {
        state.copiesById = {}
      } else {
        const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath])
        Object.keys(Model.copiesById).forEach(k => {
          delete Model.copiesById[k]
        })
      }
    },

    // Creates a copy of the record with the passed-in id, stores it in copiesById
    createCopy(state, id) {
      const { servicePath, keepCopiesInStore, serverAlias } = state
      const current = state.keyedById[id] || state.tempsById[id]
      const Model = _get(models, [serverAlias, 'byServicePath', servicePath])

      let item

      if (Model) {
        item = new Model(current, { clone: true })
      } else {
        const existingClone = state.copiesById[id]

        item = existingClone ? merge(existingClone, current) : merge({}, current)
      }

      if (keepCopiesInStore) {
        state.copiesById[id] = item
      } else {
        // Make sure model has a copiesById
        if (!Model.hasOwnProperty('copiesById')) {
          Object.defineProperty(Model, 'copiesById', { value: {} })
        }
        // Since it won't be added to the store, make it a Vue object
        if (!isReactive) {
          item = reactive(item)
        }
        Model.copiesById[id] = item
      }
    },

    // Resets the copy to match the original record, locally
    resetCopy(state, id) {
      const { servicePath, keepCopiesInStore } = state
      const Model = _get(models, [state.serverAlias, 'byServicePath', servicePath])
      const copy = keepCopiesInStore
        ? state.copiesById[id]
        : Model && _get(Model, ['copiesById', id])

      if (copy) {
        const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id]
        merge(copy, original)
      }
    },

    // Deep assigns copy to original record, locally
    commitCopy(state, id) {
      const { servicePath, keepCopiesInStore } = state
      const Model = _get(models, [state.serverAlias, 'byServicePath', servicePath])
      const copy = keepCopiesInStore
        ? state.copiesById[id]
        : Model && _get(Model, ['copiesById', id])

      if (copy) {
        const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id]
        merge(original, copy)
      }
    },

    // Removes the copy from copiesById
    clearCopy(state, id) {
      const { keepCopiesInStore } = state
      const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath])

      const copiesById = keepCopiesInStore ? state.copiesById : Model.copiesById

      if (copiesById[id]) {
        delete copiesById[id]
      }
    },

    /**
     * Stores pagination data on state.pagination based on the query identifier
     * (qid) The qid must be manually assigned to `params.qid`
     */
    updatePaginationForQuery(state, { qid, response, query = {} }) {
      const { data, total } = response
      const { idField } = state
      const ids = data.map(i => i[idField])
      const queriedAt = new Date().getTime()
      const { queryId, queryParams, pageId, pageParams } = getQueryInfo({ qid, query }, response)

      if (!state.pagination[qid]) {
        state.pagination[qid] = {}
      }
      if (!query.hasOwnProperty('$limit') && response.hasOwnProperty('limit')) {
        state.pagination.defaultLimit = response.limit
      }
      if (!query.hasOwnProperty('$skip') && response.hasOwnProperty('skip')) {
        state.pagination.defaultSkip = response.skip
      }

      const mostRecent = {
        query,
        queryId,
        queryParams,
        pageId,
        pageParams,
        queriedAt,
        total,
      }

      const qidData = state.pagination[qid] || {}
      Object.assign(qidData, { mostRecent })
      qidData[queryId] = qidData[queryId] || {}
      const queryData = {
        total,
        queryParams,
      }
      Object.assign(qidData[queryId], queryData)

      const pageData = {
        [pageId]: { pageParams, ids, queriedAt },
      }
      Object.assign(qidData[queryId], pageData)

      const newState = Object.assign({}, state.pagination[qid], qidData)

      state.pagination[qid] = newState
    },

    setPending(state, method: PendingServiceMethodName): void {
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      state[`is${uppercaseMethod}Pending`] = true
    },
    unsetPending(state, method: PendingServiceMethodName): void {
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      state[`is${uppercaseMethod}Pending`] = false
    },

    setIdPending(state, payload: { method: PendingIdServiceMethodName; id: Id | Id[] }): void {
      const { method, id } = payload
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      const isIdMethodPending = state[
        `isId${uppercaseMethod}Pending`
      ] as ServiceState['isIdCreatePending']
      // if `id` is an array, ensure it doesn't have duplicates
      const ids = Array.isArray(id) ? [...new Set(id)] : [id]
      ids.forEach(id => {
        if (typeof id === 'number' || typeof id === 'string') {
          isIdMethodPending.push(id)
        }
      })
    },
    unsetIdPending(state, payload: { method: PendingIdServiceMethodName; id: Id | Id[] }): void {
      const { method, id } = payload
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      const isIdMethodPending = state[
        `isId${uppercaseMethod}Pending`
      ] as ServiceState['isIdCreatePending']
      // if `id` is an array, ensure it doesn't have duplicates
      const ids = Array.isArray(id) ? [...new Set(id)] : [id]
      ids.forEach(id => {
        const idx = isIdMethodPending.indexOf(id)
        if (idx >= 0) {
          delete isIdMethodPending[idx]
        }
      })
    },

    setError(state, payload: { method: PendingServiceMethodName; error: Error }): void {
      const { method, error } = payload
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      state[`errorOn${uppercaseMethod}`] = Object.assign({}, serializeError(error))
    },
    clearError(state, method: PendingServiceMethodName): void {
      const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
      state[`errorOn${uppercaseMethod}`] = null
    },
  }
}
