/**
 * data reducers
 */
import { applicationState } from "../../state"
import {
  replaceLast,
  addItemDefaulAttributes,
  handleParentChildCountChange,
  handleDataIdPrefixing,
} from "@racletteCore/lib/dataHelper"
import { mergeDeepRight, clone, difference } from "ramda"
import { v4 as uuidv4 } from "uuid"
import * as dbItems from "./dbItems/index"

const addDataToDataStore = (state, action, remote = false) => {
  const { result, actionIdentifier } = action.payload
  const { actionId, cacheKey } = actionIdentifier
  const newState = structuredClone(state)

  result.forEach((_item) => {
    const item = structuredClone(_item)

    item.__requestedFor = handleRequestActionIdRegistry(
      item,
      state[item._id],
      actionId,
      cacheKey,
    )
    if (!state[item._id] || remote) {
      // if the item is completely new to the cache, or from a remote query we'll just add it
      return (newState[item._id] = item)
    }
    if (state[item._id]) {
      if (!item._updatedAt && !state[item._id]._updatedAt) {
        newState[item._id] = item
      }
      if (
        item._updatedAt &&
        state[item._id]._updatedAt &&
        new Date(item._updatedAt) > new Date(state[item._id]._updatedAt)
      ) {
        newState[item._id] = item
      }
      newState[item._id].__requestedFor = item.__requestedFor
    }
  })

  return newState
}

/**
 * applyItemTemplate
 *
 * @param { object } item, the required partial for item we wan't to create
 * @param { string } type, The type of the docItem to create
 * @return { object } item, a docItem merged with the objectTemplate for a given type
 *
 * will take the required item partials and merges them with the object template to ensure offline functionallity
 */
const applyItemTemplate = (item, type) => {
  // TODO add dynamic plugin datatype schema retrieval here
  let itemTemplate = dbItems[type]
  if (!itemTemplate) {
    console.warn(`no dbItem template for ${type} found`)
  }
  itemTemplate = dbItems.defaultItem(type, item)

  return itemTemplate
}

/**
 * handleRequestActionIdRegistry
 *
 * @param { object } newItem, the version of an item we want to put into the cache
 * @param { object } oldItem, the old version of the item we currently want to add to cache
 * @param { string } actionId, the actionId responsible for this item to be loaded into the cache
 * @param { string } cacheKey, the cacheKey from the queriesCache
 * @return { object } __requestedFor, a map containing all the actionIds and the dateTime they were handled
 *
 * will take an item from api and will make sure that all request actionIds are
 * properly handled to be able to remove items from cache if not requested by any action anymore
 */
const handleRequestActionIdRegistry = (
  newItem,
  oldItem,
  actionId = false,
  cacheKey = false,
) => {
  if (!newItem.__requestedFor) {
    newItem.__requestedFor = {}
  }
  // add the actual actionId if one was supplied
  if (actionId) {
    newItem.__requestedFor[actionId] = new Date().toISOString()
  }
  if (cacheKey) {
    newItem.__requestedFor[cacheKey] = new Date().toISOString()
  }
  if (!oldItem || !oldItem.__requestedFor) {
    return newItem.__requestedFor
  }
  newItem.__requestedFor = mergeDeepRight(
    oldItem.__requestedFor,
    newItem.__requestedFor,
  )

  return newItem.__requestedFor
}

/**
 * updateDataCache
 *
 * @param { object } oldCache, the dataStore before the new item update from the backend
 * @param { array } newItems, new items we just received from the backend
 * @return { object } newCache, a updated dataStore
 *
 * whenever we receive data item updates from the backend we want to update those items
 * we currently have in our dataStore.
 * */
const updateDataCache = (oldCache, newItems) => {
  const newCache = structuredClone(oldCache)

  newItems.forEach((item) => {
    const oldItem = oldCache[item._id]

    // Tickets will just get updated if they have already been in the data Store due to a query
    if (oldItem) {
      newCache[item._id] = item
    }
    // apply the requestAction registry handler
    if (newCache[item._id]) {
      newCache[item._id].__requestedFor = handleRequestActionIdRegistry(
        newCache[item._id],
        oldItem,
      )
    }
    // we will always delete the offline flag as this function will only be called by the dataRead which retrieves synced backend items
    if (newCache[item._id]) {
      delete newCache[item._id]._offline
    }
  })

  return newCache
}

