/**
 * Data helpers
 *
 */
import { clone, path } from "ramda"
import type { QueryParamsInput } from "@racletteCore/types"
import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
import log from "@racletteCore/lib/logger"
import { $eventbus } from "@racletteCore/main"
import { $store } from "@racletteCore/main"

const handleErrors = (e) => {
  $eventbus.emit("backend/error", e)
  log.api("handleErrors", e)
  // TODO expand error handling for other transports
  if (e.name === "AxiosError") {
    const errorResponse = e.response || {}
    $eventbus.emit("ui/addToSnackbar", {
      message: errorResponse.data?.message || "error",
      color: "error",
    })
    throw e
  }
  return {}
}
const hasOwnProperty = (obj, prop) =>
  Object.prototype.hasOwnProperty.call(obj, prop)
const fileExists = async (path: string): Promise<boolean> => {
  try {
    const response = await fetch(path, { method: "HEAD" })
    return response.ok
  } catch {
    return false
  }
}
const capitalize = (string: string) =>
  string.charAt(0).toUpperCase() + string.slice(1)

const handleDataIdPrefixing = (prefix, id) => {
  if (!prefix) {
    return id
  }
  if (id.indexOf(prefix) === 0) {
    return id
  }
  if (id.indexOf(prefix) !== -1) {
    console.error("prefix is in id but not as a prefix oO")

    return id
  }

  return prefix + id
}
const markDataAsModified = (data) => {
  const modifiedData = clone(data)
  modifiedData?.items.forEach((item) => {
    item._id = `m_${item._id}`
  })
  return modifiedData
}
const debounceHelper = (time = 500) => {
  let debounceTimer = false

  return (func) => {
    if (func) {
      clearTimeout(debounceTimer)
      debounceTimer = setTimeout(() => {
        if (func) {
          func()
        }
      }, time)
    } else {
      clearTimeout(debounceTimer)
    }
  }
}

const awaitTime = (ms = 100) =>
  new Promise((resolve) => setTimeout(resolve, ms))

const getCookie = (name: string): string | null => {
  const value = `; ${document.cookie}`
  const parts = value.split(`; ${name}=`)

  if (parts.length === 2) {
    return parts.pop()?.split(";").shift() || null
  }

  return null
}

const getItemFromLookup = (lookup, stateKey) => {
  if (!lookup) {
    return {}
  }
  if (!lookup[stateKey]) {
    return {}
  }

  return lookup[stateKey]
}

const getItemsFromLookup = (lookup, attributeArr, valArr) => {
  if (!lookup) {
    return []
  }
  const resultArr = []

  Object.keys(lookup).forEach((key) => {
    let itemAttributeValue

    if (Array.isArray(attributeArr)) {
      itemAttributeValue = path(attributeArr, lookup[key])
    }
    if (typeof attributeArr === "string") {
      itemAttributeValue = lookup[key][attributeArr]
    }
    if (typeof valArr === "string" && itemAttributeValue == valArr) {
      return resultArr.push(lookup[key])
    }
    if (Array.isArray(valArr) && valArr.indexOf(itemAttributeValue) > -1) {
      return resultArr.push(lookup[key])
    }
  })

  return resultArr
}

const getValueFromLookupItem = (lookup, stateKey, attributeArr) => {
  if (!lookup) {
    return false
  }
  if (!lookup[stateKey]) {
    return false
  }
  if (Array.isArray(attributeArr)) {
    return path(attributeArr, lookup[stateKey])
  }
  if (typeof attributeArr === "string") {
    return lookup[stateKey][attributeArr]
  }
}

const getValueFromLookup = (lookup, attributeArr) => {
  if (!lookup) {
    return false
  }
  if (Array.isArray(attributeArr)) {
    return path(attributeArr, lookup)
  }
  if (typeof attributeArr === "string") {
    return lookup[attributeArr]
  }

  return false
}

const fixDataTypesFromObject = (obj) => {
  for (const key in obj) {
    if (typeof obj[key] === "object" && obj[key] !== null) {
      // If the property is an object, recurse into it
      fixDataTypesFromObject(obj[key])
    } else if (obj[key] === "true") {
      // If the property is the string "true", convert it to a boolean
      obj[key] = true
    } else if (obj[key] === "false") {
      // If the property is the string "false", convert it to a boolean
      obj[key] = false
    } else if (!isNaN(obj[key])) {
      // If the property is a stringified number, convert it to a number
      // parseFloat is used to properly handle floats
      obj[key] = parseFloat(obj[key])
    }
  }

  return obj
}
/**
 * deepFreeze
 *
 * @param { object } obj, an object to deepFreeze
 * @return { object } a deepFrozen object
 *
 * deepFreeze an object to prevent manipulation
 */
