package com.appsmith.server.solutions;

import com.appsmith.external.helpers.MustacheHelper;
import com.appsmith.external.models.Property;
import com.appsmith.server.domains.ActionDependencyEdge;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.DslActionDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.services.NewActionService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.DirectedAcyclicGraph;
import org.jgrapht.traverse.BreadthFirstIterator;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.appsmith.external.helpers.MustacheHelper.getPossibleParents;
import static com.appsmith.external.helpers.MustacheHelper.getWordsFromMustache;
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;

@Slf4j
@Component
@RequiredArgsConstructor
public class PageLoadActionsUtil {

    private final NewActionService newActionService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * The following regex finds the immediate parent of an entity path.
     * e.g. :
     * Dropdown1.options[1].value -> Dropdown1.options[1]
     * Dropdown1.options[1] -> Dropdown1.options
     * Dropdown1.options -> Dropdown1
     */
    private final String IMMEDIATE_PARENT_REGEX = "^(.*)(\\..*|\\[.*\\])$";
    private final Pattern parentPattern = Pattern.compile(IMMEDIATE_PARENT_REGEX);

    // TODO : This should contain all the static global variables present on the page like `appsmith`, etc.
    // TODO : Add all the global variables exposed on the client side.
    private final Set<String> APPSMITH_GLOBAL_VARIABLES = Set.of();


