/**
 * State management with observer pattern and diffing
 */

export type StatePath = string; // e.g., "user.name", "items.0.title"

/**
 * Represents a change in state with full path information
 */
export interface StateChange {
  paths: StatePath[];
  newValues: Record<StatePath, any>;
}

/**
 * Options for state observer
 */
export interface StateObserverOptions {
  onChange: (change: StateChange) => void;
  pathPrefix?: string; // For nested observers
}

/**
 * Deep clone an object, handling circular references
 */
function deepClone<T>(obj: T): T {
  // Handle primitives and null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // Use a WeakMap to track visited objects and handle circular references
  const visited = new WeakMap();

  function cloneInternal(value: any): any {
    // Handle primitives and null
    if (value === null || typeof value !== 'object') {
      return value;
    }

    // Check if we've already cloned this object (circular reference)
    if (visited.has(value)) {
      return visited.get(value);
    }

    // Handle Date
    if (value instanceof Date) {
      return new Date(value.getTime());
    }

    // Handle RegExp
    if (value instanceof RegExp) {
      return new RegExp(value.source, value.flags);
    }

    // Handle Map
    if (value instanceof Map) {
      const mapClone = new Map();
      visited.set(value, mapClone);
      for (const [k, v] of value.entries()) {
        mapClone.set(cloneInternal(k), cloneInternal(v));
      }
      return mapClone;
    }

    // Handle Set
    if (value instanceof Set) {
      const setClone = new Set();
      visited.set(value, setClone);
      for (const item of value.values()) {
        setClone.add(cloneInternal(item));
      }
      return setClone;
    }

    // Handle WeakMap/WeakSet - cannot be cloned, return as-is
    if (value instanceof WeakMap || value instanceof WeakSet) {
      return value;
    }

    // Handle Array
    if (Array.isArray(value)) {
      const arrClone: any[] = [];
      visited.set(value, arrClone);
      for (let i = 0; i < value.length; i++) {
        arrClone[i] = cloneInternal(value[i]);
      }
      return arrClone;
    }

    // Handle plain objects
    const objClone: any = {};
    visited.set(value, objClone);

    // Clone string keys
    for (const key in value) {
      if (value.hasOwnProperty(key)) {
        objClone[key] = cloneInternal(value[key]);
      }
    }

    // Clone Symbol keys (not enumerable by for...in)
    const symbolKeys = Object.getOwnPropertySymbols(value);
    for (const sym of symbolKeys) {
      objClone[sym] = cloneInternal(value[sym]);
    }

    return objClone;
  }

  return cloneInternal(obj);
}

/**
 * Compare two values and detect changes with full paths
 */
function diffState(
  oldState: any,
  newState: any,
  basePath: string = ""
): StateChange {
  const paths: StatePath[] = [];
  const newValues: Record<StatePath, any> = {};

  function diff(oldVal: any, newVal: any, path: string) {
    // Handle null/undefined
    if (oldVal === newVal) return;

    // Different types or primitives
    if (
      typeof oldVal !== "object" ||
      typeof newVal !== "object" ||
      oldVal === null ||
      newVal === null
    ) {
      if (oldVal !== newVal) {
        paths.push(path);
        newValues[path] = newVal;
      }
      return;
    }

    // Arrays
    if (Array.isArray(oldVal) || Array.isArray(newVal)) {
      if (
        !Array.isArray(oldVal) ||
        !Array.isArray(newVal) ||
        oldVal.length !== newVal.length
      ) {
        paths.push(path);
        newValues[path] = newVal;
        return;
      }

      for (let i = 0; i < newVal.length; i++) {
        const itemPath = path ? `${path}.${i}` : `${i}`;
        diff(oldVal[i], newVal[i], itemPath);
      }
      return;
    }

    // Objects
    const oldKeys = new Set(Object.keys(oldVal));
    const newKeys = new Set(Object.keys(newVal));

    // Check for added or changed keys
    for (const key of newKeys) {
      const propPath = path ? `${path}.${key}` : key;
      if (!oldKeys.has(key)) {
        // New property
        paths.push(propPath);
        newValues[propPath] = newVal[key];
      } else {
        // Existing property, recurse
        diff(oldVal[key], newVal[key], propPath);
      }
    }

    // Check for removed keys
    for (const key of oldKeys) {
      if (!newKeys.has(key)) {
        const propPath = path ? `${path}.${key}` : key;
        paths.push(propPath);
        newValues[propPath] = undefined;
      }
    }
  }

  diff(oldState, newState, basePath);
  return { paths, newValues };
}