const deepFreeze = (obj) => {
  Object.keys(obj).forEach((property) => {
    if (typeof obj[property] === "object" && !Object.isFrozen(obj[property])) {
      deepFreeze(obj[property])
    }
  })

  return Object.freeze(obj)
}

/**
 * replaceLast
 *
 * @param { string } str, The string to search in
 * @param { string } pattern, The string to replace
 * @param { string } replacement, The replacement
 * @return { string } a manipulated string
 *
 * Will replace the last occurance of a given pattern in a given string with a given replacement
 */
const replaceLast = (str, pattern, replacement) => {
  const match =
    typeof pattern === "string"
      ? pattern
      : (str.match(new RegExp(pattern.source, "g")) || []).slice(-1)[0]

  if (!match) {
    return str
  }
  const last = str.lastIndexOf(match)

  return last !== -1
    ? `${str.slice(0, last)}${replacement}${str.slice(last + match.length)}`
    : str
}

/**
 * removeCacheAttributes
 *
 * @param { object } item, a dataStore item which needs it's cache attributes removed
 * @return { object } item, a manipulated dataStore item
 *
 */
const removeCacheAttributes = (item) => {
  if (typeof FormData !== "undefined" && item instanceof FormData) {
    return item
  }

  const _item = clone(item)

  if (_item.__requestedFor) {
    delete _item.__requestedFor
  }

  return _item
}

const analyzeOperationResults = (data) => {
  // Initialize result object only when needed
  const result = {}

  // Process each operation type (create, update, delete)
  for (const operationType of ["create", "update", "delete"]) {
    const operations = data[operationType]

    // Skip if no operations of this type
    if (!operations?.length) {
      continue
    }

    // Count operations in a single pass
    const counts = operations.reduce((acc, operation) => {
      const itemType = operation.operation?.type || "unknown"
      const category = operation.success ? "success" : "failure"

      // Initialize nested structure only when needed
      if (!acc[category]) {
        acc[category] = {}
      }

      acc[category][itemType] = (acc[category][itemType] || 0) + 1

      return acc
    }, {})

    // Only add to result if there are counts
    if (Object.keys(counts).length > 0) {
      result[operationType] = counts
    }
  }

  return result
}
const transformOperationResults = (data) => {
  const result = {
    pushed: [],
    deleted: [],
    deleteFailed: [],
    backendOriginal: [],
    conflictedVersions: [],
  }

  // Process create and update operations for successful pushes
  ;["create", "update"].forEach((operationType) => {
    const operations = data[operationType] || []

    operations.forEach((operation) => {
      if (operation.success) {
        result.pushed.push(operation.operation.item)
      } else if (operation.error) {
        // Handle failed operations
        if (operation.error.backendItem) {
          result.backendOriginal.push(operation.error.backendItem)
        }
        if (operation.operation.item) {
          result.conflictedVersions.push(operation.operation.item)
        }
      }
    })
  })

  // Process delete operations
  const deleteOperations = data.deleted || []

  deleteOperations.forEach((operation) => {
    if (operation.success) {
      // Add _id to delete array if operation succeeded
      result.deleted.push(operation.operation.id)
    } else {
      // Add _id to deleteFailed array if operation failed
      result.deleteFailed.push(operation.operation.id)

      // Handle backend items and conflicted versions for failed deletes
      if (operation.error?.backendItem) {
        result.backendOriginal.push(operation.error.backendItem)
      }
      if (operation.operation.item) {
        result.conflictedVersions.push(operation.operation.item)
      }
    }
  })

  // Remove empty arrays from result
  Object.keys(result).forEach((key) => {
    if (result[key].length === 0) {
      delete result[key]
    }
  })

  return result
}

/**
 * handleParentChildCountChange
 *
 * @param { object } _item, a dataStore item which needs it's childcount updated
 * @param { number } count, the count to be applied
 * @param { string } direction, either up or down
 * @return { object } item, a manipulated dataStore item
 *
 * Will update the _childCount attribute of a given dataItem to reflect changes while offline
 */
const handleParentChildCountChange = (_item, count, direction) => {
  const item = clone(_item)

  if (!item._meta) {
    item._meta = {}
  }
  let newChildCount = item?._meta?.childCount || 0

  if (direction === "up") {
    newChildCount += parseInt(count)
  }
  if (direction === "down") {
    newChildCount -= parseInt(count)
  }
  item._meta.childCount = newChildCount

  return item
}