    /**
     * This function computes the sequenced on page load actions.
     *
     * !!!WARNING!!! : This function edits the parameters edges, actionsUsedInDSL and flatPageLoadActions
     * and the same are used by the caller function for further processing.
     *
     * @param pageId : Argument used for fetching actions in this page
     * @param widgetNames : Set of widget names which SHOULD have been populated before calling this function.
     * @param edges : Set where this function adds all the relationships (dependencies) between actions
     * @param widgetDynamicBindingsMap : A map of widget path and the set of dynamic binding words in the mustache at the
     *                                 path in the widget (populated by the function `extractAllWidgetNamesAndDynamicBindingsFromDSL`
     *
     *     Example : If Table1's field tableData contains a mustache : {{Api1.data}}, the entry in the map would look like :
     *                                 Map.entry("Table1.tableData", Set.of("Api1.data"))
     *
     * @param flatPageLoadActions : A flat list of on page load actions (Not in the sequence in which these actions
     *                            would be called on page load)
     * @param actionsUsedInDSL : Set where this function adds all the actions directly used in the DSL
     *
     * @return : Returns page load actions which is a list of sets of actions. Inside a set, all actions can be executed
     * in parallel. But one set of actions MUST finish execution before the next set of actions can be executed
     * in the list.
     */
    public Mono<List<Set<DslActionDTO>>> findAllOnLoadActions(String pageId,
                                                              Set<String> widgetNames,
                                                              Set<ActionDependencyEdge> edges,
                                                              Map<String, Set<String>> widgetDynamicBindingsMap,
                                                              List<ActionDTO> flatPageLoadActions,
                                                              Set<String> actionsUsedInDSL) {

        Set<String> onPageLoadActionSet = new HashSet<>();
        Set<String> explicitUserSetOnLoadActions = new HashSet<>();
        Set<String> bindingsFromActions = new HashSet<>();

        // Function `extractAndSetActionBindingsInGraphEdges` updates this set to keep a track of all the actions which
        // have been discovered while walking the actions to ensure that we don't end up in a recursive infinite loop
        // in case of a cyclical relationship between actions (and not specific paths) and helps us exit at the appropriate
        // junction.
        // e.g : Consider the following relationships :
        // Api1.actionConfiguration.body <- Api2.data.users[0].name
        // Api2.actionConfiguration.url <- Api1.actionConfiguration.url
        // In the above case, the two actions depend on each other without there being a real cyclical dependency.
        Set<String> actionsFoundDuringWalk = new HashSet<>();

        Set<String> bindingsInWidgets = new HashSet<>();

        // Create a global set of bindings found in ALL the widgets.
        widgetDynamicBindingsMap.values().forEach(bindings -> bindingsInWidgets.addAll(bindings));

        Flux<ActionDTO> allActionsByPageIdMono = newActionService.findByPageIdAndViewMode(pageId, false, MANAGE_ACTIONS)
                .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
                .flatMap(action -> {
                    // filter actions which have been turned off by the user to run on page load and reduce the action world
                    if (hasUserSetActionToNotRunOnPageLoad(action)) {
                        return Mono.empty();
                    }
                    return Mono.just(action);
                })
                .cache();

        Mono<Map<String, ActionDTO>> actionNameToActionMapMono = allActionsByPageIdMono
                .collectMap(
                        ActionDTO::getValidName,
                        action -> action
                )
                .cache();

        Mono<Set<String>> actionsInPageMono = allActionsByPageIdMono
                .map(action -> action.getValidName())
                .collect(Collectors.toSet())
                .cache();

        Mono<Set<ActionDependencyEdge>> directlyReferencedActionsAddedToGraphMono = addDirectlyReferencedActionsToGraph(pageId,
                edges, actionsUsedInDSL, bindingsFromActions, actionsFoundDuringWalk, bindingsInWidgets,
                actionNameToActionMapMono);

        // This publisher traverses the actions and widgets to add all possible edges between entity paths

        // First find all the actions in the page whose name matches the possible entity names found in the bindings in the widget
        Mono<Set<ActionDependencyEdge>> createAllEdgesForPageMono = directlyReferencedActionsAddedToGraphMono
                // Add dependencies of all on page load actions set by the user in the graph
                .flatMap(updatedEdges -> addExplicitUserSetOnLoadActionsToGraph(pageId, updatedEdges, explicitUserSetOnLoadActions, actionsFoundDuringWalk, bindingsFromActions))
                // For all the actions found so far, recursively walk the dynamic bindings of the actions to find more relationships with other actions (& widgets)
                .flatMap(updatedEdges -> recursivelyAddActionsAndTheirDependentsToGraphFromBindings(pageId, updatedEdges, actionsFoundDuringWalk, bindingsFromActions))
                // At last, add all the widget relationships to the graph as well.
                .flatMap(updatedEdges -> addWidgetRelationshipToGraph(updatedEdges, widgetDynamicBindingsMap));


        // Create a graph given edges
        Mono<DirectedAcyclicGraph<String, DefaultEdge>> createGraphMono = Mono.zip(actionsInPageMono, createAllEdgesForPageMono)
                .map(tuple -> {
                    Set<String> allActions = tuple.getT1();
                    Set<ActionDependencyEdge> updatedEdges = tuple.getT2();
                    return constructDAG(allActions, widgetNames, updatedEdges);
                })
                .cache();

        // Generate on page load schedule
        Mono<List<Set<String>>> computeOnPageLoadScheduleNamesMono = Mono.zip(actionsInPageMono, createGraphMono, actionNameToActionMapMono)
                .map(tuple -> {
                    Set<String> actionNames = tuple.getT1();
                    DirectedAcyclicGraph<String, DefaultEdge> graph = tuple.getT2();
                    Map<String, ActionDTO> actionNameToActionMap = tuple.getT3();

                    return computeOnPageLoadActionsSchedulingOrder(graph, onPageLoadActionSet, actionNames, actionNameToActionMap);
                })
                .map(onPageLoadActionsSchedulingOrder -> {
                    // Find all explicitly turned on actions which haven't found their way into the scheduling order
                    // This scenario would happen if an explicitly turned on for page load action does not have any
                    // relationships in the page with any widgets/actions.
                    Set<String> pageLoadActionNames = new HashSet<>();
                    pageLoadActionNames.addAll(onPageLoadActionSet);
                    pageLoadActionNames.addAll(explicitUserSetOnLoadActions);
                    pageLoadActionNames.removeAll(onPageLoadActionSet);

                    // If any of the explicitly set on page load actions havent been added yet, add them to the 0th set
                    // of actions set since no relationships were found with any other appsmith entity
                    if (!pageLoadActionNames.isEmpty()) {
                        onPageLoadActionSet.addAll(explicitUserSetOnLoadActions);

                        // In case there are no page load actions, initialize the 0th set of page load actions list.
                        if (onPageLoadActionsSchedulingOrder.isEmpty()) {
                            onPageLoadActionsSchedulingOrder.add(new HashSet<>());
                        }

                        onPageLoadActionsSchedulingOrder.get(0).addAll(explicitUserSetOnLoadActions);
                    }

                    return ((List<Set<String>>) onPageLoadActionsSchedulingOrder);
                });


        // Transform the schedule order into client feasible DTO
        Mono<List<Set<DslActionDTO>>> computeCompletePageLoadActionScheduleMono =
                filterAndTransformSchedulingOrderToDTO(onPageLoadActionSet, actionNameToActionMapMono, computeOnPageLoadScheduleNamesMono)
                        .cache();


        // With the final on page load scheduling order, also set the on page load actions which would be updated
        // by the caller function
        Mono<List<ActionDTO>> flatPageLoadActionsMono = computeCompletePageLoadActionScheduleMono
                .then(actionNameToActionMapMono)
                .map(actionMap -> {
                    onPageLoadActionSet
                            .stream()
                            .forEach(actionName -> flatPageLoadActions.add(actionMap.get(actionName)));
                    return flatPageLoadActions;
                });

        return createGraphMono
                .then(flatPageLoadActionsMono)
                .then(computeCompletePageLoadActionScheduleMono);

    }

