export type DeepReadonly<T> =
  T extends (...args: never[]) => unknown ? T :
  T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T;

const isPlainRecordPrototype = (prototype: object | null): boolean => {
  return prototype === null || prototype === Object.prototype;
};

const objectPrototypeKeys = Reflect.ownKeys(Object.prototype)
  .map(String)
  .sort()
  .join('\n');
const nativeObjectSource = Function.prototype.toString.call(Object);
const isCloneablePlainRecordPrototype = (prototype: object | null): boolean => {
  if (isPlainRecordPrototype(prototype)) return true;
  if (prototype === null || Object.getPrototypeOf(prototype) !== null) return false;
  const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor')?.value;
  return typeof constructor === 'function' &&
    Function.prototype.toString.call(constructor) === nativeObjectSource &&
    Reflect.ownKeys(prototype).map(String).sort().join('\n') === objectPrototypeKeys;
};

const assertPlainData = (value: unknown, seen = new WeakSet<object>()): void => {
  if (value === null || typeof value !== 'object') return;
  if (seen.has(value)) return;
  seen.add(value);
  if (!Array.isArray(value) && !isCloneablePlainRecordPrototype(Object.getPrototypeOf(value))) {
    throw new TypeError('deepFreeze accepts only arrays and plain records');
  }
  for (const key of Reflect.ownKeys(value)) {
    if (Array.isArray(value) && key === 'length') continue;
    if (typeof key === 'symbol') {
      throw new TypeError('cloneAndDeepFreeze does not accept symbol properties');
    }
    const descriptor = Object.getOwnPropertyDescriptor(value, key);
    if (!descriptor || !('value' in descriptor)) {
      throw new TypeError('deepFreeze does not accept accessor properties');
    }
    if (!descriptor.enumerable) {
      throw new TypeError('cloneAndDeepFreeze does not accept non-enumerable properties');
    }
    assertPlainData(descriptor.value, seen);
  }
};

const clonePlainData = <T>(value: T, seen = new Map<object, unknown>()): T => {
  if (value === null || typeof value !== 'object') return value;
  const source = value as object;
  const existing = seen.get(source);
  if (existing !== undefined) return existing as T;
  const target: unknown[] | Record<string, unknown> = Array.isArray(source) ? [] : {};
  seen.set(source, target);
  for (const key of Reflect.ownKeys(source)) {
    if (Array.isArray(source) && key === 'length') continue;
    const descriptor = Object.getOwnPropertyDescriptor(source, key);
    if (!descriptor || typeof key !== 'string' || !('value' in descriptor)) {
      throw new TypeError('cloneAndDeepFreeze accepts only canonical data properties');
    }
    Object.defineProperty(target, key, {
      value: clonePlainData(descriptor.value, seen),
      enumerable: true,
      writable: true,
      configurable: true,
    });
  }
  return target as T;
};

/**
 * Deep-freezes same-realm arrays and plain records. Other mutable built-ins,
 * accessors, and foreign/custom prototypes are rejected rather than being
 * misleadingly shallow-frozen. cloneAndDeepFreeze safely normalizes
 * cross-realm plain data through structuredClone first.
 */
export function deepFreeze<T>(value: T, seen = new WeakSet<object>()): DeepReadonly<T> {
  if (value !== null && typeof value === 'object') {
    const object = value as object;
    if (seen.has(object)) return value as DeepReadonly<T>;
    seen.add(object);
    const plainRecord = isPlainRecordPrototype(Object.getPrototypeOf(object));
    if (!Array.isArray(object) && !plainRecord) {
      throw new TypeError('deepFreeze accepts only arrays and plain records');
    }
    for (const key of Reflect.ownKeys(object)) {
      if (Array.isArray(object) && key === 'length') continue;
      const descriptor = Object.getOwnPropertyDescriptor(object, key);
      if (!descriptor || !('value' in descriptor)) {
        throw new TypeError('deepFreeze does not accept accessor properties');
      }
      deepFreeze(descriptor.value, seen);
    }
    Object.freeze(object);
  }
  return value as DeepReadonly<T>;
}

/** Breaks caller aliases before applying the plain-data deep-freeze contract. */
export function cloneAndDeepFreeze<T>(value: T): DeepReadonly<T> {
  assertPlainData(value);
  return deepFreeze(clonePlainData(value));
}
