import {
  useState,
  useEffect,
  useContext,
  useCallback,
  forwardRef,
  useImperativeHandle,
  useRef
} from 'react'
import { Form } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { useMessageContext } from '../context/MessageContext'
import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
import { useLocation, useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
import { useActions } from '../context/ActionsContext'

const arrayReplaceCustomizer = (objValue, srcValue) => {
  if (Array.isArray(srcValue)) {
    return srcValue
  }
}

const getUserId = (user) => user?._id || user

const getBeingEditedByOther = (activities, currentUserId) =>
  activities.some(
    (activity) =>
      activity?.mode === 'editing' &&
      getUserId(activity.user) &&
      getUserId(activity.user) !== currentUserId
  )

const isUserEditing = (activities, currentUserId) =>
  activities.some(
    (activity) =>
      activity?.mode === 'editing' && getUserId(activity.user) === currentUserId
  )

const applyActivityUpdate = (activities, activity) => {
  const userId = getUserId(activity?.user)
  if (!userId) {
    return activities
  }

  if (!activity?.mode) {
    return activities.filter((item) => getUserId(item.user) !== userId)
  }

  const existingIndex = activities.findIndex(
    (item) => getUserId(item.user) === userId
  )

  if (existingIndex >= 0) {
    const next = [...activities]
    next[existingIndex] = { ...activities[existingIndex], ...activity }
    return next
  }

  return [...activities, activity]
}

const buildObjectFromEntries = (entries = []) => {
  return entries.reduce((acc, entry) => {
    const { namePath, value } = entry || {}
    if (!Array.isArray(namePath) || value === undefined) {
      return acc
    }
    set(acc, namePath, value)
    return acc
  }, {})
}

const getEditDisabled = (model, objectData, userProfile) => {
  if (model?.readOnly === true) {
    return true
  }

  const editAction = model?.actions?.find((action) => action.name === 'edit')
  if (!editAction?.disabled) {
    return false
  }
  if (typeof editAction.disabled === 'function') {
    return (
      editAction.disabled({ ...objectData, _user: userProfile }) ?? false
    )
  }
  return !!editAction.disabled
}

/**
 * ObjectForm is a reusable form component for editing any object type.
 * It handles fetching, updating, locking, unlocking, and validation logic.
 *
 * Props:
 * - id: string (required)
 * - type: string (required)
 * - formItems: array (for ObjectInfo/ObjectProperty items)
 * - children: function({
 *     loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData, editDisabled
 *   }) => ReactNode
 * - setCurrentObject: boolean (sync object data to ActionsContext)
 * - onStateChange: receives form state including editDisabled (from model edit action)
 */
const ObjectForm = forwardRef(
  (
    { id, type, style, children, onEdit, onStateChange, setCurrentObject = false },
    ref
  ) => {
    const [objectData, setObjectData] = useState(null)
    const serverObjectData = useRef(null)
    const onStateChangeRef = useRef(onStateChange)
    const [fetchLoading, setFetchLoading] = useState(true)
    const [editLoading, setEditLoading] = useState(false)
    const [activities, setActivities] = useState([])
    const [isEditing, setIsEditing] = useState(false)
    const isEditingRef = useRef(false)
    const [formValid, setFormValid] = useState(false)

    const [form] = Form.useForm()
    const formUpdateValues = Form.useWatch([], form)
    const { showSuccess, showError: showMessageError } = useMessageContext()
    const [deleteModalOpen, setDeleteModalOpen] = useState(false)
    const [deleteLoading, setDeleteLoading] = useState(false)
    const {
      fetchObject,
      updateObject,
      deleteObject,
      setObjectActivity,
      clearObjectActivity,
      fetchObjectActivities,
      showError,
      connected,
      subscribeToObjectUpdates,
      subscribeToObjectActivity,
      flushFile
    } = useContext(ApiServerContext)
    const { token, userProfile } = useContext(AuthContext)
    const {
      setCurrentObject: setActionsCurrentObject,
      setCurrentObjectType: setActionsCurrentObjectType
    } = useActions()
    const location = useLocation()
    const navigate = useNavigate()

    const clearAction = useCallback(
      (actionParam = 'action') => {
        const searchParams = new URLSearchParams(location.search)
        searchParams.delete(actionParam)
        const newSearch = searchParams.toString()

        const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
        navigate(newPath, { replace: true })
      },
      [location.pathname, location.search, navigate]
    )

    // Get the model definition for this object type
    const model = getModelByName(type)

    const notifyActivityState = useCallback(
      (nextActivities) => {
        onStateChangeRef.current({
          activities: nextActivities,
          beingEditedByOther: getBeingEditedByOther(
            nextActivities,
            userProfile?._id
          )
        })
      },
      [userProfile?._id]
    )

    // Check if the model has properties with type 'file' or 'fileList'
    const hasFileProperties = useCallback(() => {
      if (!model || !model.properties) return false
      return model.properties.some(
        (property) => property.type === 'file' || property.type === 'fileList'
      )
    }, [model])

    const flushOrphanFiles = useCallback(() => {
      if (!model || !model.properties || !objectData) return

      model.properties.forEach((property) => {
        if (property.type === 'file') {
          // Handle single file property
          const fileId =
            objectData[property.name]?._id || objectData[property.name]
          if (fileId) {
            flushFile(fileId)
          }
        } else if (property.type === 'fileList') {
          // Handle fileList property
          const fileList = objectData[property.name]
          if (Array.isArray(fileList)) {
            fileList.forEach((file) => {
              const fileId = file?._id || file
              if (fileId) {
                flushFile(fileId)
              }
            })
          }
        }
      })
    }, [model, objectData, flushFile])

    // Refs to store current values for cleanup
    const currentIdRef = useRef(id)
    const currentTypeRef = useRef(type)
    const currentIsEditingRef = useRef(isEditing)
    const currentSetObjectActivityRef = useRef(setObjectActivity)
    const currentClearObjectActivityRef = useRef(clearObjectActivity)
    const currentHasFilePropertiesRef = useRef(hasFileProperties)
    const currentFlushOrphanFilesRef = useRef(flushOrphanFiles)
    const fetchedObjectRef = useRef(null)

    // Update refs when values change
    useEffect(() => {
      onStateChangeRef.current = onStateChange
      currentIdRef.current = id
      currentTypeRef.current = type
      currentIsEditingRef.current = isEditing
      currentSetObjectActivityRef.current = setObjectActivity
      currentClearObjectActivityRef.current = clearObjectActivity
      currentHasFilePropertiesRef.current = hasFileProperties
      currentFlushOrphanFilesRef.current = flushOrphanFiles
    })

    // Function to calculate computed values from model properties
    const calculateComputedValues = useCallback(
      (currentData, modelDefinition) => {
        if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
          return []
        }

        // Clone currentData to allow sequential updates
        // We use this working copy to calculate subsequent dependent values
        const workingData = merge({}, currentData)

        const normalizedPath = (name, parentPath = []) => {
          if (Array.isArray(name)) {
            return [...parentPath, ...name]
          }
          if (typeof name === 'number') {
            return [...parentPath, name]
          }
          if (typeof name === 'string' && name.length > 0) {
            return [...parentPath, ...name.split('.')]
          }
          return parentPath
        }

        const getValueAtPath = (dataSource, path) => {
          if (!Array.isArray(path) || path.length === 0) {
            return dataSource
          }
          return path.reduce((acc, key) => {
            if (acc == null) return acc
            return acc[key]
          }, dataSource)
        }

        const computedEntries = []

        const processProperty = (property, parentPath = []) => {
          if (!property?.name) return

          const propertyPath = normalizedPath(property.name, parentPath)

          // Determine the scope data for calculation (parent object)
          const scopeData =
            parentPath.length === 0
              ? workingData
              : getValueAtPath(workingData, parentPath)

          if (property.value && typeof property.value === 'function') {
            try {
              const computedValue = property.value(scopeData || {})
              if (computedValue !== undefined) {
                computedEntries.push({
                  namePath: propertyPath,
                  value: computedValue
                })
                // Update workingData so subsequent properties can use this value
                set(workingData, propertyPath, computedValue)
              }
            } catch (error) {
              console.warn(
                `Error calculating value for property ${property.name}:`,
                error
              )
            }
          }

          if (
            Array.isArray(property.properties) &&
            property.properties.length > 0
          ) {
            if (property.type === 'objectChildren') {
              // Use workingData to get the latest state of children
              const childValues = getValueAtPath(workingData, propertyPath)
              if (Array.isArray(childValues)) {
                childValues.forEach((_, index) => {
                  property.properties.forEach((childProperty) => {
                    processProperty(childProperty, [...propertyPath, index])
                  })
                })
              }
            } else {
              property.properties.forEach((childProperty) => {
                processProperty(childProperty, propertyPath)
              })
            }
          }
        }

        modelDefinition.properties.forEach((property) => {
          processProperty(property, [])
        })

        return computedEntries
      },
      []
    )

    const releaseEditingState = useCallback(
      (options = {}) => {
        const { notifyParent = true } = options

        if (serverObjectData.current) {
          const computedEntries = calculateComputedValues(
            serverObjectData.current,
            model
          )
          const computedValuesObject = buildObjectFromEntries(computedEntries)
          const resetFormData = merge(
            {},
            serverObjectData.current,
            computedValuesObject
          )
          setIsEditing(false)
          isEditingRef.current = false
          form.setFieldsValue(resetFormData)
          setObjectData({ ...resetFormData, _isEditing: false })
        } else {
          setIsEditing(false)
          isEditingRef.current = false
        }

        if (notifyParent) {
          onStateChangeRef.current({ isEditing: false })
        }
      },
      [calculateComputedValues, form, model]
    )

    const handleEditingConflict = useCallback(
      (nextActivities) => {
        const currentUserId = userProfile?._id
        if (
          !isEditingRef.current ||
          !getBeingEditedByOther(nextActivities, currentUserId)
        ) {
          return
        }

        if (isUserEditing(nextActivities, currentUserId)) {
          setObjectActivity(id, type, 'editing')
          return
        }

        releaseEditingState()
        clearAction()
        showMessageError('Another user is already editing this item')
      },
      [
        id,
        type,
        userProfile?._id,
        setObjectActivity,
        releaseEditingState,
        clearAction,
        showMessageError
      ]
    )

    // Validate form on change (debounced to avoid heavy work on every keystroke)
    useEffect(() => {
      const timeoutId = setTimeout(() => {
        const currentFormValues = form.getFieldsValue()
        const mergedObjectData = {
          ...serverObjectData.current,
          ...currentFormValues,
          _isEditing: isEditingRef.current
        }

        form
          .validateFields({ validateOnly: true })
          .then(() => {
            setFormValid(true)
            onStateChangeRef.current({
              formValid: true,
              objectData: mergedObjectData,
              editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
            })
          })
          .catch(() => {
            setFormValid(false)
            onStateChangeRef.current({
              formValid: false,
              objectData: mergedObjectData,
              editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
            })
          })
      }, 150)

      return () => clearTimeout(timeoutId)
    }, [form, formUpdateValues, model, userProfile])

    // Cleanup on unmount
    useEffect(() => {
      return () => {
        if (currentIdRef.current) {
          currentClearObjectActivityRef.current(
            currentIdRef.current,
            currentTypeRef.current
          )
        }

        // Call flushOrphanFiles if component was editing and model has file properties
        if (
          currentIsEditingRef.current &&
          currentHasFilePropertiesRef.current()
        ) {
          currentFlushOrphanFilesRef.current()
        }
      }
    }, []) // Empty dependency array - only run on mount/unmount

    const handleFetchObject = useCallback(async () => {
      const objectKey = `${type}:${id}`

      try {
        setFetchLoading(true)
        onStateChangeRef.current({ loading: true })
        const data = await fetchObject(id, type)
        const initialActivities = await fetchObjectActivities(id, type)

        if (fetchedObjectRef.current !== objectKey) {
          return
        }

        setActivities(initialActivities)

        if (
          isEditingRef.current &&
          getBeingEditedByOther(initialActivities, userProfile?._id)
        ) {
          releaseEditingState()
        }

        serverObjectData.current = data

        // Calculate and set computed values on initial load
        const computedEntries = calculateComputedValues(data, model)
        const computedValuesObject = buildObjectFromEntries(computedEntries)
        const initialFormData = merge({}, data, computedValuesObject)
        setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
        form.setFieldsValue(initialFormData)
        setFetchLoading(false)
        onStateChangeRef.current({ loading: false })
      } catch (err) {
        console.error(err)
        showMessageError('Failed to fetch object info')
        showError(
          `Failed to fetch object information. Message: ${err.message}. Code: ${err.code}`,
          fetchObject
        )
      }
    }, [
      fetchObject,
      fetchObjectActivities,
      id,
      type,
      form,
      showMessageError,
      showError,
      calculateComputedValues,
      model,
      userProfile?._id,
      releaseEditingState
    ])

    // Update event handler
    const updateObjectEventHandler = useCallback((value) => {
      setObjectData((prev) =>
        mergeWith({}, prev, value, arrayReplaceCustomizer)
      )
    }, [])

    useEffect(() => {
      notifyActivityState(activities)
      handleEditingConflict(activities)
    }, [activities, notifyActivityState, handleEditingConflict])

    useEffect(() => {
      if (connected !== true || !id || token == null) {
        return
      }

      const objectKey = `${type}:${id}`
      if (fetchedObjectRef.current === objectKey) {
        return
      }

      const previousObjectKey = fetchedObjectRef.current
      if (previousObjectKey) {
        const colonIndex = previousObjectKey.indexOf(':')
        const previousType = previousObjectKey.slice(0, colonIndex)
        const previousId = previousObjectKey.slice(colonIndex + 1)
        clearObjectActivity(previousId, previousType)
      }

      fetchedObjectRef.current = objectKey
      setIsEditing(false)
      isEditingRef.current = false
      setActivities([])
      notifyActivityState([])
      form.resetFields()
      handleFetchObject()
    }, [
      id,
      type,
      token,
      connected,
      handleFetchObject,
      form,
      notifyActivityState,
      clearObjectActivity
    ])

    useEffect(() => {
      if (!id) {
        return
      }

      const objectKey = `${type}:${id}`
      const activityHandler = (activity, activitiesList) => {
        if (fetchedObjectRef.current !== objectKey) {
          return
        }

        setActivities((prev) => {
          return Array.isArray(activitiesList)
            ? activitiesList
            : applyActivityUpdate(prev, activity)
        })
      }

      const activityUnsubscribe = subscribeToObjectActivity(
        id?.toLowerCase(),
        type,
        activityHandler
      )

      return () => {
        if (activityUnsubscribe) activityUnsubscribe()
      }
    }, [id, type, subscribeToObjectActivity])

    useEffect(() => {
      if (id && connected == true) {
        currentSetObjectActivityRef.current(
          id,
          type,
          isEditingRef.current == true ? 'editing' : 'viewing'
        )

        const objectKey = `${type}:${id}`
        const objectUpdateHandler = (value) => {
          if (fetchedObjectRef.current !== objectKey) {
            return
          }

          updateObjectEventHandler(value)
        }

        const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
          id?.toLowerCase(),
          type,
          objectUpdateHandler
        )

        return () => {
          if (objectUpdatesUnsubscribe) objectUpdatesUnsubscribe()
        }
      }
    }, [
      id,
      type,
      connected,
      subscribeToObjectUpdates,
      updateObjectEventHandler
    ])

    // Debounce objectData updates sent to parent to limit re-renders
    useEffect(() => {
      const timeoutId = setTimeout(() => {
        onStateChangeRef.current({
          objectData,
          editDisabled: getEditDisabled(model, objectData, userProfile)
        })
      }, 150)
      return () => clearTimeout(timeoutId)
    }, [objectData, model, userProfile])

    useEffect(() => {
      if (!setCurrentObject) {
        return
      }

      setActionsCurrentObjectType(type)

      return () => {
        setActionsCurrentObject(null)
        setActionsCurrentObjectType(null)
      }
    }, [
      setCurrentObject,
      type,
      setActionsCurrentObject,
      setActionsCurrentObjectType
    ])

    useEffect(() => {
      if (!setCurrentObject || !objectData?._id || objectData._id !== id) {
        return
      }

      setActionsCurrentObject(objectData)
    }, [setCurrentObject, objectData, id, setActionsCurrentObject])

    const startEditing = async () => {
      try {
        const latestActivities = await fetchObjectActivities(id, type)
        setActivities(latestActivities)

        if (getBeingEditedByOther(latestActivities, userProfile?._id)) {
          clearAction()
          showMessageError('Another user is already editing this item')
          return
        }

        const activityResult = await setObjectActivity(id, type, 'editing')
        if (activityResult?.success === false) {
          clearAction()
          showMessageError('Another user is already editing this item')
          return
        }

        isEditingRef.current = true
        setIsEditing(true)

        const computedEntries = calculateComputedValues(objectData, model)
        const computedValuesObject = buildObjectFromEntries(computedEntries)
        setObjectData((prev) => ({
          ...prev,
          ...computedValuesObject,
          _isEditing: true
        }))
        const nextObjectData = {
          ...objectData,
          ...computedValuesObject,
          _isEditing: true
        }
        onStateChangeRef.current({
          isEditing: true,
          objectData: nextObjectData,
          editDisabled: getEditDisabled(model, nextObjectData, userProfile)
        })
      } catch (err) {
        console.error(err)
        showMessageError('Failed to start editing')
      }
    }

    const cancelEditing = () => {
      releaseEditingState()
      setObjectActivity(id, type, 'viewing')
    }

    const handleUpdate = async () => {
      try {
        const value = await form.validateFields()
        setEditLoading(true)

        const currentFormData = {
          ...objectData,
          ...value
        }
        onStateChangeRef.current({ editLoading: true })
        await updateObject(id, type, currentFormData)
        setIsEditing(false)
        isEditingRef.current = false
        onStateChangeRef.current({ isEditing: isEditingRef.current })
        setObjectData({
          ...objectData,
          ...currentFormData,
          _isEditing: isEditingRef.current
        })
        setObjectActivity(id, type, 'viewing')
        showSuccess('Information updated successfully')
      } catch (err) {
        console.error(err)
        if (err.errorFields) {
          return
        }
        showMessageError('Failed to update information')
        showError(
          `Failed to update information. Message: ${err.message}. Code: ${err.code}`,
          () => handleUpdate()
        )
      } finally {
        handleFetchObject()
        setEditLoading(false)
        onStateChangeRef.current({ editLoading: false })
      }
    }

    const handleDelete = () => {
      setDeleteModalOpen(true)
    }

    const confirmDelete = async () => {
      setDeleteLoading(true)
      try {
        await deleteObject(id, type)
        setDeleteModalOpen(false)
        showSuccess('Deleted successfully')
        navigate(-2)
        // Optionally: trigger a callback to parent to remove this object from view
      } catch (err) {
        console.error(err)
        showMessageError('Failed to delete')
        showError(
          `Failed to delete. Message: ${err.message}. Code: ${err.code}`,
          confirmDelete
        )
      } finally {
        setDeleteLoading(false)
      }
    }

    useImperativeHandle(ref, () => ({
      startEditing,
      cancelEditing,
      handleUpdate,
      handleDelete,
      confirmDelete,
      handleFetchObject,
      editLoading,
      fetchLoading,
      isEditing,
      objectData,
      activities
    }))

    return (
      <>
        <DeleteObjectModal
          open={deleteModalOpen}
          onOk={confirmDelete}
          onCancel={() => setDeleteModalOpen(false)}
          loading={deleteLoading}
          objectType={type}
          objectName={objectData?.name || objectData?.label || ''}
        />
        <Form
          form={form}
          layout='vertical'
          style={style}
          onValuesChange={(changedValues, allFormValues) => {
            // Use the full form snapshot (allFormValues) so list fields (Form.List)
            // come through as complete arrays instead of sparse arrays like
            // [null, null, { quantity: 5 }].
            if (onEdit != undefined) {
              onEdit(allFormValues)
            }

            // Recompute derived fields from the full current form snapshot so
            // toggles like overridePrice/overrideCost are preserved while typing.
            const currentFormData = mergeWith(
              {},
              serverObjectData.current || {},
              objectData || {},
              allFormValues,
              arrayReplaceCustomizer
            )
            const computedEntries = calculateComputedValues(
              currentFormData,
              model
            )

            if (Array.isArray(computedEntries) && computedEntries.length > 0) {
              computedEntries.forEach(({ namePath, value }) => {
                if (!Array.isArray(namePath) || value === undefined) return
                const currentValue = form.getFieldValue(namePath)
                if (currentValue !== value) {
                  if (typeof form.setFieldValue === 'function') {
                    form.setFieldValue(namePath, value)
                  } else {
                    const fallbackPayload = buildObjectFromEntries([
                      { namePath, value }
                    ])
                    form.setFieldsValue(fallbackPayload)
                  }
                }
              })
            }

            const computedValuesObject = buildObjectFromEntries(computedEntries)
            const mergedFormValues = mergeWith(
              {},
              allFormValues,
              arrayReplaceCustomizer
            )
            merge(mergedFormValues, computedValuesObject)

            mergedFormValues._isEditing = isEditingRef.current

            setObjectData((prev) => {
              return mergeWith(
                {},
                prev,
                mergedFormValues,
                arrayReplaceCustomizer
              )
            })
          }}
        >
          {children({
            loading: fetchLoading,
            isEditing,
            startEditing,
            cancelEditing,
            handleUpdate,
            form,
            formValid,
            objectData,
            setIsEditing,
            setObjectData,
            editLoading,
            editDisabled: getEditDisabled(model, objectData, userProfile),
            activities,
            handleFetchObject,
            handleDelete
          })}
        </Form>
      </>
    )
  }
)

ObjectForm.displayName = 'ObjectForm'

ObjectForm.propTypes = {
  id: PropTypes.string.isRequired,
  type: PropTypes.string.isRequired,
  children: PropTypes.func.isRequired,
  style: PropTypes.object,
  onEdit: PropTypes.func,
  onStateChange: PropTypes.func,
  setCurrentObject: PropTypes.bool
}

export default ObjectForm