    /**
     * This function takes the page load schedule consisting of only action names.
     *
     * First it trims the order to remove any unwanted actions which shouldn't be run.
     * Following actions are filtered out :
     * 1. Any JS Action since they are not supported to run on page load currently. TODO : Remove this check once the
     *    client implements execution of JS functions.
     * 2. Any action which has been marked to not run on page load by the user.
     *
     * Next it creates a new schedule order consisting of DslActionDTO instead of just action names.
     * @param onPageLoadActionSet
     * @param actionNameToActionMapMono
     * @param computeOnPageLoadScheduleNamesMono
     * @return
     */
    private Mono<List<Set<DslActionDTO>>> filterAndTransformSchedulingOrderToDTO(Set<String> onPageLoadActionSet,
                                                                                 Mono<Map<String, ActionDTO>> actionNameToActionMapMono,
                                                                                 Mono<List<Set<String>>> computeOnPageLoadScheduleNamesMono) {

        return Mono.zip(computeOnPageLoadScheduleNamesMono, actionNameToActionMapMono)
                .map(tuple -> {
                    List<Set<String>> onPageLoadActionsSchedulingOrder = tuple.getT1();
                    Map<String, ActionDTO> actionMap = tuple.getT2();

                    List<Set<DslActionDTO>> onPageLoadActions = new ArrayList<>();

                    for (Set<String> names : onPageLoadActionsSchedulingOrder) {
                        Set<DslActionDTO> actionsInLevel = new HashSet<>();

                        for (String name : names) {
                            ActionDTO action = actionMap.get(name);
                            // TODO : Remove this check once JS actions on page load functionality has been
                            //  implemented on the client side
                            if (PluginType.JS.equals(action.getPluginType())) {
                                // trim out the JS actions in the on page load schedule
                                onPageLoadActionSet.remove(name);
                            } else if (hasUserSetActionToNotRunOnPageLoad(action)) {
                                onPageLoadActionSet.remove(name);
                            } else {
                                actionsInLevel.add(getDslAction(action));
                            }
                        }

                        onPageLoadActions.add(actionsInLevel);
                    }

                    return onPageLoadActions.stream().filter(setOfActions -> !setOfActions.isEmpty()).collect(Collectors.toList());
                });
    }

    /**
     * This function finds all the actions in the page whose name matches the possible entity names found in the
     * bindings in the widget. Caveat : It first removes all invalid bindings from the set of all bindings from the DSL
     * This today means only the usage of an async JS function as a call instead of referring to the `.data`.
     *
     * !!! WARNING !!! : This function updates actionsUsedInDSL set which is used to store all the directly referenced
     * actions in the DSL.
     *
     * @param pageId
     * @param edges
     * @param actionsUsedInDSL
     * @param bindingsFromActions
     * @param actionsFoundDuringWalk
     * @param bindingsInWidgets
     * @param actionNameToActionMapMono
     * @return
     */
    private Mono<Set<ActionDependencyEdge>> addDirectlyReferencedActionsToGraph(String pageId,
                                                                                Set<ActionDependencyEdge> edges,
                                                                                Set<String> actionsUsedInDSL,
                                                                                Set<String> bindingsFromActions,
                                                                                Set<String> actionsFoundDuringWalk,
                                                                                Set<String> bindingsInWidgets,
                                                                                Mono<Map<String, ActionDTO>> actionNameToActionMapMono) {
        return actionNameToActionMapMono
                .map(actionMap -> {
                    Map<String, Set<String>> bindingToPossibleParentMap = bindingsInWidgets
                            .stream()
                            .collect(Collectors.toMap(Function.identity(), v -> getPossibleParents(v)));

                    Set<String> invalidBindings = new HashSet<>();

                    bindingsInWidgets
                            .stream()
                            .forEach(binding -> {
                                Set<String> possibleParents = bindingToPossibleParentMap.get(binding);

                                Optional<String> isAction = possibleParents
                                        .stream()
                                        .filter(parent -> actionMap.get(parent) != null)
                                        .findFirst();

                                // if the binding is referring to the action, ensure that for ASYNC JS functions, it
                                // shouldn't be a function call since that is not supported in dynamic bindings.
                                if (isAction.isPresent()) {
                                    ActionDTO action = actionMap.get(isAction.get());
                                    if (isAsyncJsFunctionCall(action, binding)) {
                                        invalidBindings.add(binding);
                                    }
                                }
                            });

                    return invalidBindings;
                })
                .map(invalidBindings -> {
                    Set<String> validBindings = new HashSet<>(bindingsInWidgets);
                    validBindings.removeAll(invalidBindings);

                    Set<String> possibleEntityNamesInDsl = new HashSet<>();

                    // From all the bindings found in the widget, extract all possible entity names.
                    validBindings.stream().forEach(binding -> possibleEntityNamesInDsl.addAll(getPossibleParents(binding)));

                    return possibleEntityNamesInDsl;
                })
                .flatMapMany(possibleEntityNamesInDsl -> newActionService.findUnpublishedActionsInPageByNames(possibleEntityNamesInDsl, pageId))
                .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
                // Add dependencies of the actions found in the DSL in the graph.
                .map(action -> {
                    // This action is directly referenced in the DSL. This action is an ideal candidate for on page load
                    actionsUsedInDSL.add(action.getValidName());
                    extractAndSetActionBindingsInGraphEdges(edges, action, bindingsFromActions, actionsFoundDuringWalk);
                    return action;
                })
                .collectList()
                .thenReturn(edges);
    }

