export const isObject = <T>(item: T): item is NonNullable<T> => {
  return item != null && typeof item === 'object' && !Array.isArray(item);
};

const forbiddenProperties: string[] = ['__proto__', 'constructor', 'prototype'];

const isSafeKey = (obj: object, key: string): boolean =>
  Object.prototype.hasOwnProperty.call(obj, key) && !forbiddenProperties.includes(key);

const mergeValue = <T extends Record<PropertyKey, unknown>, K extends keyof T>(
  target: T,
  key: K,
  value?: T[K],
): void => {
  if (isObject(value)) {
    if (!target[key]) {
      target[key] = {} as T[K];
    }
    mergeDeep(target[key], value);
  } else if (value !== undefined) {
    target[key] = value;
  }
};

/**
 * Merges partial objects into the target object.
 * This modifies the target object and returns it.
 *
 * @param target   The object into which the changes are merged
 * @param sources  A list of patches to apply
 * @returns  the target
 */
export const mergeDeep = <T>(target: T, ...sources: Partial<T>[]): T => {
  for (const source of sources) {
    if (!isObject(target) || !isObject(source)) {
      continue;
    }

    for (const key in source) {
      if (!isSafeKey(source, key)) {
        continue;
      }
      mergeValue(target, key, source[key]);
    }
  }

  return target;
};
