import itertools


def random_product_without_replacement(*args, **kwargs):
    """
    Pulls random products of multiple lists without replacement.
    Works well for sparse calls that are many times less than
    the total number of combinations.
    """

    size = kwargs.get('size', 1)
    random_state = kwargs['random_state']
    used = set()
    pools = list(map(tuple, args))
    
    # Check that size doesn't exceed the number of available draws
    max_product = 1
    for pool in pools:
        max_product *= len(pool)
        
    if size > max_product:
        print('Warning: size exceeds max product!', ' Max', max_product)
        return None
    
    # If size on the same order of magnitude as the number of 
    # products just build a list of the options and pull from that
    elif size >= (max_product / 10):
        products = list(itertools.product(*pools))
        random_state.shuffle(products)
        return products[:size]
    
    # if size is much less than the number of products generate rv's on the fly
    else:
        rvs = []
        for _ in range(size):
            # rv = tuple(random.choice(pool) for pool in pools)
            rv = tuple(random_state.choice(pool) for pool in pools)
            while rv in used:
                # rv = tuple(random.choice(pool) for pool in pools)
                rv = tuple(random_state.choice(pool) for pool in pools)

            rvs.append(rv)
            used.add(rv)

    return rvs