    /**
     * This function takes all the edges found and outputs a Directed Acyclic Graph. To create the complete graph, the
     * following steps are followed :
     * 1. Trim the edges to only contain relationships between property paths belonging to appsmith entities (actions,
     * widgets, global variables provided by appsmith). If any/all the vertices of the edge are unknown, the edge
     * is removed.
     * 2. Add implicit relationship between property paths and their immediate parents. This is to ensure that in the
     * DAG, all the relationships are considered.
     * e.g following are the implicit relationships generated for property path `Dropdown1.options[1].value`:
     *
     * Dropdown1.options[1].value -> Dropdown1.options[1]
     * Dropdown1.options[1] -> Dropdown1.options
     * Dropdown1.options -> Dropdown1
     *
     * 3. Now create the DAG using the edges after the two steps.
     * @param actionNames
     * @param widgetNames
     * @param edges
     * @return
     */
    private DirectedAcyclicGraph<String, DefaultEdge> constructDAG(Set<String> actionNames,
                                                                   Set<String> widgetNames,
                                                                   Set<ActionDependencyEdge> edges) {

        DirectedAcyclicGraph<String, DefaultEdge> actionSchedulingGraph =
                new DirectedAcyclicGraph<>(DefaultEdge.class);

        Set<ActionDependencyEdge> implicitParentChildEdges = new HashSet<>();

        // Remove any edge which contains an unknown entity - aka neither a known action nor a known widget
        // Note : appsmith world objects like `appsmith` would also count as an unknown here.
        // TODO : Handle the above global variables provided by appsmith in the following filtering.
        edges = edges.stream().filter(edge -> {

                    String source = edge.getSource();
                    String target = edge.getTarget();

                    Set<String> vertices = Set.of(source, target);

                    AtomicReference<Boolean> isValidVertex = new AtomicReference<>(true);

                    // Assert that the vertices which are entire property paths have a possible parent which is either
                    // an action or a widget or a static variable provided by appsmith at page/application level.
                    vertices
                            .stream()
                            .forEach(vertex -> {
                                Optional<String> validEntity = getPossibleParents(vertex)
                                        .stream()
                                        .filter(parent -> {
                                            if (!actionNames.contains(parent) && !widgetNames.contains(parent) && !APPSMITH_GLOBAL_VARIABLES.contains(parent)) {
                                                return false;
                                            }
                                            return true;
                                        }).findFirst();

                                // If any of the generated entity names from the path are valid appsmith entity name,
                                // the vertex is considered valid
                                if (validEntity.isPresent()) {
                                    isValidVertex.set(TRUE);
                                } else {
                                    isValidVertex.set(FALSE);
                                }
                            });

                    return isValidVertex.get();
                })
                .collect(Collectors.toSet());

        // Now add the relationship aka when a child gets updated, the parent should get updated as well. Aka
        // parent depends on the child.
        for (ActionDependencyEdge edge : edges) {
            String source = edge.getSource();
            String target = edge.getTarget();

            Set<String> vertices = Set.of(source, target);

            vertices.stream().forEach(vertex -> implicitParentChildEdges.addAll(generateParentChildRelationships(vertex)));

        }

        edges.addAll(implicitParentChildEdges);

        // Now create the graph from all the edges.
        for (ActionDependencyEdge edge : edges) {

            String source = edge.getSource();
            String target = edge.getTarget();

            actionSchedulingGraph.addVertex(source);
            actionSchedulingGraph.addVertex(target);

            try {
                actionSchedulingGraph.addEdge(source, target);
            } catch (IllegalArgumentException e) {
                // This error is also thrown when adding an edge which makes the graph cyclical
                if (e.getMessage().contains("Edge would induce a cycle")) {
                    throw new AppsmithException(AppsmithError.CYCLICAL_DEPENDENCY_ERROR, edge.toString());
                }
            }
        }

        return actionSchedulingGraph;
    }