/**
 * init
 */
const init = (state, action) => applicationState.data
/**
 * project init
 */
const projectInit = (state, action) => applicationState.data

/**
 * Adds data to the state based on the action payload coming from a query.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains two properties: `payload` and `actionId`.
 *   @param {Array} action.payload - An array of items to be added to the state. Each item is an object that must contain an `_id` property.
 *   @param {string} action.actionId - A unique identifier for the action.
 * @returns {Object} - Returns the new state after adding the data from the action payload.
 */
const dataAddFromClient = (state, action) => {
  // this was formerly used by the offline mode, maybe remove this from core?
  const { result } = action.payload
  if (!result || !Array.isArray(result)) {
    return state
  }
  const newState = addDataToDataStore(state, action)
  return newState
}

const dataAddFromServer = (state, action) => {
  const { result } = action.payload
  if (!result || !Array.isArray(result)) {
    return state
  }

  action.actionResult.added = result
  const newState = addDataToDataStore(state, action, true)

  return newState
}

/**
 * Pushes data to the state based on the action payload coming from the socket.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {Array} action.payload - An array of items to be added to the state. Each item is an object that must contain an `_id` property.
 * @returns {Object} - Returns the new state after pushing the data from the action payload.
 */
const dataRead = (state, action) => {
  const { payload } = action

  // early return if no data
  if (!payload) {
    return state
  }
  if (!Array.isArray(payload)) {
    console.error("data push needs to be an array!")

    return state
  }

  action.actionResult.added = action.payload
  const newState = updateDataCache(state, payload)

  return newState
}

/**
 * Creates a new data Item in the state and the backend based on the action payload.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {Object} action.payload - An object containing the properties `item`, `type`, and `actionId`.
 *     @param {Object} action.payload.item - The item to be created.
 *     @param {string} action.payload.type - The type of the item.
 *     @param {string} action.payload.actionId - The ID of the action.
 * @returns {Object} - Returns the new state after creating the data from the action payload.
 */
const dataCreate = (state, action) => {
  const { project, user } = action.storeLookup
  const item = clone(action.payload.item)
  const type = action.payload.type
  const actionId = action.payload.actionId
  const docIdPrefix = action.docIdPrefix
  let parent = false

  if (!item._id) {
    item._id = uuidv4()
  }
  if (docIdPrefix) {
    item._id = handleDataIdPrefixing(docIdPrefix, item._id)
  }
  if (!item.project) {
    item.project = project._id
  }
  if (!item.owner) {
    item.owner = user._id
  }

  if (state[item.parent]) {
    parent = clone(state[item.parent])
  }

  const updateObj = {}

  updateObj[item._id] = addItemDefaulAttributes(
    applyItemTemplate(item, type),
    user,
  )
  // handle on the fly create
  updateObj[item._id].__requestedFor = handleRequestActionIdRegistry(
    updateObj[item._id],
    false,
    actionId,
  )
  if (parent) {
    action.actionResult.updated.itemsBefore[item.parent] = parent
    updateObj[item.parent] = handleParentChildCountChange(parent, 1, "up")
  }
  if (item.tags) {
    // lets raise the child count for all tags which re removed from the item -> needed for query triggering
    item.tags.forEach((docId) => {
      if (state[docId]) {
        const tag = clone(state[docId])

        action.actionResult.updated.itemsBefore[docId] = tag

        // lets add the tag to our update object as we will update the tag while we update the item
        updateObj[docId] = handleParentChildCountChange(tag, 1, "up")
      }
    })
  }
  action.actionResult.updated.items = updateObj

  // Add item to actionResult to handle in dataEffect
  action.actionResult.created.push({
    item,
    type,
  })

  /* Parent childcount for parents which where not present in the dataStore will be handled in the effects */
  return { ...state, ...updateObj }
}

