import {
  type ColumnType,
  type TableType,
  type ViewType,
  isCreatedOrLastModifiedByCol,
  isCreatedOrLastModifiedTimeCol,
  isVirtualCol,
} from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
import { findGroupByPath } from '../components/smartsheet/grid/canvas/utils/groupby'
import type { CanvasGroup } from '../lib/types'
import { useInfiniteGroups } from './useInfiniteGroups'
import { type CellRange, type Row } from '#imports'

export function useGridViewData(
  _meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
  viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>,
  where?: ComputedRef<string | undefined>,
  reloadVisibleDataHook?: EventHook<void>,
) {
  const tablesStore = useTablesStore()
  const { activeTable } = storeToRefs(tablesStore)
  const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())

  const meta = computed(() => _meta.value || activeTable.value)

  const { t } = useI18n()

  const optimisedQuery = useState('optimisedQuery', () => true)

  const isPublic = inject(IsPublicInj, ref(false))

  const { getMeta } = useMetas()

  const reloadAggregate = inject(ReloadAggregateHookInj)

  const { addUndo, clone, defineViewScope } = useUndoRedo()

  const { base } = storeToRefs(useBase())

  const { $api } = useNuxtApp()

  const isBulkOperationInProgress = ref(false)

  const {
    cachedGroups,
    totalGroups,
    toggleExpand,
    groupByColumns,
    isGroupBy,
    buildNestedFilterArr,
    clearGroupCache,
    syncCount: groupSyncCount,
    fetchMissingGroupChunks,
    updateGroupAggregations,
    toggleExpandAll,
  } = useInfiniteGroups(viewMeta, meta, where, {
    syncVisibleData,
  })

  const {
    insertRow,
    updateRowProperty,
    addEmptyRow,
    deleteRow,
    deleteRowById,
    updateOrSaveRow,
    cachedRows,
    clearCache,
    totalRows,
    actualTotalRows,
    bulkUpdateView,
    removeRowIfNew,
    syncCount,
    fetchChunk,
    recoverLTARRefs,
    getChunkIndex,
    selectedRows,
    chunkStates,
    isRowSortRequiredRows,
    clearInvalidRows,
    applySorting,
    CHUNK_SIZE,
    isLastRow,
    isFirstRow,
    getExpandedRowIndex,
    loadData,
    updateRecordOrder,
    selectedAllRecords,
    selectedAllRecordsSkipPks,
    loadAggCommentsCount,
    navigateToSiblingRow,
    getRows,
    getDataCache,
    groupDataCache,
  } = useInfiniteData({
    meta,
    viewMeta,
    callbacks: {
      syncVisibleData,
      getCount,
      getWhereFilter: async (_path?: Array<number>) => where?.value ?? '',
      getWhereFilterArr: getGroupFilterArr,
      reloadAggregate: triggerAggregateReload,
      findGroupByPath: (path?: Array<number>) => {
        return findGroupByPath(cachedGroups.value, path)
      },
    },
    groupByColumns,
    where,
    isPublic,
  })

  function triggerAggregateReload(params: {
    fields?: Array<{ title: string; aggregation?: string | undefined }>
    path: Array<number>
  }) {
    const { fields, path } = params

    reloadAggregate?.trigger(params)

    if (!isGroupBy.value) {
      return
    }

    function collectChildGroups(group: CanvasGroup): CanvasGroup[] {
      const result: CanvasGroup[] = [group]
      if (group.groups && group.groups.size > 0) {
        for (const childGroup of group.groups.values()) {
          result.push(...collectChildGroups(childGroup))
        }
      }
      return result
    }

    const targetGroup = findGroupByPath(cachedGroups.value, path)
    if (!targetGroup) return

    const groupsToUpdate = collectChildGroups(targetGroup)
    updateGroupAggregations(groupsToUpdate, fields)

    if (path.length > 1) {
      for (let i = path.length - 1; i > 0; i--) {
        const parentPath = path.slice(0, i)
        const parentGroup = findGroupByPath(cachedGroups.value, parentPath)
        if (parentGroup) {
          const parentAndChildren = collectChildGroups(parentGroup)
          updateGroupAggregations(parentAndChildren, fields)
        }
      }
    }
  }

  const reloadAggregateListener = (v: Record<string, any> = {}) => {
    const { path, fields } = v
    if (!path?.length && isGroupBy.value) {
      const allGroups: CanvasGroup[] = []

      function collectAllGroups(groups: Map<number, CanvasGroup>) {
        const groupArray = Array.from(groups.values())
        allGroups.push(...groupArray)

        for (const group of groupArray) {
          if (group.groups && group.groups.size > 0) {
            collectAllGroups(group.groups)
          }
        }
      }

      collectAllGroups(cachedGroups.value)

      if (allGroups.length) {
        updateGroupAggregations(allGroups, fields)
      }
    }
  }

  reloadAggregate?.on(reloadAggregateListener)

  onBeforeUnmount(() => {
    reloadAggregate?.off(reloadAggregateListener)
  })

  function getCount(path?: Array<number>) {
    if (!path?.length) return
    let currentGroups = cachedGroups.value
    const pathCopy = [...path]

    for (let i = 0; i < path.length - 1; i++) {
      const groupIndex = pathCopy[i]
      const group = currentGroups.get(groupIndex)

      if (!group || !group.groups) {
        console.warn(`Invalid path: Group at index ${groupIndex} not found or has no subgroups`)
        return undefined
      }

      currentGroups = group.groups
    }

    const finalIndex = pathCopy[path.length - 1]
    const targetGroup = currentGroups.get(finalIndex)

    if (!targetGroup) {
      console.warn(`Target group at path [${path}] not found`)
      return undefined
    }

    return targetGroup.count
  }

  async function getGroupFilterArr(path: Array<number> = [], _ignoreWhereFilter = false) {
    let group = findGroupByPath(cachedGroups.value, path)

    if (!group) {
      try {
        let currentGroups = cachedGroups.value
        let parentGroup: CanvasGroup | undefined
        let targetIndex: number | undefined

        for (let depth = 0; depth < path.length; depth++) {
          const groupIndex = path[depth]
          const currentGroup = currentGroups.get(groupIndex)

          if (!currentGroup) {
            targetIndex = groupIndex
            break
          }

          if (depth === path.length - 1) {
            targetIndex = groupIndex
            break
          }

          if (!currentGroup.isExpanded || !currentGroup.groups) {
            return []
          }

          parentGroup = currentGroup
          currentGroups = currentGroup.groups
        }

        if (targetIndex !== undefined) {
          await fetchMissingGroupChunks(targetIndex, targetIndex, parentGroup)
        }

        group = findGroupByPath(cachedGroups.value, path)
      } catch (error) {
        console.error(`Failed to load group for path ${path}:`, error)
        return []
      }
    }

    return buildNestedFilterArr(group)
  }

  function syncVisibleData() {
    reloadVisibleDataHook?.trigger()
  }

  async function deleteSelectedRows(path: Array<number> = []): Promise<void> {
    let removedRowsData: Record<string, any>[] = []
    let compositePrimaryKey = ''
    isBulkOperationInProgress.value = true
    const dataCache = getDataCache(path)

    for (const row of dataCache.selectedRows.value) {
      const { row: rowData, rowMeta } = row

      if (!rowMeta.selected || rowMeta.new) {
        continue
      }

      const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
      const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[]) as string
      const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])

      if (extractedPk && compositePkValue) {
        if (!compositePrimaryKey) compositePrimaryKey = extractedPk
        removedRowsData.push({
          [compositePrimaryKey]: compositePkValue as string,
          pkData,
          row: clone(rowData),
          rowMeta,
        })
      }
    }

    if (!removedRowsData.length) return

    try {
      const { list } = await $api.internal.getOperation((meta.value as any).fk_workspace_id!, meta.value!.base_id!, {
        operation: 'dataList',
        tableId: meta.value?.id as string,
        pks: removedRowsData.map((row) => row[compositePrimaryKey]).join(','),
        getHiddenColumns: true,
        limit: removedRowsData.length,
      })

      removedRowsData = removedRowsData.map((row) => {
        const rowObj = row.row
        const rowPk = rowPkData(rowObj, meta.value?.columns as ColumnType[])

        const fullRecord = list.find((r: Record<string, any>) => {
          return Object.keys(rowPk).every((key) => r[key] === rowPk[key])
        })

        if (!fullRecord) return { ...row }
        return {
          ...row,
          row: { ...fullRecord },
        }
      })

      await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
    } catch (e: any) {
      const errorMessage = await extractSdkResponseErrorMsg(e)
      isBulkOperationInProgress.value = false
      return message.error(`${t('msg.error.deleteRowFailed')}: ${errorMessage}`)
    }

    await updateCacheAfterDelete(removedRowsData, false, path)

    addUndo({
      undo: {
        fn: async (removedRowsData: Record<string, any>[], path: Array<number>) => {
          const rowsToInsert = removedRowsData.reverse()

          const insertedRowIds = await bulkInsertRows(rowsToInsert as Row[], undefined, true, path)

          if (Array.isArray(insertedRowIds)) {
            await Promise.all(rowsToInsert.map((row, _index) => recoverLTARRefs(row.row)))
          }
        },
        args: [removedRowsData, clone(path)],
      },
      redo: {
        fn: async (toBeRemovedData: Record<string, any>[], path: Array<number>) => {
          try {
            isBulkOperationInProgress.value = true

            await bulkDeleteRows(toBeRemovedData.map((row) => row.pkData))

            await updateCacheAfterDelete(toBeRemovedData, false, path)

            await syncCount(path)
          } finally {
            isBulkOperationInProgress.value = false
          }
        },
        args: [removedRowsData, clone(path)],
      },
      scope: defineViewScope({ view: viewMeta.value }),
    })
    isBulkOperationInProgress.value = false

    await syncCount(path)
  }

  async function bulkInsertRows(
    rows: Row[],
    {
      metaValue = meta.value,
      viewMetaValue = viewMeta.value,
    }: {
      metaValue?: TableType
      viewMetaValue?: ViewType
    } = {},
    undo = false,
    path: Array<number> = [],
  ): Promise<string[]> {
    if (!metaValue || !viewMetaValue) {
      throw new Error('Meta value or view meta value is undefined')
    }

    const dataCache = getDataCache(path)

    isBulkOperationInProgress.value = true

    const autoGeneratedKeys = new Set(
      metaValue.columns
        ?.filter((c) => !c.pk && (isCreatedOrLastModifiedTimeCol(c) || isCreatedOrLastModifiedByCol(c)))
        .map((c) => c.title!),
    )

    try {
      const rowsToInsert = await Promise.all(
        rows.map(async (currentRow) => {
          const { missingRequiredColumns, insertObj } = await populateInsertObject({
            meta: metaValue,
            ltarState: {},
            getMeta,
            row: currentRow.row,
            undo,
          })

          if (missingRequiredColumns.size === 0) {
            const newInsertObj = { ...insertObj }
            for (const key of autoGeneratedKeys) {
              delete newInsertObj[key]
            }
            return {
              insertObj: newInsertObj,
              rowIndex: currentRow.rowMeta.rowIndex,
            }
          }
          return null
        }),
      )

      const validRowsToInsert = rowsToInsert.filter(Boolean) as { insertObj: Record<string, any>; rowIndex: number }[]

      const bulkInsertedIds = await $api.dbDataTableRow.create(
        metaValue.id!,
        validRowsToInsert.map((row) => row!.insertObj),
        {
          viewId: viewMetaValue.id,
          undo,
        },
      )

      validRowsToInsert.sort((a, b) => (a!.rowIndex ?? 0) - (b!.rowIndex ?? 0))

      const newCachedRows = new Map<number, Row>()

      for (const [index, row] of dataCache.cachedRows.value) {
        newCachedRows.set(index, { ...row, rowMeta: { ...row.rowMeta, rowIndex: index } })
      }

      for (const { insertObj, rowIndex } of validRowsToInsert) {
        // If there's already a row at this index, shift it and all subsequent rows
        if (newCachedRows.has(rowIndex!)) {
          const rowsToShift = Array.from(newCachedRows.entries())
            .filter(([index]) => index >= rowIndex!)
            .sort((a, b) => b[0] - a[0]) // Sort in descending order

          for (const [index, row] of rowsToShift) {
            const newIndex = index + 1
            newCachedRows.set(newIndex, { ...row, rowMeta: { ...row.rowMeta, rowIndex: newIndex } })
          }
        }

        const newRow = {
          row: { ...insertObj, id: bulkInsertedIds[validRowsToInsert.indexOf({ insertObj, rowIndex })] },
          oldRow: {},
          rowMeta: { rowIndex: rowIndex!, new: false, path },
        }
        newCachedRows.set(rowIndex!, newRow)
      }

      const indices = new Set<number>()
      for (const [_, row] of newCachedRows) {
        if (indices.has(row.rowMeta.rowIndex)) {
          console.error(`Op: bulkInsertRows ${undo}:  Duplicate index detected:`, row.rowMeta.rowIndex)
          break
        }
        indices.add(row.rowMeta.rowIndex)
      }

      dataCache.cachedRows.value = newCachedRows

      dataCache.totalRows.value += validRowsToInsert.length

      await syncCount(path, true, false)
      syncVisibleData()

      return bulkInsertedIds
    } catch (error: any) {
      const errorMessage = await extractSdkResponseErrorMsg(error)
      message.error(`Failed to bulk insert rows: ${errorMessage}`)
      throw error
    } finally {
      isBulkOperationInProgress.value = false
    }
  }

  async function bulkUpdateRows(
    rows: Row[],
    props: string[],
    { metaValue = meta.value, onError }: { metaValue?: TableType; viewMetaValue?: ViewType; onError?: (e: any) => void } = {},
    undo = false,
    path: Array<number> = [],
  ): Promise<void> {
    isBulkOperationInProgress.value = true

    const dataCache = getDataCache(path)

    await Promise.all(
      rows.map(async (row) => {
        if (row.rowMeta) {
          row.rowMeta.changed = false
          await until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v)
          row.rowMeta.saving = true
        }
      }),
    )
    const pksIndex = [] as { pk: string; rowIndex: number }[]

    const updateArray = rows.map((row) => {
      const pk = rowPkData(row.row, metaValue?.columns as ColumnType[])
      const updateData = props.reduce((acc, prop) => ({ ...acc, [prop]: row.row[prop] }), {})
      pksIndex.push({
        pk: extractPkFromRow(row.row, metaValue?.columns as ColumnType[]) as string,
        rowIndex: row.rowMeta.rowIndex!,
      })
      return { ...updateData, ...pk }
    })

    try {
      const newRows = (await $api.dbTableRow.bulkUpdate(
        NOCO,
        metaValue?.base_id as string,
        metaValue?.id as string,
        updateArray,
      )) as Record<string, any>

      triggerAggregateReload({ fields: props.map((p) => ({ title: p })), path })

      newRows.forEach((newRow: Record<string, any>) => {
        const pk = extractPkFromRow(newRow, metaValue?.columns as ColumnType[])
        const rowIndex = pksIndex.find((pkIndex) => pkIndex.pk === pk)?.rowIndex

        if (rowIndex !== undefined && rowIndex !== null) {
          const row = dataCache.cachedRows.value.get(rowIndex)
          if (row) {
            row.rowMeta.saving = false
            row.row = {
              ...row.row,
              ...newRow,
            }
            dataCache.cachedRows.value.set(rowIndex, row)
          }
        }
      })
    } catch (e) {
      console.error(e)
      onError?.(e)
      message.error(await extractSdkResponseErrorMsg(e as any))
      isBulkOperationInProgress.value = false
      return
    } finally {
      rows.forEach((row) => {
        if (row.rowMeta) row.rowMeta.saving = false
      })
    }
    if (!undo) {
      addUndo({
        undo: {
          fn: async (undoRows: Row[], props: string[], path: Array<number>) => {
            await bulkUpdateRows(
              undoRows.map((r) => ({
                ...r,
                row: r.oldRow,
                oldRow: r.row,
              })),
              props,
              undefined,
              true,
              path,
            )
          },
          args: [clone(rows), props, clone(path)],
        },
        redo: {
          fn: async (redoRows: Row[], props: string[], path: Array<number>) => {
            await bulkUpdateRows(redoRows, props, undefined, true, path)
          },
          args: [clone(rows), props, clone(path)],
        },
        scope: defineViewScope({ view: viewMeta.value }),
      })
    }

    applySorting(rows)
    syncVisibleData()
    reloadViewDataHook?.trigger()
    isBulkOperationInProgress.value = false
  }

  async function bulkUpsertRows(
    insertRows: Row[],
    updateRows: Row[],
    props: string[],
    {
      metaValue = meta.value,
      viewMetaValue = viewMeta.value,
    }: {
      metaValue?: TableType
      viewMetaValue?: ViewType
    } = {},
    columns: Partial<ColumnType>[],
    undo = false,
    path: Array<number> = [],
  ) {
    const dataCache = getDataCache(path)

    try {
      isBulkOperationInProgress.value = true
      const newCols = (meta.value.columns ?? []).filter((col: ColumnType) => columns.some((c) => c.title === col.title))

      const rowsToFetch = updateRows.filter((row) => !dataCache.cachedRows.value.has(row.rowMeta.rowIndex!))
      const chunksToFetch = new Set(rowsToFetch.map((row) => Math.floor(row.rowMeta.rowIndex! / CHUNK_SIZE)))

      await Promise.all(Array.from(chunksToFetch).map((chunkId) => fetchChunk(chunkId, path)))

      const getPk = (row: Row) => extractPkFromRow(row.row, metaValue?.columns as ColumnType[])

      const ogUpdateRows = updateRows.map((_row) => {
        const row = _row ?? dataCache.cachedRows.value.get((_row as Row).rowMeta.rowIndex!)

        newCols.forEach((col: ColumnType) => {
          row.oldRow[col.title!] = undefined
        })

        return clone(row)
      })

      const cleanRow = (row: any) => {
        const cleanedRow = { ...row }
        metaValue?.columns?.forEach((col) => {
          if (col.system || isVirtualCol(col)) delete cleanedRow[col.title!]
        })
        return cleanedRow
      }

      updateRows = updateRows.map((row) => {
        const cachedRow = dataCache.cachedRows.value.get(row.rowMeta.rowIndex!)
        if (cachedRow) {
          return {
            ...cachedRow,
            row: { ...cachedRow.row, ...row.row },
            oldRow: cachedRow.row,
          }
        }
        return row
      })

      const bulkUpsertedRows = await $api.dbTableRow.bulkUpsert(
        NOCO,
        metaValue?.base_id ?? (base.value?.id as string),
        metaValue?.id as string,
        [...insertRows.map((row) => cleanRow(row.row)), ...updateRows.map((row) => cleanRow(row.row))],
        {},
      )

      const existingPks = new Set(Array.from(dataCache.cachedRows.value.values()).map((row) => getPk(row)))
      const [insertedRows, updatedRows] = bulkUpsertedRows.reduce(
        ([inserted, updated], row) => {
          const isPkExisting = existingPks.has(extractPkFromRow(row, metaValue?.columns as ColumnType[]))
          return isPkExisting
            ? [inserted, [...updated, { row, rowMeta: {}, oldRow: row }]]
            : [[...inserted, { row, rowMeta: {}, oldRow: {} }], updated]
        },
        [[], []] as [Row[], Row[]],
      )

      insertedRows.forEach((row: Row, index: number) => {
        const newIndex = dataCache.totalRows.value + index
        row.rowMeta.rowIndex = newIndex
        dataCache.cachedRows.value.set(newIndex, { ...row, rowMeta: { ...row.rowMeta, rowIndex: newIndex } })
      })
      updatedRows.forEach((row: Row) => {
        const existingRow = Array.from(dataCache.cachedRows.value.entries()).find(([_, r]) => getPk(r) === getPk(row))
        if (existingRow) {
          dataCache.cachedRows.value.set(existingRow[0], {
            ...row,
            rowMeta: { ...row.rowMeta, rowIndex: existingRow[0] },
          })
        }
      })

      dataCache.totalRows.value += insertedRows.length

      if (!undo) {
        addUndo({
          undo: {
            fn: async (insertedRows: Row[], ogUpdateRows: Row[], path: Array<number>) => {
              try {
                isBulkOperationInProgress.value = true

                await bulkDeleteRows(
                  insertedRows.map((row) => rowPkData(row.row, metaValue?.columns as ColumnType[]) as Record<string, any>),
                )
                await bulkUpdateRows(
                  ogUpdateRows.map((r) => ({
                    ...r,
                    row: r.oldRow,
                    oldRow: r.row,
                  })),
                  props,
                  { metaValue },
                  true,
                  path,
                )
                isBulkOperationInProgress.value = true

                const columnsHash = (
                  await $api.internal.getOperation(meta.value!.fk_workspace_id!, meta.value!.base_id!, {
                    operation: 'columnsHash',
                    tableId: meta.value?.id as string,
                  })
                ).hash

                await $api.internal.postOperation(
                  meta.value!.fk_workspace_id!,
                  meta.value!.base_id!,
                  { operation: 'columnsBulk', tableId: meta.value?.id as string },
                  {
                    hash: columnsHash,
                    ops: newCols.map((col: ColumnType) => ({
                      op: 'delete',
                      column: col,
                    })),
                  },
                )

                insertedRows.forEach((row) => {
                  dataCache.cachedRows.value.delete(row.rowMeta.rowIndex!)
                })

                dataCache.totalRows.value = dataCache.totalRows.value - insertedRows.length

                syncVisibleData()

                await getMeta(meta.value!.base_id!, meta.value?.id as string, true)
              } catch (e) {
              } finally {
                isBulkOperationInProgress.value = false
              }
            },
            args: [clone(insertedRows), clone(ogUpdateRows), clone(path)],
          },
          redo: {
            fn: async (insertRows: Row[], updateRows: Row[], path: Array<number>) => {
              try {
                isBulkOperationInProgress.value = true
                const columnsHash = (
                  await $api.internal.getOperation(meta.value!.fk_workspace_id!, meta.value!.base_id!, {
                    operation: 'columnsHash',
                    tableId: meta.value?.id as string,
                  })
                ).hash

                await $api.internal.postOperation(
                  meta.value!.fk_workspace_id!,
                  meta.value!.base_id!,
                  { operation: 'columnsBulk', tableId: meta.value?.id as string },
                  {
                    hash: columnsHash,
                    ops: newCols.map((col: ColumnType) => ({
                      op: 'add',
                      column: col,
                    })),
                  },
                )

                await bulkUpsertRows(insertRows, updateRows, props, { metaValue, viewMetaValue }, columns, true, path)
                isBulkOperationInProgress.value = true

                await getMeta(meta.value!.base_id!, meta.value?.id as string, true)

                syncVisibleData()
              } finally {
                isBulkOperationInProgress.value = false
              }
            },
            args: [clone(insertedRows), clone(updatedRows), clone(path)],
          },

          scope: defineViewScope({ view: viewMeta.value }),
        })
      }
      reloadViewDataHook?.trigger()
      syncVisibleData()
      await syncCount(path, true, false)
    } catch (error: any) {
      message.error(await extractSdkResponseErrorMsg(error))
    } finally {
      isBulkOperationInProgress.value = false
    }
  }

  async function updateCacheAfterDelete(
    rowsToDelete: Record<string, any>[],
    nested = true,
    path: Array<number> = [],
  ): Promise<void> {
    const dataCache = getDataCache(path)
    const maxCachedIndex = Math.max(...dataCache.cachedRows.value.keys())
    const newCachedRows = new Map<number, Row>()

    const deleteSet = new Set(rowsToDelete.map((row) => (nested ? row.row : row).rowMeta.rowIndex))
    const affectedChunks = new Set<number>()
    let deletionCount = 0

    for (let i = 0; i <= maxCachedIndex + 1; i++) {
      if (deleteSet.has(i)) {
        deletionCount++
        affectedChunks.add(getChunkIndex(i))
        continue
      }

      if (dataCache.cachedRows.value.has(i)) {
        const row = dataCache.cachedRows.value.get(i)
        if (row) {
          const newIndex = i - deletionCount

          row.rowMeta.rowIndex = newIndex
          newCachedRows.set(newIndex, row)
          affectedChunks.add(getChunkIndex(i))
          affectedChunks.add(getChunkIndex(newIndex))
        }
      }
    }

    const rowsByChunk = new Map<number, number>()
    for (const [_, row] of newCachedRows) {
      const chunkIndex = getChunkIndex(row.rowMeta.rowIndex)
      rowsByChunk.set(chunkIndex, (rowsByChunk.get(chunkIndex) || 0) + 1)
    }

    for (const chunkIndex of affectedChunks) {
      if (!rowsByChunk.has(chunkIndex) || rowsByChunk.get(chunkIndex) < CHUNK_SIZE) {
        dataCache.chunkStates.value[chunkIndex] = undefined
      }
    }

    const indices = new Set<number>()
    for (const [_, row] of newCachedRows) {
      if (indices.has(row.rowMeta.rowIndex)) {
        console.error(`Op: updateCacheAfterDelete: Duplicate index detected:`, row.rowMeta.rowIndex)
        break
      }
      indices.add(row.rowMeta.rowIndex)
    }

    dataCache.cachedRows.value = newCachedRows
    dataCache.totalRows.value = Math.max(0, dataCache.totalRows.value - rowsToDelete.length)

    await syncCount(path)
    syncVisibleData()
  }

  async function deleteRangeOfRows(cellRange: CellRange, path: Array<number> = []): Promise<void> {
    if (!cellRange._start || !cellRange._end) return
    isBulkOperationInProgress.value = true
    const dataCache = getDataCache(path)

    const start = Math.min(cellRange._start.row, cellRange._end.row)
    const end = Math.max(cellRange._start.row, cellRange._end.row)

    let rowsToDelete: Record<string, any>[] = []
    let compositePrimaryKey = ''

    await getRows(start, end, path)

    for (let i = start; i <= end; i++) {
      const cachedRow = dataCache.cachedRows.value.get(i)
      if (!cachedRow) {
        console.warn(`Record at index ${i} not found in local cache`)
        continue
      }

      const { row: rowData, rowMeta } = cachedRow

      if (!rowMeta.new) {
        const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
        const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[])
        const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])

        if (extractedPk && compositePkValue) {
          if (!compositePrimaryKey) compositePrimaryKey = extractedPk

          rowsToDelete.push({
            [compositePrimaryKey]: compositePkValue,
            pkData,
            ...cachedRow,
            rowIndex: i,
          })
        }
      }
    }

    if (!rowsToDelete.length) return

    const { list } = await $api.internal.getOperation((meta.value as any).fk_workspace_id!, meta.value!.base_id!, {
      operation: 'dataList',
      tableId: meta.value?.id as string,
      pks: rowsToDelete.map((row) => row[compositePrimaryKey]).join(','),
      getHiddenColumns: 'true',
      limit: rowsToDelete.length,
    })

    try {
      rowsToDelete = rowsToDelete.map((row) => {
        const rowObj = row.row
        const rowPk = rowPkData(rowObj, meta.value?.columns as ColumnType[])

        const fullRecord = list.find((r: Record<string, any>) => {
          return Object.keys(rowPk).every((key) => r[key] === rowPk[key])
        })

        if (!fullRecord) {
          console.warn(`Full record not found for row with index ${row.rowMeta.rowIndex}`)
          return row
        }
        row.row = fullRecord
        return row
      })

      await bulkDeleteRows(rowsToDelete.map((row) => row.pkData))
    } catch (e: any) {
      const errorMessage = await extractSdkResponseErrorMsg(e)
      message.error(`${t('msg.error.deleteRowFailed')}: ${errorMessage}`)
      isBulkOperationInProgress.value = false
      throw e
    }

    addUndo({
      undo: {
        fn: async (deletedRows: Record<string, any>[], path: Array<number>) => {
          const rowsToInsert = deletedRows.reverse()

          const insertedRowIds = await bulkInsertRows(rowsToInsert, undefined, true, path)

          if (Array.isArray(insertedRowIds)) {
            await Promise.all(rowsToInsert.map((row, _index) => recoverLTARRefs(row.row)))
          }
        },
        args: [rowsToDelete, clone(path)],
      },
      redo: {
        fn: async (rowsToDelete: Record<string, any>[], path: Array<number>) => {
          await bulkDeleteRows(rowsToDelete.map((row) => row.pkData))
          await updateCacheAfterDelete(rowsToDelete, false, path)
        },
        args: [rowsToDelete, clone(path)],
      },
      scope: defineViewScope({ view: viewMeta.value }),
    })
    await updateCacheAfterDelete(rowsToDelete, false, path)
    isBulkOperationInProgress.value = false
  }

  async function bulkDeleteRows(
    rows: Record<string, string>[],
    {
      metaValue = meta.value,
      viewMetaValue = viewMeta.value,
    }: {
      metaValue?: TableType
      viewMetaValue?: ViewType
    } = {},
  ): Promise<any> {
    try {
      const bulkDeletedRowsData = await $api.internal.postOperation(
        (metaValue as any).fk_workspace_id!,
        metaValue!.base_id!,
        {
          operation: 'dataDelete',
          tableId: metaValue?.id as string,
          viewId: viewMetaValue?.id as string,
        },
        rows.length === 1 ? rows[0] : rows,
      )

      triggerAggregateReload({ path: [] })

      return rows.length === 1 && bulkDeletedRowsData ? [bulkDeletedRowsData] : bulkDeletedRowsData
    } catch (error: any) {
      const errorMessage = await extractSdkResponseErrorMsg(error)
      message.error(`Bulk delete failed: ${errorMessage}`)
    }
  }

  async function bulkDeleteAll(path: Array<number> = []) {
    try {
      isBulkOperationInProgress.value = true

      await $api.internal.postOperation(
        (meta.value as any).fk_workspace_id!,
        meta.value!.base_id!,
        {
          operation: 'bulkDataDeleteAll',
          tableId: meta.value.id!,
          where: where?.value,
          viewId: viewMeta.value?.id,
          skipPks: Object.values(selectedAllRecordsSkipPks.value).join(','),
        },
        {},
      )
    } catch (error) {
    } finally {
      clearCache(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, path)
      await syncCount(path)
      syncVisibleData?.()
      isBulkOperationInProgress.value = false
    }
  }

  return {
    cachedRows,
    loadData,
    insertRow,
    updateRowProperty,
    addEmptyRow,
    deleteRow,
    deleteRowById,
    deleteSelectedRows,
    deleteRangeOfRows,
    updateOrSaveRow,
    bulkUpdateRows,
    bulkUpsertRows,
    bulkUpdateView,
    bulkDeleteAll,
    loadAggCommentsCount,
    syncCount,
    removeRowIfNew,
    navigateToSiblingRow,
    getExpandedRowIndex,
    optimisedQuery,
    isLastRow,
    isFirstRow,
    clearCache,
    totalRows,
    actualTotalRows,
    selectedRows,
    syncVisibleData,
    chunkStates,
    clearInvalidRows,
    applySorting,
    isRowSortRequiredRows,
    isBulkOperationInProgress,
    updateRecordOrder,
    selectedAllRecords,
    selectedAllRecordsSkipPks,
    getRows,
    getDataCache,
    groupDataCache,
    // Groupby
    cachedGroups,
    totalGroups,
    toggleExpand,
    groupByColumns,
    isGroupBy,
    groupSyncCount,
    fetchMissingGroupChunks,
    clearGroupCache,
    toggleExpandAll,
  }
}