    /**
     * This function takes a Directed Acyclic Graph and computes on page load actions. The final results is a list of set
     * of actions. The set contains all the independent actions which can be executed in parallel. The List represents
     * dependencies. The 0th index set contains actions which are executable immediately. The next index contains all
     * actions which depend on one or more of the actions which were executed from the 0th index set and so on.
     * Breadth First level by level traversal is used to compute each set of such independent actions.
     * @param dag : The DAG graph containing all the edges representing dependencies between appsmith entities in the page.
     * @param onPageLoadActionSet
     * @param actionNames : All the action names for the page
     * @param actionNameToActionMap : Map indexed by the action name for all the actions in the page.
     * @return
     */
    private List<Set<String>> computeOnPageLoadActionsSchedulingOrder(DirectedAcyclicGraph<String, DefaultEdge> dag,
                                                                          Set<String> onPageLoadActionSet,
                                                                          Set<String> actionNames,
                                                                          Map<String, ActionDTO> actionNameToActionMap) {
        List<Set<String>> onPageLoadActions = new ArrayList<>();

        // Find all root nodes to start the BFS traversal from
        List<String> rootNodes = dag.vertexSet().stream()
                .filter(key -> dag.incomingEdgesOf(key).size() == 0)
                .collect(Collectors.toList());

        BreadthFirstIterator<String, DefaultEdge> bfsIterator = new BreadthFirstIterator<>(dag, rootNodes);

        // Implementation of offline scheduler by using level by level traversal. Level i+1 actions would be dependent
        // on Level i actions. All actions in a level can run independently and hence would get added to the same set.
        while (bfsIterator.hasNext()) {

            String vertex = bfsIterator.next();
            int level = bfsIterator.getDepth(vertex);
            if (onPageLoadActions.size() <= level) {
                onPageLoadActions.add(new HashSet<>());
            }

            Set<String> actionsFromBinding = actionCandidatesForPageLoadFromBinding(actionNames, actionNameToActionMap, vertex, onPageLoadActionSet);
            onPageLoadActions.get(level).addAll(actionsFromBinding);
            onPageLoadActionSet.addAll(actionsFromBinding);
        }

        // Trim all empty sets from the list before returning.
        return onPageLoadActions.stream().filter(setOfActions -> !setOfActions.isEmpty()).collect(Collectors.toList());
    }

    /**
     * This function gets a set of binding names that come from other actions. It looks for actions in the page with
     * the same names as words in the binding names set. If yes, it creates a new set of dynamicBindingNames, adds these newly
     * found actions' bindings in the set, adds the new actions and their bindings to actionNames and edges and
     * recursively calls itself with the new set of dynamicBindingNames.
     * This ensures that the DAG that we create is complete and contains all possible actions and their dependencies
     *
     * @return
     */
    private Mono<Set<ActionDependencyEdge>> recursivelyAddActionsAndTheirDependentsToGraphFromBindings(String pageId,
                                                                                                       Set<ActionDependencyEdge> edges,
                                                                                                       Set<String> actionsFoundDuringWalk,
                                                                                                       Set<String> dynamicBindings) {
        if (dynamicBindings == null || dynamicBindings.isEmpty()) {
            return Mono.just(edges);
        }

        Set<String> possibleActionNames = new HashSet<>();

        dynamicBindings.stream().forEach(binding -> possibleActionNames.addAll(getPossibleParents(binding)));

        // All actions found from possibleActionNames set would add their dependencies in the following set for further
        // walk to find more actions recursively.
        Set<String> newBindings = new HashSet<>();

        // First fetch all the actions in the page whose name matches the words found in all the dynamic bindings
        Mono<List<ActionDTO>> findAndAddActionsInBindingsMono = newActionService.findUnpublishedActionsInPageByNames(possibleActionNames, pageId)
                .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
                .map(action -> {

                    extractAndSetActionBindingsInGraphEdges(edges, action, newBindings, actionsFoundDuringWalk);

                    return action;
                })
                .collectList();

        return findAndAddActionsInBindingsMono
                .flatMap(actions -> {
                    // Now that the next set of bindings have been found, find recursively all actions by these names
                    // and their bindings
                    return recursivelyAddActionsAndTheirDependentsToGraphFromBindings(pageId, edges, actionsFoundDuringWalk, newBindings);
                });
    }