/**
 * Updates a data item in the state and the backend based on the action.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed.
 *   @param {Object} action.payload - An object containing the update patch for the _source of the dataItem.
 *   @param {string} action.docId - The UUID of the document.
 *   @param {string} action.override - if true, the values from the payload will override the ones from the item instead of getting merged into them
 * @returns {Object} - Returns the new state after updating the data from the action.
 */
const dataUpdate = (state, action) => {
  const { user } = action.storeLookup
  const updateObj = {}
  const { docId, payload, override, docIdPrefix } = action

  const documentId = handleDataIdPrefixing(docIdPrefix, docId)

  if (!documentId) {
    return state
  }

  const oldItem = state[documentId]

  if (!oldItem) {
    action.actionResult.updated.items[docId] = payload

    return state
  }
  // remember the item itself in it's old state
  action.actionResult.updated.itemsBefore[documentId] = oldItem
  const item = clone(oldItem)

  // merge our new payload onto the old item, be it from the object definition or the actual old state

  if (override) {
    for (const [key, value] of Object.entries(payload)) {
      item._source[key] = value
    }
  } else {
    item._source = mergeDeepRight(item._source, payload)
  }
  if (
    payload.tags &&
    clone(payload.tags) !== clone(oldItem._source.tags || [])
  ) {
    // we have a change in tags, so let's find out what happened and change the childCount values for the tags accordingly
    const tagsAdded = difference(payload.tags, oldItem._source.tags || [])
    const tagsRemoved = difference(oldItem._source.tags || [], payload.tags)

    if (tagsRemoved.length > 0) {
      // lets lower the child count for all tags which re removed from the item -> needed for query triggering
      tagsRemoved.forEach((docId) => {
        if (state[docId]) {
          const tag = clone(state[docId])

          action.actionResult.updated.itemsBefore[docId] = tag

          // lets add the tag to our update object as we will update the tag while we update the item
          updateObj[docId] = handleParentChildCountChange(tag, 1, "down")
        }
      })
    }
    if (tagsAdded.length > 0) {
      // lets raise the child count for all tags which re removed from the item -> needed for query triggering
      tagsAdded.forEach((docId) => {
        if (state[docId]) {
          const tag = clone(state[docId])

          action.actionResult.updated.itemsBefore[docId] = tag

          // lets add the tag to our update object as we will update the tag while we update the item
          updateObj[docId] = handleParentChildCountChange(tag, 1, "up")
        }
      })
    }
  }
  const updatedItem = addItemDefaulAttributes(item, user)

  updateObj[documentId] = updatedItem

  // Add item to actionResult to handle in dataEffect

  action.actionResult.updated.items[documentId] = updatedItem

  return { ...state, ...updateObj }
}

/**
 * Deletes data from the dataCache based on the action payload.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {Array} action.payload - An Array containing the uuids to be deleted
 * @returns {Object} - Returns the new state after deleting the data from the action payload.
 */
const dataHardDelete = (state, action) => {
  if (!action.payload) {
    return state
  }

  const newState = clone(state)

  action.payload.forEach((uuid) => {
    action.actionResult.deleted.hard[uuid] = newState[uuid]
    if (newState[uuid]) {
      delete newState[uuid]
    }
  })

  return newState
}

/**
 * data delete Item from backend (soft delete in cache until confirmed from backend)
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {Object} action.payload - An Array containing the items to be deleted
 *     @param {String} action.payload.docId - The UUID of the document to be deleted
 *     @param {String} action.payload.type - The type of the document
 *     @param {String} action.payload.requestCode - An optional request Code to be retrieved in socket response to link request and answer
 * @returns {Object} - Returns the new state after deleting the data from the action payload.
 */