/**
 * addItemDefaulAttributes
 *
 * @param { object } item, a dataStore item which needs to get fake backend response values to make them offline ready
 * @param { object } user, The user lookup object from the $store containing the currently signed in user account id
 * @return { object } item, a manipulated dataStore item for offline and online use
 *
 * will add metadata which also get set by the backend to a given item. The backend will override those values anyway but for
 * offline functionality we need those attributes set and updated
 */
const addItemDefaulAttributes = (item, user) => {
  item._source.updatedBy = user._id
  item._source.updatedAt = new Date().toISOString()
  item._updatedAt = item._source.updatedAt
  if (!item._source.createdAt) {
    item._source.createdAt = item._updatedAt
  }
  item._createdAt = item._source.createdAt
  item._offline = 1

  return item
}

/**
 * rememberLastDataUpdateRetrieval
 *
 * @param { string } projectId, The current projectId
 *
 * Will store the current time to the localStorage to determine the last time we had contact with the backend
 */
const rememberLastDataUpdateRetrieval = (projectId) => {
  const currentProject = projectId

  log.api("rememberLastDataUpdateRetrieval", currentProject)
  localStorage.setItem(`lastUpdate_${currentProject}`, new Date().toISOString())
}

const getValueFromNestedAttributeString = (attrString, item) => {
  if (attrString.indexOf(".") > -1) {
    const attributeChain = attrString.split(".")
    let attributeValue = false

    attributeChain.forEach((attr) => {
      if (!attributeValue) {
        attributeValue = item[attr]
      } else {
        attributeValue = attributeValue[attr]
      }
    })

    return attributeValue
  }

  return item[attrString]
}

const isAnswerToOwnRequest = (appSessionId) =>
  appSessionId == $store.appSessionId

const NUMERIC_QUERY_OPTIONS = ["from", "skip", "size", "limit"] as const

/** Clone query params and coerce `options.from|skip|size|limit` to integers (often strings from URLs/forms). */
const fixQueryParamTypes = (params: Record<string, unknown> = {}) => {
  const fixedParams = clone(params) as Record<string, unknown>
  const options = fixedParams.options

  if (options == null || typeof options !== "object" || Array.isArray(options)) {
    return fixedParams
  }

  const opts = options as Record<string, unknown>
  for (const key of NUMERIC_QUERY_OPTIONS) {
    if (opts[key] != null) {
      opts[key] = parseInt(String(opts[key]), 10)
    }
  }

  return fixedParams
}

const isPlainParamsRecord = (
  value: unknown,
): value is Record<string, unknown> =>
  value != null && typeof value === "object" && !Array.isArray(value)

const warnInvalidResolvedQueryParams = (value: unknown): void => {
  if (!isDevelopmentMode()) return
  const kind =
    value == null ? String(value) : Array.isArray(value) ? "array" : typeof value
  log.warning(
    "Invalid query params",
    `Params subscriber resolved to ${kind}, expected a plain object. Using {}.`,
  )
}

/** Resolve plain params or subscriber, then normalize query param types. */
const resolveQueryParams = (
  params?: QueryParamsInput,
): Record<string, unknown> => {
  let resolved: unknown = params
  if (typeof resolved === "function") {
    resolved = resolved()
  }
  if (!isPlainParamsRecord(resolved)) {
    warnInvalidResolvedQueryParams(resolved)
    return fixQueryParamTypes({})
  }
  return fixQueryParamTypes(resolved)
}

const promisifyActionResolve = (resolveId: string, callback?: Function) => {
  const handleCleanup = (result = {}) => {
    if (typeof callback === "function") {
      return callback(result)
    }
  }
  return new Promise(async (resolve, reject) => {
    let wasResolved = false
    $eventbus.once("store_actionResolved_" + resolveId, (result) => {
      handleCleanup(result)
      wasResolved = true
      return resolve(result)
    })
    await awaitTime(5000)
    if (!wasResolved) {
      handleCleanup()
      reject(`action ${resolveId} was not resolved within 5000ms`)
    }
  })
}
export {
  markDataAsModified,
  promisifyActionResolve,
  capitalize,
  handleDataIdPrefixing,
  analyzeOperationResults,
  transformOperationResults,
  getValueFromNestedAttributeString,
  rememberLastDataUpdateRetrieval,
  removeCacheAttributes,
  handleParentChildCountChange,
  addItemDefaulAttributes,
  replaceLast,
  deepFreeze,
  debounceHelper,
  isAnswerToOwnRequest,
  fixQueryParamTypes,
  resolveQueryParams,
  fixDataTypesFromObject,
  getItemsFromLookup,
  getItemFromLookup,
  getValueFromLookupItem,
  getValueFromLookup,
  awaitTime,
  getCookie,
  hasOwnProperty,
  fileExists,
  handleErrors,
}
