from typing import Callable
from typing import Tuple
from typing import Any


def recurse_eval(path: str, data: dict, fn: Callable) -> Tuple[str, Any]:
    """
    Given a `path` such as `a.b.0.split(' ')` this function traverses
    the `data` dictionary using the path, stopping whenever a key cannot be
    found in the `data`. `fn` is then applied to the extracted data and the
    result is returned along with the part of the path which was traversed.

    In the following example, `a.b.0` is identified as the path to return
    since `.split()` is not an item in `data`.

    >>> recurse_eval(
    ...     path="a.b.0.split(' ')",
    ...     data={"a": {"b": [{"$eval": "'hey ' * 2"}]}},
    ...     fn=lambda node, _ : eval(node["$eval"]) if "$eval" in node else node
    ... )
    ('a.b.0', 'hey hey ')

    Parameters
    ----------
    path
        The path to fetch from in the data
    data
        Dictionary which should be traversed using the path
    fn
        function to call with the fetched data as parameter
    Returns
    -------
    Tuple[str, Any]
        The path and the value after applying the `fn`
    """
    tmp = data
    current_path = []
    path = path.replace("[", ".[")
    for key in path.split("."):
        original_key = key
        if "[" in key:
            key = key.replace("[", "").replace("]", "").replace('"', "")
        try:
            tmp = tmp[key]
            current_path.append(original_key)
        except TypeError:
            try:
                tmp = tmp[int(key)]
                current_path.append(original_key)
            except ValueError:
                break
        except:
            break
    return ".".join(current_path).replace(".[", "["), fn(tmp, data)