/**
 * A utility for creating a proxy that captures changes to an object
 * and provides a way to retrieve those changes.
 */

import { deepEquals, isTemporal } from './utils'

// Resolve draft handles before calling native Map/Set membership methods.
const draftCopies = new WeakMap<object, object>()
function unwrapDraft(value: unknown): unknown {
  return value !== null && typeof value === `object`
    ? (draftCopies.get(value) ?? value)
    : value
}

/**
 * Set of array methods that iterate with callbacks and may return elements.
 * Hoisted to module scope to avoid creating a new Set on every property access.
 */
const CALLBACK_ITERATION_METHODS = new Set([
  `find`,
  `findLast`,
  `findIndex`,
  `findLastIndex`,
  `filter`,
  `map`,
  `flatMap`,
  `forEach`,
  `some`,
  `every`,
  `reduce`,
  `reduceRight`,
])

/**
 * Set of array methods that modify the array in place.
 */
const ARRAY_MODIFYING_METHODS = new Set([
  `pop`,
  `push`,
  `shift`,
  `unshift`,
  `splice`,
  `sort`,
  `reverse`,
  `fill`,
  `copyWithin`,
])

/**
 * Set of Map/Set iterator methods.
 */
const MAP_SET_ITERATOR_METHODS = new Set([
  `entries`,
  `keys`,
  `values`,
  `forEach`,
])

/**
 * Check if a value is a proxiable object (not Date, RegExp, or Temporal)
 */
function isProxiableObject(
  value: unknown,
): value is Record<string | symbol, unknown> {
  return (
    value !== null &&
    typeof value === `object` &&
    !((value as any) instanceof Date) &&
    !((value as any) instanceof RegExp) &&
    !isTemporal(value)
  )
}

/**
 * Creates a handler for array iteration methods that ensures proxied elements
 * are passed to callbacks and returned from methods like find/filter.
 */
function createArrayIterationHandler<T extends object>(
  methodName: string,
  methodFn: (...args: Array<unknown>) => unknown,
  changeTracker: ChangeTracker<T>,
  memoizedCreateChangeProxy: (
    obj: Record<string | symbol, unknown>,
    parent?: {
      tracker: ChangeTracker<Record<string | symbol, unknown>>
      prop: string | symbol
    },
  ) => { proxy: Record<string | symbol, unknown> },
): ((...args: Array<unknown>) => unknown) | undefined {
  if (!CALLBACK_ITERATION_METHODS.has(methodName)) {
    return undefined
  }

  return function (...args: Array<unknown>) {
    const callback = args[0]
    if (typeof callback !== `function`) {
      return methodFn.apply(changeTracker.copy_, args)
    }

    // Create a helper to get proxied version of an array element
    const getProxiedElement = (element: unknown, index: number): unknown => {
      if (isProxiableObject(element)) {
        const nestedParent = {
          tracker: changeTracker as unknown as ChangeTracker<
            Record<string | symbol, unknown>
          >,
          prop: String(index),
        }
        const { proxy: elementProxy } = memoizedCreateChangeProxy(
          element,
          nestedParent,
        )
        return elementProxy
      }
      return element
    }

    // Wrap the callback to pass proxied elements
    const wrappedCallback = function (
      this: unknown,
      element: unknown,
      index: number,
      array: unknown,
    ) {
      const proxiedElement = getProxiedElement(element, index)
      return callback.call(this, proxiedElement, index, array)
    }

    // For reduce/reduceRight, the callback signature is different
    if (methodName === `reduce` || methodName === `reduceRight`) {
      const reduceCallback = function (
        this: unknown,
        accumulator: unknown,
        element: unknown,
        index: number,
        array: unknown,
      ) {
        const proxiedElement = getProxiedElement(element, index)
        return callback.call(this, accumulator, proxiedElement, index, array)
      }
      return methodFn.apply(changeTracker.copy_, [
        reduceCallback,
        ...args.slice(1),
      ])
    }

    const result = methodFn.apply(changeTracker.copy_, [
      wrappedCallback,
      ...args.slice(1),
    ])

    // For find/findLast, proxy the returned element if it's an object
    if (
      (methodName === `find` || methodName === `findLast`) &&
      result &&
      typeof result === `object`
    ) {
      const foundIndex = (
        changeTracker.copy_ as unknown as Array<unknown>
      ).indexOf(result)
      if (foundIndex !== -1) {
        return getProxiedElement(result, foundIndex)
      }
    }

    // For filter, proxy each element in the result array
    if (methodName === `filter` && Array.isArray(result)) {
      return result.map((element) => {
        const originalIndex = (
          changeTracker.copy_ as unknown as Array<unknown>
        ).indexOf(element)
        if (originalIndex !== -1) {
          return getProxiedElement(element, originalIndex)
        }
        return element
      })
    }

    return result
  }
}

