import {
  forwardRef,
  useImperativeHandle,
  useRef,
  useEffect,
  useLayoutEffect,
  useState,
  useCallback,
  useMemo,
  createElement
} from 'react'
import { flushSync } from 'react-dom'
import {
  Table,
  Skeleton,
  Row,
  Col,
  Flex,
  Spin,
  Button,
  Space,
  Tooltip,
  Form,
  Splitter,
  Card
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { useMediaQuery } from 'react-responsive'
import { useContext } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
import config from '../../../config'
import loglevel from 'loglevel'
import {
  getModelProperties,
  getModelByName
} from '../../../database/ObjectModels'
import ObjectProperty from './ObjectProperty'
import ObjectCard from './ObjectCard'
import FilterSidebar from './FilterSidebar'
import SortSidebar from './SortSidebar'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import { useLocation } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import { useActions } from '../context/ActionsContext'
import ActionsIcon from '../../Icons/ActionsIcon'
import FilterIcon from '../../Icons/FilterIcon'
import ScrollBox from './ScrollBox'
import SimplePropertyFilter from './SimplePropertyFilter'
import FilterInput from './FilterInput'
import {
  getActiveFilterValues,
  useTableStatePersistence
} from '../context/TableStateContext'

const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)

const SCROLL_THRESHOLD = 50
const SKELETON_HEIGHT = 49.5

const getCardColSpan = (containerWidth) => {
  if (containerWidth >= 2980) return 2
  if (containerWidth >= 1780) return 4
  if (containerWidth >= 1080) return 8
  if (containerWidth >= 680) return 12
  return 24
}

const toFilterExpression = (values) => {
  if (!values?.length) return undefined
  const parts = values.map((value) => {
    if (value && typeof value === 'object') {
      return String(value._id ?? value.type ?? JSON.stringify(value))
    }
    return String(value)
  })
  return parts.length === 1 ? parts[0] : parts.join('|')
}

const fromFilterExpression = (expr) => {
  if (expr === undefined || expr === null || expr === '') return null
  if (typeof expr === 'string' && expr.includes('|')) {
    return expr.split('|').filter((part) => part !== '')
  }
  return [expr]
}

const ColumnFilterDropdown = ({
  setSelectedKeys,
  selectedKeys,
  confirm,
  clearFilters,
  visible,
  propertyName,
  propertyLabel,
  modelType
}) => {
  const [expression, setExpression] = useState('')
  const [draft, setDraft] = useState(selectedKeys || [])
  const selectedKeysRef = useRef(selectedKeys)
  selectedKeysRef.current = selectedKeys

  // Re-sync from the applied sidebar/column filter each time the dropdown opens
  useEffect(() => {
    if (!visible) return
    const keys = selectedKeysRef.current || []
    setDraft(keys)
    setExpression(toFilterExpression(keys) ?? '')
  }, [visible])

  const handleDraftChange = (next) => {
    setDraft(next)
    setExpression(toFilterExpression(next) ?? '')
  }

  const handleExpressionChange = (text) => {
    setExpression(text)
    setDraft(fromFilterExpression(text) || [])
  }

  const applyFilter = () => {
    const trimmed = expression.trim()
    if (!trimmed) {
      clearFilters()
    } else if (draft?.length) {
      setSelectedKeys(draft)
    } else {
      setSelectedKeys([trimmed])
    }
    confirm()
  }

  const resetFilter = () => {
    setExpression('')
    setDraft([])
    clearFilters()
    confirm()
  }

  return (
    <div style={{ padding: 8 }}>
      <Flex vertical gap='small'>
        <Space.Compact>
          <FilterInput
            placeholder={'Filter ' + propertyLabel}
            value={expression}
            onChange={handleExpressionChange}
            onPressEnter={applyFilter}
            style={{ width: 200 }}
          />
          <Button onClick={resetFilter} icon={<XMarkIcon />} />
          <Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
        </Space.Compact>
        <Card size='small' styles={{ body: { padding: 0, height: 200 } }}>
          <ScrollBox inner={true} smallPadding={true}>
            <div style={{ padding: '18px 20px', minWidth: 0 }}>
              <SimplePropertyFilter
                modelType={modelType}
                propertyName={propertyName}
                value={draft}
                onChange={handleDraftChange}
              />
            </div>
          </ScrollBox>
        </Card>
      </Flex>
    </div>
  )
}

ColumnFilterDropdown.propTypes = {
  setSelectedKeys: PropTypes.func,
  selectedKeys: PropTypes.array,
  confirm: PropTypes.func,
  clearFilters: PropTypes.func,
  visible: PropTypes.bool,
  propertyName: PropTypes.string,
  propertyLabel: PropTypes.string,
  modelType: PropTypes.string
}

const RowForm = ({ record, isEditing, onRegister, children }) => {
  const [form] = Form.useForm()
  useEffect(() => {
    if (isEditing && record && !record.isSkeleton) {
      form.setFieldsValue(record)
      onRegister(record._id, form)
    }
    return () => {
      if (record?._id) onRegister(record._id, null)
    }
  }, [isEditing, record, form, onRegister])

  return (
    <Form form={form} component={false}>
      {children}
    </Form>
  )
}

RowForm.propTypes = {
  record: PropTypes.object,
  isEditing: PropTypes.bool,
  onRegister: PropTypes.func,
  children: PropTypes.node
}

const EditableRow = ({ record, isEditing, onRegister, ...props }) => {
  return (
    <RowForm record={record} isEditing={isEditing} onRegister={onRegister}>
      <tr {...props} />
    </RowForm>
  )
}