    /**
     * This function finds all the actions which have been set to run on page load by the user and adds their
     * dependencies to the graph.
     *
     * Note : If such an action has no dependencies and no interesting entity depends on it,
     * this action would still not get added to the output of page load scheduler. This function only ensures that the
     * dependencies of user set on page load actions are accounted for.
     *
     * !!! WARNING !!! : This function updates the set `explicitUserSetOnLoadActions` and adds the names of all such
     * actions found in this function.
     *
     * @param pageId
     * @param edges
     * @param explicitUserSetOnLoadActions
     * @param actionsFoundDuringWalk
     * @param bindingsFromActions
     * @return
     */
    private Mono<Set<ActionDependencyEdge>> addExplicitUserSetOnLoadActionsToGraph(String pageId,
                                                                                   Set<ActionDependencyEdge> edges,
                                                                                   Set<String> explicitUserSetOnLoadActions,
                                                                                   Set<String> actionsFoundDuringWalk,
                                                                                   Set<String> bindingsFromActions) {

        //First fetch all the actions which have been tagged as on load by the user explicitly.
        return newActionService.findUnpublishedOnLoadActionsExplicitSetByUserInPage(pageId)
                .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
                // Add the vertices and edges to the graph for these actions
                .map(actionDTO -> {
                    extractAndSetActionBindingsInGraphEdges(edges, actionDTO, bindingsFromActions, actionsFoundDuringWalk);
                    explicitUserSetOnLoadActions.add(actionDTO.getValidName());
                    return actionDTO;
                })
                .collectList()
                .thenReturn(edges);
    }

    /**
     * Given an action, this function adds all the dependencies the action to the graph edges. This is achieved by first
     * walking the action configuration and finding the paths and the mustache JS snippets found at the said path. Then
     * the relationship between the complete path and the property paths found in the mustache JS snippets are added to
     * the graph edges.
     *
     * !!! WARNING !!! : This function updates the set actionsFoundDuringWalk since this function is called from all
     * places to add the action dependencies. If the action has already been discovered, this function exits by checking
     * in the actionsFoundDuringWalk, else, it adds it to the set.
     * This function also updates `edges` by adding all the new relationships for the said action in the set.
     *
     * @param edges
     * @param action
     * @param bindingsFromActions
     * @param actionsFoundDuringWalk
     */
    private void extractAndSetActionBindingsInGraphEdges(Set<ActionDependencyEdge> edges,
                                                         ActionDTO action,
                                                         Set<String> bindingsFromActions,
                                                         Set<String> actionsFoundDuringWalk) {


        // Check if the action has been deleted in unpublished state. If yes, ignore it.
        if (action.getDeletedAt() != null) {
            return;
        }

        String name = action.getValidName();

        if (actionsFoundDuringWalk.contains(name)) {
            // This action has already been found in our walk. Ignore this.
            return;
        }
        actionsFoundDuringWalk.add(name);

        Map<String, Set<String>> actionBindingMap = getActionBindingMap(action);

        Set<String> allBindings = new HashSet<>();
        actionBindingMap.values().stream().forEach(bindings -> allBindings.addAll(bindings));

        if (!allBindings.containsAll(action.getJsonPathKeys())) {
            Set<String> invalidBindings = new HashSet<>(action.getJsonPathKeys());
            invalidBindings.removeAll(allBindings);
            log.error("Invalid dynamic binding path list for action id {}. Not taking the following bindings in " +
                            "consideration for computing on page load actions : {}",
                    action.getId(),
                    invalidBindings);
        }

        Set<String> bindingPaths = actionBindingMap.keySet();

        for (String bindingPath : bindingPaths) {
            Set<String> dynamicBindings = actionBindingMap.get(bindingPath);
            for (String binding : dynamicBindings) {
                Set<String> entityPaths = getWordsFromMustache(binding);
                for (String source : entityPaths) {
                    ActionDependencyEdge edge = new ActionDependencyEdge(source, bindingPath);
                    edges.add(edge);
                }
                // Add all the binding words further examination for dependencies in the future.
                bindingsFromActions.addAll(entityPaths);
            }
        }
    }