/**
 * Creates a Symbol.iterator handler for arrays that yields proxied elements.
 */
function createArrayIteratorHandler<T extends object>(
  changeTracker: ChangeTracker<T>,
  memoizedCreateChangeProxy: (
    obj: Record<string | symbol, unknown>,
    parent?: {
      tracker: ChangeTracker<Record<string | symbol, unknown>>
      prop: string | symbol
    },
  ) => { proxy: Record<string | symbol, unknown> },
): () => Iterator<unknown> {
  return function () {
    const array = changeTracker.copy_ as unknown as Array<unknown>
    let index = 0

    return {
      next() {
        if (index >= array.length) {
          return { done: true, value: undefined }
        }

        const element = array[index]
        let proxiedElement = element

        if (isProxiableObject(element)) {
          const nestedParent = {
            tracker: changeTracker as unknown as ChangeTracker<
              Record<string | symbol, unknown>
            >,
            prop: String(index),
          }
          const { proxy: elementProxy } = memoizedCreateChangeProxy(
            element,
            nestedParent,
          )
          proxiedElement = elementProxy
        }

        index++
        return { done: false, value: proxiedElement }
      },
      [Symbol.iterator]() {
        return this
      },
    }
  }
}

/**
 * Creates a wrapper for methods that modify a collection (array, Map, Set).
 * The wrapper calls the method and marks the change tracker as modified.
 */
function createModifyingMethodHandler<T extends object>(
  methodFn: (...args: Array<unknown>) => unknown,
  changeTracker: ChangeTracker<T>,
  markChanged: (tracker: ChangeTracker<T>) => void,
): (...args: Array<unknown>) => unknown {
  return function (...args: Array<unknown>) {
    const result = methodFn.apply(changeTracker.copy_, args)
    markChanged(changeTracker)
    return result
  }
}

/**
 * Use the native live iterator, but expose tracked values. Editing an entry
 * changes its owned draft copy in place; it must not delete/reinsert a Set slot.
 */
function createMapSetIteratorHandler<T extends object>(
  methodName: string,
  prop: string | symbol,
  changeTracker: ChangeTracker<T>,
  collectionProxy: unknown,
  memoizedCreateChangeProxy: (
    obj: Record<string | symbol, unknown>,
    parent?: ChangeParent,
  ) => { proxy: Record<string | symbol, unknown> },
): ((...args: Array<unknown>) => unknown) | undefined {
  if (!MAP_SET_ITERATOR_METHODS.has(methodName) && prop !== Symbol.iterator) {
    return undefined
  }

  return (...args) => {
    const copy = changeTracker.copy_ as Map<unknown, unknown> | Set<unknown>
    const isMap = copy instanceof Map
    if (isMap && methodName === `keys`) return copy.keys()

    const track = (value: unknown) =>
      isProxiableObject(value)
        ? memoizedCreateChangeProxy(value, {
            tracker: changeTracker as unknown as ChangeTracker<
              Record<string | symbol, unknown>
            >,
            prop: ``,
            retainIdentity: true,
          }).proxy
        : value

    if (methodName === `forEach`) {
      const callback = args[0]
      if (typeof callback !== `function`)
        throw new TypeError(`forEach callback must be a function`)
      return copy.forEach((value, key) => {
        const tracked = track(value)
        callback.call(args[1], tracked, isMap ? key : tracked, collectionProxy)
      })
    }

    const entries = copy.entries()
    const pairs =
      methodName === `entries` || (isMap && prop === Symbol.iterator)
    return {
      next() {
        const result = entries.next()
        if (result.done) return result
        const [key, value] = result.value
        const tracked = track(value)
        return {
          done: false,
          value: pairs ? [isMap ? key : tracked, tracked] : tracked,
        }
      },
      [Symbol.iterator]() {
        return this
      },
    }
  }
}

