import copy
from typing import Callable
from typing import MutableSequence
from typing import Mapping


def apply_modifier(initial_dict, modifier):
    """
    Returns a copy of an initial dictionary with modified values.

    :param initial_dict: any dictionary to create a copy from and modify
    :param modifier: either a callable that modifies the dictionary or a
    dictionary that contains other modifiers under corresponding keys.
    :return: a copy of the dictionary
    :rtype: dict

    Examples:
      >>> apply_modifier({'a': 7}, lambda _: {})
      {}

      >>> apply_modifier({'b': 7}, {'b': float})
      {'b': 7.0}

      >>> apply_modifier({'c': {'x': 1}, 'd': 2}, {'c': {'x': float}})
      {'c': {'x': 1.0}, 'd': 2}

      >>> apply_modifier({'c': {'x': 1}}, {'d': float})
      {'c': {'x': 1}}

      >>> apply_modifier({'c': {'x': 1}}, {'c': lambda _: 'test'})
      {'c': 'test'}

      >>> apply_modifier({'c': [1, 2, 3]}, {'c': [float]})
      {'c': [1.0, 2.0, 3.0]}

      >>> apply_modifier({'c': []}, {'c': [float]})
      {'c': []}

      >>> apply_modifier(
      ...   {'c': [{'x': [1]}, {'y': 4}]}, {'c': [{'x': [float]}]})
      {'c': [{'x': [1.0]}, {'y': 4}]}

      >>> apply_modifier(
      ...   {'c': [{'x': [1], 'y': 2}]}, {'c': [{'x': [float], 'y': float}]})
      {'c': [{'x': [1.0], 'y': 2.0}]}

    """
    initial_copy = copy.deepcopy(initial_dict)

    if isinstance(modifier, Callable):
        return modifier(initial_copy)

    if isinstance(modifier, MutableSequence):
        if not modifier:
            raise ValueError('list modifier is empty')

        if not isinstance(initial_copy, MutableSequence):
            raise ValueError(
                f'cannot apply list modifier to {type(initial_copy)} type'
            )
        sub_modifier = modifier[0]

        return [apply_modifier(v, sub_modifier) for v in initial_copy]

    if not isinstance(modifier, Mapping):
        raise ValueError('modifier is not dictionary nor callable')

    for key, sub_modifier in modifier.items():
        if key not in initial_copy:
            continue

        initial_copy[key] = apply_modifier(initial_copy[key], sub_modifier)

    return initial_copy