    /**
     * This function walks the widget bindings and adds all the relationships which have been discovered earlier while
     * walking the DSL and extracting all the relationships into edges between the widget path where there is a dynamic
     * binding and the entities in the bindings.
     *
     * @param edges
     * @param widgetBindingMap
     * @return
     */
    private Mono<Set<ActionDependencyEdge>> addWidgetRelationshipToGraph(Set<ActionDependencyEdge> edges,
                                                                         Map<String, Set<String>> widgetBindingMap) {

        return Mono.just(widgetBindingMap)
                .map(widgetDynamicBindingsMap -> {
                    widgetDynamicBindingsMap.forEach((widgetPath, widgetDynamicBindings) -> {

                        // Given the widget path, add all the relationships between the binding to the path
                        widgetDynamicBindings.stream().forEach(binding -> {
                            Set<String> entityPaths = getWordsFromMustache(binding);
                            for (String source : entityPaths) {
                                ActionDependencyEdge edge = new ActionDependencyEdge(source, widgetPath);
                                edges.add(edge);
                            }
                        });

                    });

                    return widgetDynamicBindingsMap;
                })
                .thenReturn(edges);
    }


    private boolean hasUserSetActionToNotRunOnPageLoad(ActionDTO unpublishedAction) {
        if (TRUE.equals(unpublishedAction.getUserSetOnLoad())
                && !TRUE.equals(unpublishedAction.getExecuteOnLoad())) {
            return true;
        }

        return false;
    }

    /**
     * This function walks the action configuration and extracts a map of all the dynamic bindings present and the
     * action path where they exist.
     *
     * @param action
     * @return
     */
    private Map<String, Set<String>> getActionBindingMap(ActionDTO action) {

        List<Property> dynamicBindingPathList = action.getDynamicBindingPathList();
        Map<String, Set<String>> completePathToDynamicBindingMap = new HashMap<>();

        Map<String, Object> configurationObj = objectMapper.convertValue(action.getActionConfiguration(), Map.class);

        if (dynamicBindingPathList != null) {
            // Each of these might have nested structures, so we iterate through them to find the leaf node for each
            for (Property x : dynamicBindingPathList) {
                final String fieldPath = String.valueOf(x.getKey());
                String[] fields = fieldPath.split("[].\\[]");
                // For nested fields, the parent dsl to search in would shift by one level every iteration
                Object parent = configurationObj;
                Iterator<String> fieldsIterator = Arrays.stream(fields).filter(fieldToken -> !fieldToken.isBlank()).iterator();
                boolean isLeafNode = false;
                // This loop will end at either a leaf node, or the last identified JSON field (by throwing an exception)
                // Valid forms of the fieldPath for this search could be:
                // root.field.list[index].childField.anotherList.indexWithDotOperator.multidimensionalList[index1][index2]
                while (fieldsIterator.hasNext()) {
                    String nextKey = fieldsIterator.next();
                    if (parent instanceof JSONObject) {
                        parent = ((JSONObject) parent).get(nextKey);
                    } else if (parent instanceof Map) {
                        parent = ((Map<String, ?>) parent).get(nextKey);
                    } else if (parent instanceof List) {
                        if (Pattern.matches(Pattern.compile("[0-9]+").toString(), nextKey)) {
                            try {
                                parent = ((List) parent).get(Integer.parseInt(nextKey));
                            } catch (IndexOutOfBoundsException e) {
                                // The index being referred does not exist. Hence the path would not exist. Ignore the
                                // binding
                            }
                        } else {
                            // The list configuration does not seem correct. Hence the path would not exist. Ignore the
                            // binding
                        }
                    }
                    // After updating the parent, check for the types
                    if (parent == null) {
                        // path doesnt seem to exist. Ignore.
                    } else if (parent instanceof String) {
                        // If we get String value, then this is a leaf node
                        isLeafNode = true;
                    }
                }
                // Only extract mustache keys from leaf nodes
                if (isLeafNode) {

                    Boolean isBindingPresentInString = MustacheHelper.laxIsBindingPresentInString((String) parent);
                    // We found the path. But if the path has mustache bindings, record the same in the map
                    // In case
                    if (isBindingPresentInString || PluginType.JS.equals(action.getPluginType())) {
                        Set<String> mustacheKeysFromFields;
                        // Stricter extraction of dynamic bindings
                        if (isBindingPresentInString) {
                            mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent);
                        } else {
                            // this must be a JS function. No need to extract mustache. The entire string is JS body
                            mustacheKeysFromFields = Set.of((String) parent);
                        }

                        String completePath = action.getValidName() + ".actionConfiguration." + fieldPath;
                        if (completePathToDynamicBindingMap.containsKey(completePath)) {
                            Set<String> mustacheKeysForWidget = completePathToDynamicBindingMap.get(completePath);
                            mustacheKeysFromFields.addAll(mustacheKeysForWidget);
                        }
                        completePathToDynamicBindingMap.put(completePath, mustacheKeysFromFields);
                    }

                }
            }
        }