// Add TypedArray interface with proper type
interface TypedArray {
  length: number
  [index: number]: number
}

// Update type for ChangeTracker
interface ChangeParent {
  tracker: ChangeTracker<Record<string | symbol, unknown>>
  prop: string | symbol
  // Map/Set entries already belong to the parent's private copy.
  retainIdentity?: boolean
}

interface ChangeTracker<T extends object> {
  valueCopies: WeakMap<object, unknown>
  originalObject: T
  modified: boolean
  copy_: T
  assigned_: Record<string | symbol, boolean>
  parent?: ChangeParent
  target: T
}

/**
 * Deep clones an object while preserving special types like Date and RegExp
 */

function deepClone<T extends unknown>(
  obj: T,
  visited = new WeakMap<object, unknown>(),
): T {
  // Handle null and undefined
  if (obj === null || obj === undefined) {
    return obj
  }

  // Handle primitive types
  if (typeof obj !== `object`) {
    return obj
  }

  // If we've already cloned this object, return the cached clone
  if (visited.has(obj as object)) {
    return visited.get(obj as object) as T
  }

  if (obj instanceof Date) {
    return new Date(obj.getTime()) as unknown as T
  }

  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags) as unknown as T
  }

  if (Array.isArray(obj)) {
    const arrayClone = [] as Array<unknown>
    visited.set(obj as object, arrayClone)
    obj.forEach((item, index) => {
      arrayClone[index] = deepClone(item, visited)
    })
    return arrayClone as unknown as T
  }

  // Handle TypedArrays
  if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
    // Get the constructor to create a new instance of the same type
    const TypedArrayConstructor = Object.getPrototypeOf(obj).constructor
    const clone = new TypedArrayConstructor(
      (obj as unknown as TypedArray).length,
    ) as unknown as TypedArray
    visited.set(obj as object, clone)

    // Copy the values
    for (let i = 0; i < (obj as unknown as TypedArray).length; i++) {
      clone[i] = (obj as unknown as TypedArray)[i]!
    }

    return clone as unknown as T
  }

  if (obj instanceof Map) {
    const clone = new Map() as Map<unknown, unknown>
    visited.set(obj as object, clone)
    obj.forEach((value, key) => {
      clone.set(key, deepClone(value, visited))
    })
    return clone as unknown as T
  }

  if (obj instanceof Set) {
    const clone = new Set()
    visited.set(obj as object, clone)
    obj.forEach((value) => {
      clone.add(deepClone(value, visited))
    })
    return clone as unknown as T
  }

  // Handle Temporal objects
  if (isTemporal(obj)) {
    // Temporal objects are immutable, so we can return them directly
    // This preserves all their internal state correctly
    return obj
  }

  const clone = {} as Record<string | symbol, unknown>
  visited.set(obj as object, clone)

  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clone[key] = deepClone(
        (obj as Record<string | symbol, unknown>)[key],
        visited,
      )
    }
  }

  const symbolProps = Object.getOwnPropertySymbols(obj)
  for (const sym of symbolProps) {
    clone[sym] = deepClone(
      (obj as Record<string | symbol, unknown>)[sym],
      visited,
    )
  }

  return clone as T
}

