export interface ChangeEventDetail {
  path: string
  value: unknown
  oldValue: unknown
}

// target -> Proxy
const cachedRaw = new WeakMap<object, object>()
// Proxy -> target
const cachedProxies = new WeakMap<object, object>()

export function createReactive<T extends object>(
  target: T,
  onChange: (detail: ChangeEventDetail) => void,
  path = ''
): T {
  if (cachedRaw.has(target)) {
    // return target
    return cachedRaw.get(target) as T // Return existing proxy
  }
  if (cachedProxies.has(target)) {
    return target
  }

  const handler: ProxyHandler<T> = {
    get(target, prop, receiver) {
      // Retrieve the original value. If 'prop' is a getter, 'receiver' (the proxy) is used as 'this'.
      const value = Reflect.get(target, prop, receiver)
      // If the retrieved value is a function, bind it to the original 'target' object.
      // This ensures that methods (e.g., Set.prototype.add, Map.prototype.set) are called
      // with the correct 'this' context, which is the original unwrapped object.
      // Special handling for Set and Map mutating methods
      if (target instanceof Set) {
        if (['add', 'delete', 'clear'].includes(String(prop))) {
          return (...args: unknown[]) => {
            const oldValue = new Set(target) // shallow copy before mutation
            // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
            const result = (value as Function).apply(target, args)
            onChange({
              path,
              value: target,
              oldValue,
            })
            return result
          }
        }
      }
      if (target instanceof Map) {
        if (['set', 'delete', 'clear'].includes(String(prop))) {
          return (...args: unknown[]) => {
            const oldValue = new Map(target) // shallow copy before mutation
            // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
            const result = (value as Function).apply(target, args)
            onChange({
              path,
              value: target,
              oldValue,
            })
            return result
          }
        }
      }
      if (typeof value === 'function' && !Array.isArray(target)) {
        // If the value is a function, bind it to the original target object.
        return value.bind(target)
      }
      // If the value is an object (and not null, and not a function due to the check above),
      // then recursively create a reactive proxy for it.
      if (typeof value === 'object' && value !== null) {
        const finalPath = path ? `${path}.${String(prop)}` : String(prop)
        return createReactive(value, onChange, finalPath)
      }
      // For primitive values or null, return the value directly.
      return value
    },
    set(target, prop, value, receiver) {
      const oldValue = Reflect.get(target, prop, receiver)
      const success = Reflect.set(target, prop, value) // do not set the receiver here
      // The receiver is the proxy itself and if we set the value on the proxy, it will
      // create an infinite loop.
      if (success && oldValue !== value) {
        // Check if the old value was proxied and remove it from the cache
        if (typeof oldValue === 'object' && oldValue !== null) {
          const oldProxy = cachedRaw.get(oldValue)
          if (oldProxy) {
            cachedRaw.delete(oldValue)
            cachedProxies.delete(oldProxy)
          }
        }
        onChange({
          path: path ? `${path}.${String(prop)}` : String(prop),
          value,
          oldValue,
        })
      }
      return success
    },
    deleteProperty(target, prop) {
      const oldValue = Reflect.get(target, prop)
      const success = Reflect.deleteProperty(target, prop)
      if (success) {
        // Check if the old value was proxied and remove it from the cache
        if (typeof oldValue === 'object' && oldValue !== null) {
          const oldProxy = cachedRaw.get(oldValue)
          if (oldProxy) {
            cachedRaw.delete(oldValue)
            cachedProxies.delete(oldProxy)
          }
        }
        onChange({
          path: path ? `${path}.${String(prop)}` : String(prop),
          value: undefined,
          oldValue,
        })
      }
      return success
    },
  }
  const proxy = new Proxy(target, handler)
  cachedRaw.set(target, proxy)
  cachedProxies.set(proxy, target)
  return proxy
}

/**
 * Removes the proxy and cleans up the cached data.
 *
 * @param reactiveObject The reactive object to unhook.
 * @returns true if the object was reactive and was unhooked, false otherwise.
 */
export function unhookReactive(reactiveObject: object): boolean {
  const raw = cachedProxies.get(reactiveObject)
  if (!raw) {
    return false // Not a reactive object
  }
  cachedRaw.delete(raw)
  cachedProxies.delete(reactiveObject)
  return true
}