        return completePathToDynamicBindingMap;
    }

    /**
     * This function creates all `child -> parent` edges given a path.
     *
     * For example, given path `Dropdown1.options[1].value`, the following relationships are generated :
     *
     * Dropdown1.options[1].value -> Dropdown1.options[1]
     * Dropdown1.options[1] -> Dropdown1.options
     * Dropdown1.options -> Dropdown1
     *
     * @param path
     * @return
     */
    private Set<ActionDependencyEdge> generateParentChildRelationships(String path) {
        Set<ActionDependencyEdge> edges = new HashSet<>();

        String parent;

        while (true) {
            try {
                Matcher matcher = parentPattern.matcher(path);
                matcher.find();
                parent = matcher.group(1);
                edges.add(new ActionDependencyEdge(path, parent));
                path = parent;
            } catch (IllegalStateException | IndexOutOfBoundsException e) {
                // No matches being found. Break out of infinite loop
                break;
            }
        }

        return edges;
    }

    /**
     * Given a dynamic binding, this function checks if it is a candidate for on page load. This is done by checking for
     * the following two conditions :
     *  - Is it an action name (which has been found in the page). If not, ignore.
     *  - Has this action already been found for page load? If yes, ignore.
     *  - If JS, following two conditions are checked for :
     *      - If sync function, ignore. This is because client would execute the same during dynamic binding eval
     *      - If async function, it is a candidate only if the data for the function is referred in the dynamic binding.
     *
     * @param allActionNames
     * @param actionNameToActionMap
     * @param dynamicBinding
     * @param existingActionsInOnPageLoad
     * @return
     */
    private Set<String> actionCandidatesForPageLoadFromBinding(Set<String> allActionNames,
                                                               Map<String, ActionDTO> actionNameToActionMap,
                                                               String dynamicBinding,
                                                               Set<String> existingActionsInOnPageLoad) {

        Set<String> onPageLoadCandidates = new HashSet<>();

        Set<String> possibleParents = getPossibleParents(dynamicBinding);

        for (String entity : possibleParents) {
            // if this generated entity name from the binding matches an action name which hasn't yet been added to the
            // on page load list, check for its eligibility
            if (allActionNames.contains(entity) && !existingActionsInOnPageLoad.contains(entity)) {
                // This is definitely an action which hasn't yet been discovered for on page load.
                ActionDTO action = actionNameToActionMap.get(entity);

                Boolean isCandidateForPageLoad = TRUE;

                if (PluginType.JS.equals(action.getPluginType()) &&
                        TRUE.equals(action.getActionConfiguration().getIsAsync())) {

                    // This is an async JS action. Only add it for page load if it is not a function call. Aka the data
                    // of this async call is being referred to in the binding.

                    String validBinding = entity + "." + "data";
                    if (!dynamicBinding.contains(validBinding)) {
                        isCandidateForPageLoad = FALSE;
                    }
                }

                if (isCandidateForPageLoad) {
                    onPageLoadCandidates.add(entity);
                }
            }
        }

        return onPageLoadCandidates;
    }

    private boolean isAsyncJsFunctionCall(ActionDTO action, String binding) {
        if (PluginType.JS.equals(action.getPluginType()) &&
                TRUE.equals(action.getActionConfiguration().getIsAsync())) {

            // This function is ASYNC. Now check if the binding is a function call. If yes, then this action would not
            // be a candidate for on page load.
            String name = action.getValidName();

            // Regex pattern to check for actionname(parameter1, parameter2..) where no parameters are also allowed.
            String regexForFunctionCall = name + "\\([^\\)]*\\)(\\.[^\\)]*\\))?";
            Pattern functionCallPattern = Pattern.compile(regexForFunctionCall);
            Matcher matcher = functionCallPattern.matcher(binding);

            if (matcher.find()) {
                return TRUE;
            }
        }

        return FALSE;
    }

    private DslActionDTO getDslAction(ActionDTO actionDTO) {

        DslActionDTO dslActionDTO = new DslActionDTO();

        dslActionDTO.setId(actionDTO.getId());
        dslActionDTO.setPluginType(actionDTO.getPluginType());
        dslActionDTO.setJsonPathKeys(actionDTO.getJsonPathKeys());
        dslActionDTO.setName(actionDTO.getValidName());

        if (actionDTO.getActionConfiguration() != null) {
            dslActionDTO.setTimeoutInMillisecond(actionDTO.getActionConfiguration().getTimeoutInMillisecond());
        }

        return dslActionDTO;
    }
}