/**
 * Creates a proxy that tracks changes to the target object
 *
 * @param target The object to proxy
 * @param parent Optional parent information
 * @returns An object containing the proxy and a function to get the changes
 */
export function createChangeProxy<
  T extends Record<string | symbol, any | undefined>,
>(
  target: T,
  parent?: ChangeParent,
): {
  proxy: T

  getChanges: () => Record<string | symbol, any>
} {
  const changeProxyCache = new Map<object, object>()

  function memoizedCreateChangeProxy<
    TInner extends Record<string | symbol, any | undefined>,
  >(
    innerTarget: TInner,
    innerParent?: ChangeParent,
  ): {
    proxy: TInner
    getChanges: () => Record<string | symbol, any>
  } {
    if (changeProxyCache.has(innerTarget)) {
      return changeProxyCache.get(innerTarget) as {
        proxy: TInner
        getChanges: () => Record<string | symbol, any>
      }
    } else {
      const changeProxy = createChangeProxy(innerTarget, innerParent)
      changeProxyCache.set(innerTarget, changeProxy)
      return changeProxy
    }
  }
  // Create a WeakMap to cache proxies for nested objects
  // This prevents creating multiple proxies for the same object
  // and handles circular references
  const proxyCache = new Map<object, object>()

  // Existing values share one private copy per row. Newly inserted objects
  // retain normal references during the callback; the result is detached below.
  const valueCopies =
    parent?.tracker.valueCopies ?? new WeakMap<object, unknown>()
  const changeTracker: ChangeTracker<T> = {
    valueCopies,
    copy_: parent
      ? ((valueCopies.get(target) ?? target) as T)
      : deepClone(target, valueCopies),
    originalObject: deepClone(target),
    modified: false,
    assigned_: {},
    parent,
    target, // Store reference to the target object
  }

  // Mark this object and all its ancestors as modified
  // Also propagate the actual changes up the chain
  function markChanged(state: ChangeTracker<object>) {
    if (!state.modified) {
      state.modified = true
    }

    // Propagate the change up the parent chain
    if (state.parent) {
      if (!state.parent.retainIdentity) {
        // Update parent's copy with this object's current state
        state.parent.tracker.copy_[state.parent.prop] = state.copy_
        state.parent.tracker.assigned_[state.parent.prop] = true
      }

      // Mark parent as changed
      markChanged(state.parent.tracker)
    }
  }

  // Check if all properties in the current state have reverted to original values
  function checkIfReverted(
    state: ChangeTracker<Record<string | symbol, unknown>>,
  ): boolean {
    if (state.copy_ instanceof Map || state.copy_ instanceof Set) {
      // Compare entry contents: these containers have no assigned properties.
      return deepEquals(
        Array.from(state.copy_),
        Array.from(
          state.originalObject as unknown as
            | Map<unknown, unknown>
            | Set<unknown>,
        ),
      )
    }
    // If there are no assigned properties, object is unchanged
    if (
      Object.keys(state.assigned_).length === 0 &&
      Object.getOwnPropertySymbols(state.assigned_).length === 0
    ) {
      return true
    }

    // Check each assigned regular property
    for (const prop in state.assigned_) {
      // If this property is marked as assigned
      if (state.assigned_[prop] === true) {
        const currentValue = state.copy_[prop]
        const originalValue = (state.originalObject as any)[prop]

        // If the value is not equal to original, something is still changed
        if (!deepEquals(currentValue, originalValue)) {
          return false
        }
      } else if (state.assigned_[prop] === false) {
        // Property was deleted, so it's different from original
        return false
      }
    }

    // Check each assigned symbol property
    const symbolProps = Object.getOwnPropertySymbols(state.assigned_)
    for (const sym of symbolProps) {
      if (state.assigned_[sym] === true) {
        const currentValue = (state.copy_ as any)[sym]
        const originalValue = (state.originalObject as any)[sym]

        // If the value is not equal to original, something is still changed
        if (!deepEquals(currentValue, originalValue)) {
          return false
        }
      } else if (state.assigned_[sym] === false) {
        // Property was deleted, so it's different from original
        return false
      }
    }

    // All assigned properties match their original values
    return true
  }

  // Update parent status based on child changes
  function checkParentStatus(
    parentState: ChangeTracker<Record<string | symbol, unknown>>,
  ) {
    // Check if all properties of the parent are reverted
    const isReverted = checkIfReverted(parentState)

    if (isReverted) {
      // If everything is reverted, clear the tracking
      parentState.modified = false
      parentState.assigned_ = {}

      // Continue up the chain
      if (parentState.parent) {
        checkParentStatus(parentState.parent.tracker)
      }
    }
  }

  // Create a proxy for the target object
  function createObjectProxy<TObj extends object>(obj: TObj): TObj {
    // If we've already created a proxy for this object, return it
    if (proxyCache.has(obj)) {
      return proxyCache.get(obj) as TObj
    }

    // Create a proxy for the object
    const proxy = new Proxy(obj, {
      get(ptarget, prop, receiver) {
        const value =
          changeTracker.copy_[prop as keyof T] ??
          changeTracker.originalObject[prop as keyof T]

        // If it's a getter, return the value directly
        const desc = Object.getOwnPropertyDescriptor(ptarget, prop)
        if (desc?.get) {
          return value
        }

        // If the value is a function, bind it to the ptarget
        if (typeof value === `function`) {
          // For Array methods that modify the array
          if (Array.isArray(ptarget)) {
            const methodName = prop.toString()

            if (ARRAY_MODIFYING_METHODS.has(methodName)) {
              return createModifyingMethodHandler(
                value,
                changeTracker,
                markChanged,
              )
            }

            // Handle array iteration methods (find, filter, forEach, etc.)
            const iterationHandler = createArrayIterationHandler(
              methodName,
              value,
              changeTracker,
              memoizedCreateChangeProxy,
            )
            if (iterationHandler) {
              return iterationHandler
            }

            // Handle array Symbol.iterator for for...of loops
            if (prop === Symbol.iterator) {
              return createArrayIteratorHandler(
                changeTracker,
                memoizedCreateChangeProxy,
              )
            }
          }

          // For Map and Set methods that modify the collection
          if (ptarget instanceof Map || ptarget instanceof Set) {
            const methodName = prop.toString()

            const resolveValue = (entry: unknown) => {
              const raw = unwrapDraft(entry)
              return raw !== null && typeof raw === `object`
                ? (valueCopies.get(raw) ?? raw)
                : raw
            }

            if (
              methodName === `has` ||
              methodName === `delete` ||
              methodName === `add` ||
              methodName === `set`
            ) {
              return (...args: Array<unknown>) => {
                if (ptarget instanceof Set) args[0] = resolveValue(args[0])
                else if (methodName === `set`) args[1] = resolveValue(args[1])
                const result = value.apply(ptarget, args)
                if (methodName !== `has`) markChanged(changeTracker)
                return result === ptarget ? receiver : result
              }
            }

            if (ptarget instanceof Map && methodName === `get`) {
              return (key: unknown) => {
                const entry = ptarget.get(key)
                return isProxiableObject(entry)
                  ? memoizedCreateChangeProxy(entry, {
                      tracker: changeTracker,
                      prop: ``,
                      retainIdentity: true,
                    }).proxy
                  : entry
              }
            }

            if (methodName === `clear`) {
              return createModifyingMethodHandler(
                value,
                changeTracker,
                markChanged,
              )
            }

            // Handle iterator methods for Map and Set
            const iteratorHandler = createMapSetIteratorHandler(
              methodName,
              prop,
              changeTracker,
              receiver,
              memoizedCreateChangeProxy,
            )
            if (iteratorHandler) {
              return iteratorHandler
            }
          }
          return value.bind(ptarget)
        }

        // If the value is an object (but not Date, RegExp, or Temporal), create a proxy for it
        if (isProxiableObject(value)) {
          // Create a parent reference for the nested object
          const nestedParent = {
            tracker: changeTracker,
            prop: String(prop),
          }

          // Create a proxy for the nested object
          const { proxy: nestedProxy } = memoizedCreateChangeProxy(
            value,
            nestedParent,
          )

          // Cache the proxy
          proxyCache.set(value, nestedProxy)

          return nestedProxy
        }

        return value
      },

      set(_sobj, prop, value) {
        const currentValue = changeTracker.copy_[prop as keyof T]

        // Only track the change if the value is actually different
        if (!deepEquals(currentValue, value)) {
          // Check if the new value is equal to the original value
          // Important: Use the originalObject to get the true original value
          const originalValue = changeTracker.originalObject[prop as keyof T]
          const isRevertToOriginal = deepEquals(value, originalValue)

          if (isRevertToOriginal) {
            // If the value is reverted to its original state, remove it from changes
            delete changeTracker.assigned_[prop.toString()]

            // Make sure the copy is updated with the original value
            changeTracker.copy_[prop as keyof T] = deepClone(originalValue)

            // Check if all properties in this object have been reverted
            const allReverted = checkIfReverted(changeTracker)

            if (allReverted) {
              // If all have been reverted, clear tracking
              changeTracker.modified = false
              changeTracker.assigned_ = {}

              // If we're a nested object, check if the parent needs updating
              if (parent) {
                checkParentStatus(parent.tracker)
              }
            } else {
              // Some properties are still changed
              changeTracker.modified = true
            }
          } else {
            // Set the value on the copy
            changeTracker.copy_[prop as keyof T] = value

            // Track that this property was assigned - store using the actual property (symbol or string)
            changeTracker.assigned_[prop.toString()] = true

            // Mark this object and its ancestors as modified
            markChanged(changeTracker)
          }
        }

        return true
      },

      defineProperty(ptarget, prop, descriptor) {
        // Forward the defineProperty to the target to maintain Proxy invariants
        // This allows Object.seal() and Object.freeze() to work on the proxy
        const result = Reflect.defineProperty(ptarget, prop, descriptor)
        if (result && `value` in descriptor) {
          changeTracker.copy_[prop as keyof T] = deepClone(descriptor.value)
          changeTracker.assigned_[prop.toString()] = true
          markChanged(changeTracker)
        }
        return result
      },

      getOwnPropertyDescriptor(ptarget, prop) {
        // Forward to target to maintain Proxy invariants for seal/freeze
        return Reflect.getOwnPropertyDescriptor(ptarget, prop)
      },

      preventExtensions(ptarget) {
        // Forward to target to allow Object.seal() and Object.preventExtensions()
        return Reflect.preventExtensions(ptarget)
      },

      isExtensible(ptarget) {
        // Forward to target to maintain consistency
        return Reflect.isExtensible(ptarget)
      },

      deleteProperty(dobj, prop) {
        const stringProp = typeof prop === `symbol` ? prop.toString() : prop

        if (stringProp in dobj) {
          // Check if the property exists in the original object
          const hadPropertyInOriginal =
            stringProp in changeTracker.originalObject

          // Forward the delete to the target using Reflect
          // This respects Object.seal/preventExtensions constraints
          const result = Reflect.deleteProperty(dobj, prop)

          if (result) {
            // If the property didn't exist in the original object, removing it
            // should revert to the original state
            if (!hadPropertyInOriginal) {
              delete changeTracker.assigned_[stringProp]

              // If this is the last change and we're not a nested object,
              // mark the object as unmodified
              if (
                Object.keys(changeTracker.assigned_).length === 0 &&
                Object.getOwnPropertySymbols(changeTracker.assigned_).length ===
                  0
              ) {
                changeTracker.modified = false
              } else {
                // We still have changes, keep as modified
                changeTracker.modified = true
              }
            } else {
              // Mark this property as deleted
              changeTracker.assigned_[stringProp] = false
              markChanged(changeTracker)
            }
          }

          return result
        }

        return true
      },
    })

    // Cache the proxy
    proxyCache.set(obj, proxy)
    draftCopies.set(proxy, changeTracker.copy_)

    return proxy
  }

  // Create a proxy for the target object
  // Use the unfrozen copy_ as the proxy target to avoid Proxy invariant violations
  // when the original target is frozen (e.g., from Immer)
  const proxy = createObjectProxy(changeTracker.copy_ as unknown as T)

  // Return the proxy and a function to get the changes
  return {
    proxy,
    getChanges: () => {
      // First, check if the object is still considered modified
      if (!changeTracker.modified) {
        return {}
      }

      // If we have a copy, return it directly
      // Check if valueObj is actually an object
      if (
        typeof changeTracker.copy_ !== `object` ||
        Array.isArray(changeTracker.copy_)
      ) {
        return changeTracker.copy_
      }

      if (Object.keys(changeTracker.assigned_).length === 0) {
        return changeTracker.copy_
      }

      const result: Record<string, any | undefined> = {}
      const mayHaveChangedAliases = Object.keys(changeTracker.assigned_).some(
        (key) => typeof changeTracker.copy_[key] === `object`,
      )

      // Iterate through keys in keyObj
      for (const key in changeTracker.copy_) {
        const value: unknown = changeTracker.copy_[key]
        const original: unknown = changeTracker.originalObject[key]
        // Include sibling aliases changed through another route. An untouched
        // link back to this row does not add a field to its sparse change set.
        if (
          (changeTracker.assigned_[key] === true ||
            (mayHaveChangedAliases &&
              changeTracker.copy_[key] !== changeTracker.copy_ &&
              !deepEquals(
                value instanceof Set ? Array.from(value) : value,
                original instanceof Set ? Array.from(original) : original,
              ))) &&
          key in changeTracker.copy_
        ) {
          result[key] = changeTracker.copy_[key]
        }
      }

      return result as unknown as Record<string | symbol, unknown>
    },
  }
}