EditableRow.propTypes = {
  record: PropTypes.object,
  isEditing: PropTypes.bool,
  onRegister: PropTypes.func
}

const ObjectTable = forwardRef(
  (
    {
      type,
      pageSize = 25,
      scrollHeight = 'calc(var(--unit-100vh) - 258px)',
      onDataChange,
      initialPage = 1,
      cards = false,
      visibleColumns = {},
      masterFilter = {},
      size = 'middle',
      onStateChange,
      showFilterSidebar = false,
      showSortSidebar = false,
      expandHeight = false,
      showActions = true,
      saveFilterInSession = false,
      saveFilterInUrl = false,
      useFilterInSession = false,
      useFilterInUrl = false,
      saveSortInSession = false,
      saveSortInUrl = false,
      useSortInSession = false,
      useSortInUrl = false,
      onRowAction
    },
    ref
  ) => {
    const { token, userProfile } = useContext(AuthContext)
    const { isElectron } = useContext(ElectronContext)
    const { callAction } = useActions()
    const onStateChangeRef = useRef(onStateChange)
    useEffect(() => {
      onStateChangeRef.current = onStateChange
    }, [onStateChange])
    const {
      fetchObjects,
      connected,
      subscribeToObjectUpdates,
      subscribeToObjectTypeUpdates,
      updateMultipleObjects,
      setObjectActivity,
      clearObjectActivity
    } = useContext(ApiServerContext)
    const isMobile = useMediaQuery({ maxWidth: 768 })
    const location = useLocation()
    const {
      getPersistedFilter,
      getPersistedSorter,
      persistFilter,
      persistSort,
      persistTableState,
      registerPageFilter
    } = useTableStatePersistence({
      scope: type,
      pagePath: location.pathname,
      saveFilterInSession,
      saveFilterInUrl,
      useFilterInSession,
      useFilterInUrl,
      saveSortInSession,
      saveSortInUrl,
      useSortInSession,
      useSortInUrl
    })
    var adjustedScrollHeight = scrollHeight
    if (isMobile) {
      adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
    }
    if (cards) {
      adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
    }
    if (isElectron) {
      adjustedScrollHeight = 'calc(var(--unit-100vh) - 238px)'
    }
    if (isMobile && isElectron) {
      adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
    }
    if (cards && isElectron) {
      adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)'
    }
    const tableRef = useRef(null)
    const model = getModelByName(type)
    const activeFilterRef = useRef({})
    const tableSorterRef = useRef({})
    const [sidebarFilter, setSidebarFilter] = useState({})
    const [tableSorter, setTableSorter] = useState({})
    const [initialized, setInitialized] = useState(false)

    // Table state
    const [pages, setPages] = useState([])
    const pagesRef = useRef(pages)
    const tableData = useMemo(() => {
      const items = pages.flatMap((page) => page.items)
      const seen = new Set()
      return items.filter((item) => {
        const id = item?._id
        if (id == null) return true
        if (seen.has(id)) {
          logger.warn('Duplicate table row omitted:', id)
          return false
        }
        seen.add(id)
        return true
      })
    }, [pages])
    const [loading, setLoading] = useState(true)
    const [lazyLoading, setLazyLoading] = useState(false)

    const [isEditing, setIsEditing] = useState(false)
    const [editLoading, setEditLoading] = useState(false)
    const rowFormsRef = useRef({})
    const registerForm = useCallback((id, form) => {
      if (form) {
        rowFormsRef.current[id] = form
      } else {
        delete rowFormsRef.current[id]
      }
    }, [])

    useEffect(() => {
      onStateChangeRef.current?.({ isEditing, editLoading })
    }, [isEditing, editLoading])

    const subscribedIdsRef = useRef([])
    // const [typeSubscribed, setTypeSubscribed] = useState(false)
    const unsubscribesRef = useRef([])
    const updateEventHandlerRef = useRef()
    const subscribeToObjectTypeUpdatesRef = useRef(null)
    const subscribedTypeRef = useRef(null)
    const newEventHandlerRef = useRef()
    const subscriptionFilterRef = useRef()
    const subscribeToObjectTypeUpdatesFnRef = useRef(
      subscribeToObjectTypeUpdates
    )
    const prevValuesRef = useRef({ type, masterFilter })

    const rowActions =
      model.actions?.filter((action) => action.row == true) || []

    const createSkeletonData = useCallback(
      (pageNum) => {
        return Array(pageSize)
          .fill(null)
          .map((_, index) => ({
            _id: `skeleton-${pageNum}-${index}`,
            isSkeleton: true
          }))
      },
      [pageSize]
    )

    const createSkeletonPage = useCallback(
      (pageNum) => ({
        pageNum,
        items: createSkeletonData(pageNum),
        isSkeletonPage: true
      }),
      [createSkeletonData]
    )

    const loadingPagesRef = useRef(new Set())
    const pendingScrollAnchorRef = useRef(null)
    const dataLoadGenerationRef = useRef(0)
    const [tableListKey, setTableListKey] = useState(0)
    const isInternalSortUpdateRef = useRef(false)

    const beginDataReload = useCallback(() => {
      dataLoadGenerationRef.current += 1
      loadingPagesRef.current.clear()
      pendingScrollAnchorRef.current = null
      pagesRef.current = []
      setTableListKey((key) => key + 1)
      return dataLoadGenerationRef.current
    }, [])

    const isStaleDataLoad = useCallback((generation) => {
      return generation !== dataLoadGenerationRef.current
    }, [])

    const clearTablePages = useCallback(() => {
      pagesRef.current = []
      flushSync(() => {
        setPages([])
      })
    }, [])

    const setTablePages = useCallback((nextPages) => {
      pagesRef.current = nextPages
      setPages(nextPages)
    }, [])

    const mergeLoadedPage = useCallback((currentPages, loadedPage) => {
      const withoutSkeletons = currentPages.filter(
        (page) => !page.isSkeletonPage
      )
      const withoutDuplicate = withoutSkeletons.filter(
        (page) => page.pageNum !== loadedPage.pageNum
      )
      return [...withoutDuplicate, loadedPage].sort(
        (a, b) => a.pageNum - b.pageNum
      )
    }, [])

    const renderActions = (objectData) => {
      return (
        <Flex gap='small' align='center' justify='center'>
          {rowActions.map((action, index) => {
            var disabled = false
            if (action.disabled) {
              if (typeof action.disabled === 'function') {
                disabled = action.disabled({
                  ...objectData,
                  _user: userProfile
                })
              } else {
                disabled = action.disabled
              }
            }
            return (
              <Tooltip key={index} title={action.label} arrow={false}>
                <Button
                  icon={
                    action.icon ? (
                      createElement(action.icon)
                    ) : (
                      <QuestionCircleIcon />
                    )
                  }
                  disabled={disabled || objectData?.isSkeleton}
                  type={'text'}
                  size={'small'}
                  onClick={() => {
                    if (
                      onRowAction &&
                      onRowAction(action, objectData) !== false
                    ) {
                      return
                    }
                    callAction(
                      action,
                      { ...objectData, _user: userProfile },
                      type
                    )
                  }}
                />
              </Tooltip>
            )
          })}
        </Flex>
      )
    }

    const fetchPage = useCallback(
      async (pageNum = 1, filter = null, sorter = null) => {
        if (filter == null) {
          filter = activeFilterRef.current
        } else {
          activeFilterRef.current = filter
        }
        if (sorter == null) {
          sorter = tableSorterRef.current
        } else {
          tableSorterRef.current = {
            field: sorter.field,
            order: sorter.order
          }
        }
        return fetchObjects(type, {
          page: pageNum,
          limit: pageSize,
          filter: { ...filter, ...masterFilter },
          sorter,
          onDataChange
        })
      },
      [type, masterFilter, pageSize, onDataChange, fetchObjects]
    )

    const skeletonsHeight = useMemo(() => {
      return SKELETON_HEIGHT * pageSize
    }, [pageSize])

    const skeletonBoundaryIds = useMemo(() => {
      const firstPage = pages[0]
      const lastPage = pages[pages.length - 1]
      return {
        previous:
          firstPage?.isSkeletonPage && firstPage.items.length > 0
            ? firstPage.items[firstPage.items.length - 1]._id
            : null,
        next:
          lastPage?.isSkeletonPage && lastPage.items[0]
            ? lastPage.items[0]._id
            : null
      }
    }, [pages])

    const createPageWindow = useCallback(
      (loadedPages, direction) => {
        const dedupedByPageNum = new Map()
        loadedPages.forEach((page) => {
          dedupedByPageNum.set(page.pageNum, page)
        })
        const sortedPages = [...dedupedByPageNum.values()].sort(
          (a, b) => a.pageNum - b.pageNum
        )
        const visiblePages =
          direction === 'previous'
            ? sortedPages.slice(0, 2)
            : sortedPages.slice(-2)
        const firstPage = visiblePages[0]
        const lastPage = visiblePages[visiblePages.length - 1]
        const nextPages = []

        if (firstPage?.pageNum > 1) {
          nextPages.push(createSkeletonPage(firstPage.pageNum - 1))
        }
        nextPages.push(...visiblePages)
        if (lastPage?.hasMore) {
          nextPages.push(createSkeletonPage(lastPage.pageNum + 1))
        }

        return nextPages
      },
      [createSkeletonPage]
    )

    const fetchData = useCallback(
      async (pageNum = 1, filter = null, sorter = null) => {
        const generation = dataLoadGenerationRef.current
        try {
          const result = await fetchPage(pageNum, filter, sorter)
          if (isStaleDataLoad(generation)) return []
          const loadedPage = {
            pageNum,
            items: result.data || [],
            hasMore: result.hasMore
          }
          setPages((prev) =>
            prev.map((page) => (page.pageNum === pageNum ? loadedPage : page))
          )
          setLoading(false)
          return result.data || []
        } catch (error) {
          if (!isStaleDataLoad(generation)) {
            setLoading(false)
          }
          throw error
        }
      },
      [fetchPage, isStaleDataLoad]
    )

    const findRenderedRow = useCallback((scrollTarget, id) => {
      if (!scrollTarget || id == null) return null
      return Array.from(scrollTarget.querySelectorAll('tr[data-row-key]')).find(
        (row) => row.getAttribute('data-row-key') === String(id)
      )
    }, [])

    const captureScrollAnchor = useCallback(
      (scrollTarget, placeholderPage, direction, currentPages) => {
        if (!scrollTarget) return null

        const targetTop = scrollTarget.getBoundingClientRect().top
        const closestRow = (items) => {
          return items
            .map((item, index) => {
              const element = findRenderedRow(scrollTarget, item._id)
              return element
                ? {
                    index,
                    id: item._id,
                    top: element.getBoundingClientRect().top
                  }
                : null
            })
            .filter(Boolean)
            .sort(
              (a, b) =>
                Math.abs(a.top - targetTop) - Math.abs(b.top - targetTop)
            )[0]
        }

        const placeholderAnchor = closestRow(placeholderPage.items)
        const loadedPages = currentPages.filter((page) => !page.isSkeletonPage)
        const retainedPage =
          direction === 'previous'
            ? loadedPages[0]
            : loadedPages[loadedPages.length - 1]
        const fallbackAnchor = retainedPage
          ? closestRow(retainedPage.items)
          : null

        return { placeholderAnchor, fallbackAnchor }
      },
      [findRenderedRow]
    )

    const restoreScrollAnchor = useCallback(
      (scrollTarget, anchor, loadedPage) => {
        if (!scrollTarget || !anchor) return

        const loadedItem = loadedPage.items[anchor.placeholderAnchor?.index]
        const anchorId = loadedItem?._id ?? anchor.fallbackAnchor?.id
        const previousTop = loadedItem
          ? anchor.placeholderAnchor?.top
          : anchor.fallbackAnchor?.top
        if (anchorId == null || previousTop == null) return

        const element = findRenderedRow(scrollTarget, anchorId)
        if (!element) return
        const renderedTop = element.getBoundingClientRect().top
        scrollTarget.scrollTop += renderedTop - previousTop
      },
      [findRenderedRow]
    )

    useLayoutEffect(() => {
      const pendingAnchor = pendingScrollAnchorRef.current
      if (!pendingAnchor) return
      pendingScrollAnchorRef.current = null
      restoreScrollAnchor(
        pendingAnchor.scrollTarget,
        pendingAnchor.anchor,
        pendingAnchor.loadedPage
      )
    }, [pages, restoreScrollAnchor])

    const loadBoundaryPage = useCallback(
      async (pageNum, direction, scrollTarget) => {
        if (loadingPagesRef.current.size > 0) return

        const currentPages = pagesRef.current
        const placeholderPage = currentPages.find(
          (page) => page.pageNum === pageNum && page.isSkeletonPage
        )
        if (!placeholderPage) return
        const scrollAnchor = captureScrollAnchor(
          scrollTarget,
          placeholderPage,
          direction,
          currentPages
        )

        const generation = dataLoadGenerationRef.current
        loadingPagesRef.current.add(pageNum)
        setLazyLoading(true)
        logger.debug(`Loading ${direction} page...`)

        try {
          const result = await fetchPage(pageNum)
          if (isStaleDataLoad(generation)) return
          const loadedPage = {
            pageNum,
            items: result.data || [],
            hasMore: result.hasMore
          }

          pendingScrollAnchorRef.current = {
            scrollTarget,
            anchor: scrollAnchor,
            loadedPage
          }
          setPages((prev) => {
            if (
              !prev.some(
                (page) => page.pageNum === pageNum && page.isSkeletonPage
              )
            ) {
              return prev
            }

            const loadedPages = mergeLoadedPage(prev, loadedPage)
            return createPageWindow(loadedPages, direction)
          })
        } catch (error) {
          logger.error(`Error loading page ${pageNum}:`, error)
        } finally {
          loadingPagesRef.current.delete(pageNum)
          if (
            !isStaleDataLoad(generation) &&
            loadingPagesRef.current.size === 0
          ) {
            setLazyLoading(false)
          }
        }
      },
      [
        captureScrollAnchor,
        createPageWindow,
        fetchPage,
        isStaleDataLoad,
        mergeLoadedPage
      ]
    )

    const loadNextPage = useCallback(
      (scrollTarget) => {
        const nextPage = pagesRef.current[pagesRef.current.length - 1]
        if (nextPage?.isSkeletonPage) {
          loadBoundaryPage(nextPage.pageNum, 'next', scrollTarget)
        }
      },
      [loadBoundaryPage]
    )

    const loadPreviousPage = useCallback(
      (scrollTarget) => {
        const previousPage = pagesRef.current[0]
        if (previousPage?.isSkeletonPage) {
          loadBoundaryPage(previousPage.pageNum, 'previous', scrollTarget)
        }
      },
      [loadBoundaryPage]
    )

    const handleScroll = useCallback(
      (e) => {
        const { target } = e
        const firstPage = pagesRef.current[0]
        const lastPage = pagesRef.current[pagesRef.current.length - 1]

        if (
          lastPage?.isSkeletonPage &&
          target.scrollHeight - target.scrollTop - target.clientHeight <=
            skeletonsHeight + SCROLL_THRESHOLD
        ) {
          loadNextPage(target)
        } else if (
          firstPage?.isSkeletonPage &&
          target.scrollTop <= skeletonsHeight + SCROLL_THRESHOLD
        ) {
          loadPreviousPage(target)
        }
      },
      [loadNextPage, loadPreviousPage, skeletonsHeight]
    )

    const reload = useCallback(async () => {
      setLazyLoading(true)
      try {
        const loadedPages = pagesRef.current.filter(
          (page) => !page.isSkeletonPage
        )
        for (let i = 0; i < loadedPages.length; i++) {
          await fetchData(loadedPages[i].pageNum)
        }
      } finally {
        setLazyLoading(false)
      }
    }, [fetchData])

    const startEditing = useCallback(() => {
      setIsEditing(true)
      tableData.forEach((item) => {
        if (!item.isSkeleton) {
          setObjectActivity(item._id, type, 'editing')
        }
      })
    }, [tableData, setObjectActivity, type])

    const cancelEditing = useCallback(() => {
      setIsEditing(false)
      tableData.forEach((item) => {
        if (!item.isSkeleton) {
          clearObjectActivity(item._id, type)
        }
      })
    }, [tableData, clearObjectActivity, type])

    const handleUpdate = useCallback(async () => {
      setEditLoading(true)
      try {
        const updates = await Promise.all(
          Object.entries(rowFormsRef.current).map(async ([id, form]) => {
            const values = await form.validateFields()
            return { _id: id, ...values }
          })
        )
        await updateMultipleObjects(type, updates)
        setIsEditing(false)
        reload()
        tableData.forEach((item) => {
          if (!item.isSkeleton) {
            clearObjectActivity(item._id, type)
          }
        })
      } catch (err) {
        logger.error('Error updating objects:', err)
      } finally {
        setEditLoading(false)
      }
    }, [type, updateMultipleObjects, reload, tableData, clearObjectActivity])

    // Update event handler for real-time updates
    const updateEventHandler = useCallback((id, updatedData) => {
      setPages((prevPages) =>
        prevPages.map((page) => {
          const updatedItems = page.items.map((item) => {
            if (item._id === id) {
              return { ...item, ...updatedData }
            }
            return item
          })
          return {
            ...page,
            items: updatedItems
          }
        })
      )

      if (rowFormsRef.current[id]) {
        rowFormsRef.current[id].setFieldsValue(updatedData)
      }
    }, [])

    // Store the latest updateEventHandler in a ref
    updateEventHandlerRef.current = updateEventHandler

    const newEventHandler = useCallback(
      (params) => {
        logger.debug('New event handler:', params)
        reload()
      },
      [reload]
    )

    const subscriptionFilter = useMemo(() => {
      const active = {}
      Object.entries(sidebarFilter).forEach(([k, v]) => {
        if (v !== '' && v !== undefined) active[k] = v
      })
      return { ...active, ...masterFilter }
    }, [sidebarFilter, masterFilter])

    newEventHandlerRef.current = newEventHandler
    subscriptionFilterRef.current = subscriptionFilter
    subscribeToObjectTypeUpdatesFnRef.current = subscribeToObjectTypeUpdates

    // Subscribe to real-time updates for all items
    useEffect(() => {
      if (pages.length > 0 && connected == true) {
        // Get all non-skeleton item IDs from all pages
        const allItemIds = pages
          .flatMap((page) => page.items || [])
          .filter((item) => !item.isSkeleton)
          .map((item) => item._id)
          .filter(Boolean)

        // Find new items that need subscription
        const newItemIds = allItemIds.filter(
          (id) => !subscribedIdsRef.current.includes(id)
        )

        // Subscribe to new items only
        newItemIds.forEach((itemId) => {
          const unsubscribe = subscribeToObjectUpdates(
            itemId?.toLowerCase(),
            type,
            (updateData) => {
              updateEventHandlerRef.current(itemId, updateData)
            }
          )
          subscribedIdsRef.current.push(itemId)
          if (unsubscribe) {
            unsubscribesRef.current.push(unsubscribe)
          }
        })

        // Clean up subscriptions for items that are no longer in the pages
        const currentSubscribedIds = subscribedIdsRef.current
        const itemsToUnsubscribe = currentSubscribedIds.filter(
          (id) => !allItemIds.includes(id)
        )

        itemsToUnsubscribe.forEach((itemId) => {
          const index = subscribedIdsRef.current.indexOf(itemId)
          if (index > -1) {
            subscribedIdsRef.current.splice(index, 1)
            const unsubscribe = unsubscribesRef.current[index]
            if (unsubscribe) {
              unsubscribe()
            }
            unsubscribesRef.current.splice(index, 1)
          }
        })
      }
    }, [pages, type, subscribeToObjectUpdates, connected])

    // Cleanup effect for component unmount
    useEffect(() => {
      return () => {
        if (connected == true && unsubscribesRef.current) {
          // Clean up all subscriptions when component unmounts
          unsubscribesRef.current.forEach((unsubscribe) => {
            if (unsubscribe) unsubscribe()
          })
          unsubscribesRef.current = []
          subscribedIdsRef.current = []

          // Clean up type subscription
        }
        if (connected == true && subscribeToObjectTypeUpdatesRef.current) {
          subscribeToObjectTypeUpdatesRef.current()
          subscribeToObjectTypeUpdatesRef.current = null
        }
      }
    }, [connected])

    useEffect(() => {
      if (connected !== true) return
      if (subscribedTypeRef.current === type) return

      const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
        type,
        subscriptionFilterRef.current,
        (params) => newEventHandlerRef.current(params)
      )
      subscribeToObjectTypeUpdatesRef.current = unsubscribe
      subscribedTypeRef.current = type
      return () => {
        if (unsubscribe) unsubscribe()
        if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
          subscribeToObjectTypeUpdatesRef.current = null
        }
        if (subscribedTypeRef.current === type) {
          subscribedTypeRef.current = null
        }
      }
    }, [type, connected])

    const updateData = useCallback(
      (id, updatedData) => {
        updateEventHandler(id, updatedData)
      },
      [updateEventHandler]
    )

    const loadPage = useCallback(
      async (pageNum, filter = null, sorter = null) => {
        const generation = beginDataReload()
        clearTablePages()
        if (isStaleDataLoad(generation)) return

        const skeletonPage = createSkeletonPage(pageNum)
        flushSync(() => {
          setTablePages([skeletonPage])
          setLoading(true)
        })
        if (isStaleDataLoad(generation)) return

        try {
          const firstResult = await fetchPage(pageNum, filter, sorter)
          if (isStaleDataLoad(generation)) return
          const loadedPages = [
            {
              pageNum,
              items: firstResult.data || [],
              hasMore: firstResult.hasMore
            }
          ]

          if (!isStaleDataLoad(generation)) {
            setTablePages(createPageWindow(loadedPages, 'next'))
          }
        } catch (error) {
          if (!isStaleDataLoad(generation)) {
            logger.error(`Error loading page ${pageNum}:`, error)
            setTablePages([])
          }
        } finally {
          if (!isStaleDataLoad(generation)) {
            setLoading(false)
          }
        }
      },
      [
        beginDataReload,
        clearTablePages,
        createPageWindow,
        createSkeletonPage,
        fetchPage,
        isStaleDataLoad,
        setTablePages
      ]
    )

    const loadInitialPage = useCallback(async () => {
      const filter = getPersistedFilter()
      const sorter = getPersistedSorter()
      const activeFilter = getActiveFilterValues(filter)

      if (Object.keys(filter).length > 0) {
        setSidebarFilter(filter)
      }
      if (sorter.field) {
        tableSorterRef.current = sorter
        setTableSorter(sorter)
      }

      loadPage(initialPage, activeFilter, sorter.field ? sorter : null)
    }, [getPersistedFilter, getPersistedSorter, initialPage, loadPage])

    useImperativeHandle(ref, () => ({
      reload,
      setData: (newData) => {
        setPages([{ pageNum: 1, items: newData }])
      },
      updateData,
      startEditing,
      cancelEditing,
      handleUpdate,
      isEditing,
      editLoading
    }))

    useEffect(() => {
      if (
        connected == true &&
        token != null &&
        !pages.includes(initialPage) &&
        !initialized
      ) {
        loadInitialPage()
        setInitialized(true)
      }
    }, [token, loadInitialPage, initialPage, pages, initialized, connected])

    // Watch for changes in type and masterFilter, reset component state when they change
    useEffect(() => {
      const prevValues = prevValuesRef.current

      // Deep comparison for objects, simple comparison for primitives
      const hasChanged =
        prevValues.type !== type ||
        JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)

      if (hasChanged) {
        beginDataReload()
        pagesRef.current = []
        setPages([])
        activeFilterRef.current = {}
        tableSorterRef.current = {}
        setSidebarFilter({})
        setTableSorter({})
        setInitialized(false)
        setLoading(true)
        setLazyLoading(false)
        prevValuesRef.current = { type, masterFilter }
      }
    }, [type, masterFilter, beginDataReload])

    useEffect(() => {
      registerPageFilter(sidebarFilter)
      return () => registerPageFilter({})
    }, [sidebarFilter, registerPageFilter])

    const getFilterDropdown = ({
      setSelectedKeys,
      selectedKeys,
      confirm,
      clearFilters,
      visible,
      propertyName,
      propertyLabel
    }) => (
      <ColumnFilterDropdown
        setSelectedKeys={setSelectedKeys}
        selectedKeys={selectedKeys}
        confirm={confirm}
        clearFilters={clearFilters}
        visible={visible}
        propertyName={propertyName}
        propertyLabel={propertyLabel}
        modelType={type}
      />
    )

    const handleTableChange = (pagination, filters, sorter) => {
      if (isInternalSortUpdateRef.current) return

      const next = { ...sidebarFilter }

      Object.entries(filters).forEach(([key, value]) => {
        const expression = toFilterExpression(value)
        if (expression !== undefined) {
          next[key] = expression
        } else {
          delete next[key]
        }
      })

      const nextSorter = {
        field: sorter.columnKey,
        order: sorter.order
      }

      setSidebarFilter(next)
      setTableSorter(nextSorter)
      persistTableState(next, nextSorter)
      loadPage(initialPage, getActiveFilterValues(next), nextSorter)
    }

    const handleSidebarFilterChange = useCallback(
      (newSidebarFilter) => {
        setSidebarFilter(newSidebarFilter)
        persistFilter(newSidebarFilter)
        loadPage(
          initialPage,
          getActiveFilterValues(newSidebarFilter),
          tableSorter.field ? tableSorter : null
        )
      },
      [initialPage, loadPage, persistFilter, tableSorter]
    )

    const handleSidebarSortChange = useCallback(
      (newSorter) => {
        const nextSorter = newSorter?.field
          ? { field: newSorter.field, order: newSorter.order }
          : {}
        isInternalSortUpdateRef.current = true
        setTableSorter(nextSorter)
        tableSorterRef.current = nextSorter
        persistSort(nextSorter)
        loadPage(
          initialPage,
          getActiveFilterValues(sidebarFilter),
          nextSorter.field ? nextSorter : null
        )
        requestAnimationFrame(() => {
          isInternalSortUpdateRef.current = false
        })
      },
      [initialPage, loadPage, persistSort, sidebarFilter]
    )

    const modelProperties = getModelProperties(type)
    // Table columns from model properties
    const columnsWithSkeleton = [
      {
        title: lazyLoading ? <LoadingOutlined /> : cards ? model.icon : null,
        key: 'icon',
        width: 45,
        fixed: 'left',
        render: () => {
          return <Flex justify='center'>{createElement(model.icon)}</Flex>
        }
      }
    ]

    useEffect(() => {
      pagesRef.current = pages
    }, [pages])

    useEffect(() => {
      if (!expandHeight || cards) return

      const findAntTableBody = (element) => {
        if (!element) return null
        if (element.classList?.contains('ant-table-body')) return element
        for (const child of element.children) {
          const found = findAntTableBody(child)
          if (found) return found
        }
        return null
      }

      const root = tableRef.current?.nativeElement ?? tableRef.current
      const tableBody = findAntTableBody(root)
      if (!tableBody) return

      tableBody.style.minHeight = adjustedScrollHeight

      return () => {
        tableBody.style.minHeight = ''
      }
    }, [expandHeight, adjustedScrollHeight, cards, loading, tableData])

    // Add columns in the order specified by model.columns
    model.columns.forEach((colName) => {
      const prop = modelProperties.find((p) => p.name === colName)
      if (prop) {
        // Check if column should be visible based on visibleColumns prop
        if (
          Object.keys(visibleColumns).length > 0 &&
          visibleColumns[prop.name] === false
        ) {
          return // Skip this column if it's not visible
        }

        var fixed = prop.columnFixed || undefined
        var width = 200

        switch (prop.type) {
          case 'text':
            width = 200
            break
          case 'number':
            width = 100
            break
          case 'dateTime':
            width = 200
            break
          case 'state':
            width = 200
            break
          case 'id':
            width = 180
            break
          default:
            break
        }
        // Check if this property should be filterable based on model.filters
        const isFilterable = model.filters && model.filters.includes(prop.name)

        // Check if this property should be sortable based on model.sorters
        const isSortable = model.sorters && model.sorters.includes(prop.name)

        const columnConfig = {
          sorter: isSortable ? { multiple: 1 } : undefined,
          sortOrder:
            tableSorter?.field === prop.name ? tableSorter.order : null,
          title: prop.label,
          width: prop.columnWidth || width,
          fixed: isMobile ? undefined : fixed,
          key: prop.name,
          filterIcon: () => {
            return <FilterIcon />
          },
          render: (text, record) => {
            if (record?.isSkeleton) {
              return (
                <Skeleton.Input
                  active
                  size='small'
                  style={{ width: '100%', height: 24.5 }}
                />
              )
            }
            return (
              <ObjectProperty
                {...prop}
                longId={false}
                inTable={true}
                objectData={record}
                isEditing={isEditing}
              />
            )
          }
        }

        if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) {
          columnConfig.filterDropdown = ({
            setSelectedKeys,
            selectedKeys,
            confirm,
            clearFilters,
            visible
          }) =>
            getFilterDropdown({
              setSelectedKeys,
              selectedKeys,
              confirm,
              clearFilters,
              visible,
              propertyName: prop.name,
              propertyLabel: prop.label
            })
          columnConfig.filteredValue = fromFilterExpression(
            sidebarFilter[prop.name]
          )
        }

        columnsWithSkeleton.push(columnConfig)
      }
    })

    if (
      showActions &&
      rowActions.length > 0 &&
      tableData.some((item) => !item?.isSkeleton)
    ) {
      columnsWithSkeleton.push({
        title: (
          <Flex gap='small' align='center' justify='center'>
            <ActionsIcon />
          </Flex>
        ),
        key: 'actions',
        fixed: 'right',
        width: 20 + rowActions.length * 30, // Adjust width based on number of actions
        render: (record) => {
          return renderActions(record)
        }
      })
    }

    // Card view rendering
    const [cardColSpan, setCardColSpan] = useState(24)
    const cardsContainerNodeRef = useRef(null)
    const cardsResizeObserverRef = useRef(null)

    const measureCardColSpan = useCallback(() => {
      const node = cardsContainerNodeRef.current
      if (!node) return
      const panel = node.closest('.ant-splitter-panel')
      const width = (panel ?? node).getBoundingClientRect().width
      if (width <= 0) return
      const nextSpan = getCardColSpan(width)
      setCardColSpan((prev) => (prev === nextSpan ? prev : nextSpan))
    }, [])

    const objectTableCardsContainerRef = useCallback(
      (node) => {
        cardsResizeObserverRef.current?.disconnect()
        cardsResizeObserverRef.current = null
        cardsContainerNodeRef.current = node

        if (!node) return

        let rafId = null
        const scheduleMeasure = () => {
          if (rafId != null) cancelAnimationFrame(rafId)
          rafId = requestAnimationFrame(() => {
            rafId = null
            measureCardColSpan()
          })
        }

        scheduleMeasure()

        const observer = new ResizeObserver(scheduleMeasure)
        const panel = node.closest('.ant-splitter-panel')
        if (panel) {
          observer.observe(panel)
        } else {
          observer.observe(node)
        }

        cardsResizeObserverRef.current = observer
      },
      [measureCardColSpan]
    )

    useLayoutEffect(() => {
      if (!cards) return
      measureCardColSpan()
    }, [cards, showFilterSidebar, showSortSidebar, measureCardColSpan])

    useEffect(() => {
      return () => cardsResizeObserverRef.current?.disconnect()
    }, [])

    // Card view: load when the leading skeleton card in a boundary page enters view
    useLayoutEffect(() => {
      if (!cards) return
      const container = cardsContainerNodeRef.current
      if (!container) return
      const scrollEl = container.querySelector('.simplebar-content-wrapper')
      if (!scrollEl) return

      const observer = new IntersectionObserver(
        (entries) => {
          for (const entry of entries) {
            if (!entry.isIntersecting) continue
            const boundary = entry.target.dataset.skeletonBoundary
            if (boundary === 'next') {
              loadNextPage(scrollEl)
            } else if (boundary === 'previous') {
              loadPreviousPage(scrollEl)
            }
          }
        },
        {
          root: scrollEl,
          rootMargin: `${SCROLL_THRESHOLD}px`,
          threshold: 0
        }
      )

      container
        .querySelectorAll('[data-skeleton-boundary]')
        .forEach((el) => observer.observe(el))

      return () => observer.disconnect()
    }, [cards, skeletonBoundaryIds, loadNextPage, loadPreviousPage])

    const renderCards = () => {
      return (
        <ScrollBox
          style={{
            maxHeight: `100%`
          }}
        >
          <div className='objectTableCards'>
            <Row gutter={[16, 16]} key={`${cardColSpan}-${tableListKey}`}>
              {tableData.map((record) => {
                if (record?._id == undefined) {
                  return null
                }

                const skeletonBoundary =
                  record._id === skeletonBoundaryIds.next
                    ? 'next'
                    : record._id === skeletonBoundaryIds.previous
                      ? 'previous'
                      : undefined

                return (
                  <Col span={cardColSpan} key={record._id}>
                    <div
                      style={{ width: '100%', overflow: 'hidden' }}
                      data-skeleton-boundary={skeletonBoundary}
                    >
                      <RowForm
                        record={record}
                        isEditing={isEditing}
                        onRegister={registerForm}
                      >
                        <Flex align={'center'} vertical gap={'middle'}>
                          <ObjectCard
                            isSkeleton={record?.isSkeleton || false}
                            model={model}
                            modelProperties={modelProperties}
                            visibleColumns={visibleColumns}
                            record={record}
                            isEditing={isEditing}
                            rowActions={rowActions}
                            renderActions={renderActions}
                          />
                        </Flex>
                      </RowForm>
                    </div>
                  </Col>
                )
              })}
            </Row>
          </div>
        </ScrollBox>
      )
    }

    const components = useMemo(
      () => ({
        body: {
          row: EditableRow
        }
      }),
      []
    )

    const onRow = useCallback(
      (record) => ({
        record,
        isEditing,
        onRegister: registerForm
      }),
      [isEditing, registerForm]
    )

    const tableContent = (
      <Flex
        gap={'middle'}
        vertical
        style={{ flex: 1, minWidth: 0, minHeight: 0 }}
      >
        <Splitter
          className={'farmcontrol-splitter large'}
          onResize={measureCardColSpan}
          onResizeEnd={measureCardColSpan}
        >
          <Splitter.Panel>
            {cards ? (
              <div
                className='objectTableCardsContainer'
                ref={objectTableCardsContainerRef}
              >
                <Spin
                  indicator={<LoadingOutlined />}
                  spinning={loading}
                  style={{ height: '100%' }}
                >
                  {renderCards()}
                </Spin>
              </div>
            ) : (
              <Table
                key={tableListKey}
                ref={tableRef}
                dataSource={tableData}
                columns={columnsWithSkeleton}
                className='dashboard-table'
                pagination={false}
                scroll={{ y: adjustedScrollHeight }}
                rowKey='_id'
                loading={{
                  spinning: loading,
                  indicator: <LoadingOutlined spin />
                }}
                onScroll={handleScroll}
                onChange={handleTableChange}
                showSorterTooltip={false}
                style={{ height: '100%' }}
                size={size}
                components={components}
                onRow={onRow}
              />
            )}
          </Splitter.Panel>
          {(showSortSidebar || showFilterSidebar) && (
            <Splitter.Panel defaultSize={'25%'}>
              <Flex
                vertical
                gap='large'
                style={{ height: '100%', minHeight: 0 }}
              >
                {showSortSidebar && (
                  <SortSidebar
                    type={type}
                    sorter={tableSorter}
                    onSortChange={handleSidebarSortChange}
                    stretchHeight={showSortSidebar && !showFilterSidebar}
                  />
                )}
                {showFilterSidebar && (
                  <Flex style={{ flex: 1, minHeight: 0 }}>
                    <FilterSidebar
                      type={type}
                      filter={sidebarFilter}
                      onFilterChange={handleSidebarFilterChange}
                      masterFilter={masterFilter}
                    />
                  </Flex>
                )}
              </Flex>
            </Splitter.Panel>
          )}
        </Splitter>
      </Flex>
    )

    return tableContent
  }
)

ObjectTable.displayName = 'ObjectTable'

ObjectTable.propTypes = {
  type: PropTypes.string.isRequired,
  pageSize: PropTypes.number,
  scrollHeight: PropTypes.string,
  onDataChange: PropTypes.func,
  initialPage: PropTypes.number,
  cards: PropTypes.bool,
  cardRenderer: PropTypes.func,
  visibleColumns: PropTypes.object,
  masterFilter: PropTypes.object,
  size: PropTypes.string,
  onStateChange: PropTypes.func,
  showFilterSidebar: PropTypes.bool,
  showSortSidebar: PropTypes.bool,
  expandHeight: PropTypes.bool,
  showActions: PropTypes.bool,
  saveFilterInSession: PropTypes.bool,
  saveFilterInUrl: PropTypes.bool,
  useFilterInSession: PropTypes.bool,
  useFilterInUrl: PropTypes.bool,
  saveSortInSession: PropTypes.bool,
  saveSortInUrl: PropTypes.bool,
  useSortInSession: PropTypes.bool,
  useSortInUrl: PropTypes.bool,
  onRowAction: PropTypes.func
}

export default ObjectTable
