from typing import Dict
from typing import Any
from typing import List
from functools import reduce


def traverse_dict(obj: Dict[Any, Any], properties: List[str]) -> Any:
    """
    Traverse a dictionary for a given list of properties.

    This is useful for traversing a deeply nested dictionary.
    Instead of recursion, we are using reduce to update the `dict`.
    Missing properties will lead to KeyErrors.

    .. ipython:: python
        :okexcept:

        from dialogy.utils import traverse_dict

        input_ = {
            "planets": {
                "mars": [{
                    "name": "",
                    "languages": [{
                        "beep": {"speakers": 11},
                    }, {
                        "bop": {"speakers": 30},
                    }]
                }]
            }
        }
        traverse_dict(input_, ["planets", "mars", 0 , "languages", 1, "bop"])

        # element with index 3 doesn't exist!
        traverse_dict(input_, ["planets", "mars", 0 , "languages", 3, "bop"])

    :param obj: The `dict` to traverse.
    :type obj: Dict[Any, Any]
    :param properties: List of properties to be parsed as a path to be navigated in the `dict`.
    :type properties: List[int]
    :return: A value within a deeply nested dict.
    :rtype: Any
    :raises KeyError: Missing property in the dictionary.
    :raises TypeError: Properties don't describe a path due to possible type error.
    """
    try:
        return reduce(lambda o, k: o[k], properties, obj)
    except KeyError as key_error:
        raise KeyError(
            f"Missing property {key_error} in {obj}. Check the types. Failed for path {properties}"
        ) from key_error
    except TypeError as type_error:
        raise TypeError(
            f"The properties aren't describing path within a dictionary. | {type_error}. Failed for path {properties}"
        ) from type_error