/**
 * Create an observable state object that tracks changes
 */
export function createObservableState<T extends object>(
  initialState: T,
  options?: StateObserverOptions
): T {
  // Use default options if not provided
  const opts: StateObserverOptions = options || { onChange: () => {} };

  // Handle null/undefined by using an empty object
  // This allows modules to start with null state
  if (initialState === null || initialState === undefined) {
    initialState = {} as T;
  }

  // Detect and reject primitive wrapper objects (Number, String, Boolean)
  // These cannot be properly proxied due to internal slots
  if (
    initialState instanceof Number ||
    initialState instanceof String ||
    initialState instanceof Boolean
  ) {
    throw new TypeError(
      "Cannot create observable state from primitive wrapper objects (Number, String, Boolean). " +
      "Use plain primitives or regular objects instead."
    );
  }

  // Clone the initial state to ensure each observable has its own copy
  // This prevents multiple modules from sharing the same underlying state object
  initialState = deepClone(initialState);

  // Keep a snapshot of the last known state
  let lastSnapshot = deepClone(initialState);
  const pathPrefix = opts.pathPrefix || "";

  // Track if we're in a batch update
  let batchDepth = 0;
  let pendingChange: StateChange | null = null;

  function notifyChange() {
    if (batchDepth > 0) return;

    // Compare current state with last snapshot
    const change = diffState(lastSnapshot, state, pathPrefix);

    if (change.paths.length > 0) {
      // Update snapshot
      lastSnapshot = deepClone(state);

      // Merge with pending changes if any
      if (pendingChange) {
        change.paths.push(...pendingChange.paths);
        Object.assign(change.newValues, pendingChange.newValues);
        pendingChange = null;
      }

      // Notify
      opts.onChange(change);
    }
  }

  // Track if we have a pending microtask notification
  let notificationPending = false;

  function scheduleBatch() {
    if (batchDepth === 0) {
      // If not in a batch, schedule notification in next microtask to coalesce rapid changes
      if (!notificationPending) {
        notificationPending = true;
        queueMicrotask(() => {
          notificationPending = false;
          if (batchDepth === 0) {
            notifyChange();
          }
        });
      }
    }
  }

  // Cache proxies to handle circular references
  const proxyCache = new WeakMap<any, any>();

  function createProxy(target: any, basePath: string): any {
    // Return cached proxy if it exists (handles circular references)
    if (proxyCache.has(target)) {
      return proxyCache.get(target);
    }

    const proxy = new Proxy(target, {
      get(obj, prop) {
        const value = obj[prop];

        // Expose batch control methods
        if (prop === "__beginBatch") {
          return () => {
            batchDepth++;
          };
        }
        if (prop === "__endBatch") {
          return () => {
            batchDepth--;
            if (batchDepth === 0) {
              notifyChange();
            }
          };
        }
        if (prop === "__getSnapshot") {
          return () => deepClone(obj);
        }

        // Return proxied nested objects/arrays, but NOT special types
        if (value && typeof value === "object") {
          // Check for special object types that should not be proxied
          if (
            value instanceof Date ||
            value instanceof RegExp ||
            value instanceof Map ||
            value instanceof Set ||
            value instanceof WeakMap ||
            value instanceof WeakSet
          ) {
            return value;
          }

          // Proxy regular objects and arrays
          return createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
        }

        return value;
      },

      set(obj, prop, value) {
        const oldValue = obj[prop];

        // Set the new value
        obj[prop] = value;

        if (oldValue !== value) {
          scheduleBatch();
        }

        return true;
      },

      deleteProperty(obj, prop) {
        if (prop in obj) {
          delete obj[prop];
          scheduleBatch();
        }
        return true;
      },
    });

    // Cache the proxy before returning
    proxyCache.set(target, proxy);
    return proxy;
  }

  const state = createProxy(initialState, pathPrefix);
  return state as T;
}

/**
 * Helper to batch multiple state updates
 */
export function batchStateUpdates<T>(state: T, fn: () => void): void {
  const s = state as any;
  if (s.__beginBatch && s.__endBatch) {
    s.__beginBatch();
    try {
      fn();
    } finally {
      s.__endBatch();
    }
  } else {
    fn();
  }
}

/**
 * Get a snapshot of the current state
 */
export function getStateSnapshot<T>(state: T): T {
  const s = state as any;
  if (s.__getSnapshot) {
    return s.__getSnapshot();
  }
  return deepClone(state);
}
