type PlainObject = Record<string, unknown>

export function deepMerge<T extends PlainObject>(baseValue: T, overrideValue: PlainObject): T {
  if (!isPlainObject(baseValue)) {
    return cloneValue(overrideValue) as T
  }

  const result = cloneValue(baseValue) as PlainObject

  if (!isPlainObject(overrideValue)) {
    return result as T
  }

  for (const [key, value] of Object.entries(overrideValue)) {
    if (isPlainObject(value) && isPlainObject(result[key])) {
      result[key] = deepMerge(result[key] as PlainObject, value)
      continue
    }

    result[key] = cloneValue(value)
  }

  return result as T
}

function cloneValue<T>(value: T): T {
  if (Array.isArray(value)) {
    return value.map((item) => cloneValue(item)) as T
  }

  if (isPlainObject(value)) {
    const output: PlainObject = {}
    for (const [key, item] of Object.entries(value)) {
      output[key] = cloneValue(item)
    }
    return output as T
  }

  return value
}

export function isPlainObject(value: unknown): value is PlainObject {
  return Object.prototype.toString.call(value) === '[object Object]'
}