const dataDelete = (state, action) => {
  if (!action.payload) {
    return state
  }
  const { user } = action.storeLookup
  const payload = action.payload
  const updateObj = {}
  /* deletes from items which are not in cache will be handled by effects */
  const documentId = handleDataIdPrefixing(payload.docIdPrefix, payload.docId)

  if (state[documentId]) {
    updateObj[documentId] = addItemDefaulAttributes(
      clone(state[documentId]),
      user,
    )
    // TODO decide for one flag
    updateObj[documentId]._source.isDeleted = true
    updateObj[documentId]._source.trash = true
    updateObj[documentId]._deleted = true
  }
  action.actionResult.deleted.soft[documentId] = updateObj[documentId] || {}
  action.actionResult.deleted.soft[documentId]._type = payload.type

  return { ...state, ...updateObj }
}

/**
 * Removes data from the dataCache based on the action payload.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {Array} action.payload - An Array containing the uuids to be deleted
 *   @param {String} action.actionId - The Id of the query which requested the items
 * @returns {Object} - Returns the new state after removing the data from the cache.
 */
const deleteFromCache = (state, action) => {
  const { payload, actionId } = action
  const dataItems = clone(state)

  if (actionId) {
    const iterateItems =
      payload?.uuids && payload?.uuids?.length
        ? payload?.uuids
        : Object.keys(dataItems)
    if (iterateItems.length) {
      iterateItems.forEach((uuid) => {
        if (!dataItems[uuid]) {
          return
        }
        if (!dataItems[uuid].__requestedFor) {
          delete dataItems[uuid]
        }
        // remove the indicator that this item was requested for the query in deletion process
        if (dataItems[uuid].__requestedFor[actionId]) {
          delete dataItems[uuid].__requestedFor[actionId]
        }
        // if item has no other requesters (i.e also not the global requestor) we can delete the item from the store as it is not needed anymore
        if (!Object.keys(dataItems[uuid].__requestedFor).length) {
          delete dataItems[uuid]
        }
      })
    }
  }

  return dataItems
}

/**
 * Moves a dataItem to a new parent in the state based on the action payload.
 * @param {Object} state - The current state of the data. This is a key-value store where the key is the `_id` of the item and the value is the item itself.
 * @param {Object} action - The action to be performed. This object contains the `payload` property.
 *   @param {String} action.docId - The UUID of the document which should be moved
 *   @param {Object} action.payload - the payload object
 *     @param {String} action.payload.newParent - The uuid of the new parent document
 *     @param {String} action.payload.oldParent - The uuid of the old parent document
 * @returns {Object} - Returns the new state after moving the data from the action payload.
 */
const dataMove = (state, action) => {
  const { user } = action.storeLookup
  const docId = handleDataIdPrefixing(action.docIdPrefix, action.docId)
  const { newParent, oldParent } = action.payload
  const item = clone(state[docId])

  action.actionResult.moved = {
    from: oldParent,
    to: newParent,
    item: item,
  }

  if (!item) {
    console.error("item moved which is not in dataStore!")
  }
  if (!item._meta) {
    item._meta = {}
  }
  if (!item?._meta?.path) {
    console.error("item has no path!")
  }
  const updateObj = {}

  item._source.parent = newParent
  // replace the last occurance of the oldParent uuid in the items two path params with the new parents uuid -> ??? -> Profit
  item._meta.path = replaceLast(item._meta.path, oldParent, newParent)
  // handle childcounts for old and new parents if they have been in the cache already
  if (state[oldParent]) {
    updateObj[oldParent] = handleParentChildCountChange(
      state[oldParent],
      1,
      "down",
    )
  }
  if (state[newParent]) {
    updateObj[newParent] = handleParentChildCountChange(
      state[newParent],
      1,
      "up",
    )
  }

  updateObj[docId] = addItemDefaulAttributes(item, user)
  action.actionResult.updated.items = updateObj

  /* Parent childcount for parents which where not present in the dataStore will be handled in the effects */
  return { ...state, ...updateObj }
}
const addRequestedFor = (state, action) => {
  const { actionId, uuids } = action.payload
  if (!actionId) return state
  const newState = structuredClone(state)
  return newState
}
export {
  init,
  projectInit,
  dataRead,
  dataUpdate,
  dataMove,
  dataCreate,
  deleteFromCache,
  dataDelete,
  dataHardDelete,
  dataAddFromClient,
  dataAddFromServer,
  addRequestedFor,
}