/**
 * Creates proxies for an array of objects and tracks changes to each
 *
 * @param targets Array of objects to proxy
 * @returns An object containing the array of proxies and a function to get all changes
 */
export function createArrayChangeProxy<T extends object>(
  targets: Array<T>,
): {
  proxies: Array<T>
  getChanges: () => Array<Record<string | symbol, unknown>>
} {
  const proxiesWithChanges = targets.map((target) => createChangeProxy(target))

  return {
    proxies: proxiesWithChanges.map((p) => p.proxy),
    getChanges: () => proxiesWithChanges.map((p) => p.getChanges()),
  }
}

/**
 * Creates a proxy for an object, passes it to a callback function,
 * and returns the changes made by the callback
 *
 * @param target The object to proxy
 * @param callback Function that receives the proxy and can make changes to it
 * @returns The changes made to the object
 */
export function withChangeTracking<T extends object>(
  target: T,
  callback: (proxy: T) => void,
): Record<string | symbol, unknown> {
  const { proxy, getChanges } = createChangeProxy(target)

  callback(proxy)

  return deepClone(getChanges())
}

/**
 * Creates proxies for an array of objects, passes them to a callback function,
 * and returns the changes made by the callback for each object
 *
 * @param targets Array of objects to proxy
 * @param callback Function that receives the proxies and can make changes to them
 * @returns Array of changes made to each object
 */
export function withArrayChangeTracking<T extends object>(
  targets: Array<T>,
  callback: (proxies: Array<T>) => void,
): Array<Record<string | symbol, unknown>> {
  const { proxies, getChanges } = createArrayChangeProxy(targets)

  callback(proxies)

  return deepClone(getChanges())
}
