"""Create progressions through a sequentially built graph."""

from typing import Any, Callable, Dict, List, Optional, Union

from .selectors import first, last, randomness  # noqa: F401


class DFSProgressor:
    """Progressor for Depth First Search."""

    def __init__(self, graph: Optional[Dict[Any, Any]] = None) -> None:
        """Initialize progessor.

        Args:
            graph (dict, optional): Graph to build progression from.
                    The more robust the graph, the faster the solution (on average).

        """
        self._graph = graph or {}

    def create_progression(  # noqa: C901
        self,
        starter: Any,
        step_generator: Callable,
        progression_length: int = 10,
        post_generation_filters: Optional[List[Callable]] = None,
        step_selection: Union[Callable, str] = "first",
    ) -> List[Any]:
        """Progress through the graph.

        Args:
            starter (Any): Starting value to create graph.
            step_generator (Callable): Function/method to generate the options at each step.
            progression_length (int, optional): Length of progression. Defaults to 10.
            post_generation_filters (list, optional): List of functions to filter out values
                    return by `step_generator`. Defaults to None.
            step_selection (Callable | str, optional): Function/method to determine which element
                    to select from the list of options generated by `step_generator`.
                    Defaults to "first".

        Returns:
            list: List of values from the DFS.

        """
        if progression_length < 1:
            raise ValueError("progression_length must be greater than 1.")

        # Setup base values
        post_generation_filters = post_generation_filters or []

        if isinstance(step_selection, str):
            try:
                step_selection = eval(step_selection)
            except (NameError, ValueError):
                raise ValueError(f"{step_selection} isn't a valid function name.")

        def _progress(progression: List[str]) -> Union[List[Any], Callable]:
            """Help function to determine next step of the progression."""
            # Get current step
            current = progression[-1]

            # Check if already a visited node
            if current in self._graph:
                options = self._graph[current]
            else:
                options = step_generator(current)
                for _filter in post_generation_filters:  # type: ignore
                    options = _filter(current, options)
                self._graph[current] = options

            # Remove options we've already explored
            non_visited_options = [option for option in options if option not in self._graph.keys()]
            if non_visited_options == []:
                # If no options and starting node, end
                if len(progression) == 1:
                    return []

                # If no options, backtrack
                return _progress(progression[:-1])

            # Select next step
            step = step_selection(non_visited_options)  # type: ignore
            progression.append(step)

            # If progression his length, end
            if len(progression) == progression_length:
                return progression

            # Recurse
            return _progress(progression)

        # Starting call
        return _progress([starter])  # type: ignore

    @property
    def graph(self) -> Dict[Any, Any]:
        """Get internal graph."""
        return self._graph
