001package gudusoft.gsqlparser.analyzer.v2.impact;
002
003import gudusoft.gsqlparser.analyzer.v2.ETableAccessKind;
004import gudusoft.gsqlparser.analyzer.v2.ImpactEntry;
005import gudusoft.gsqlparser.analyzer.v2.ImpactResult;
006import gudusoft.gsqlparser.analyzer.v2.callgraph.CallEdge;
007import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraph;
008import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraphNode;
009import gudusoft.gsqlparser.analyzer.v2.callgraph.TableAccess;
010import gudusoft.gsqlparser.ir.common.SourceAnchor;
011
012import java.util.*;
013
014/**
015 * Impact analysis engine using real BFS traversal with path reconstruction.
016 * <p>
017 * Provides accurate graph distances (not sequential counters) and
018 * reconstructed shortest paths from source to each impacted entity.
019 */
020public class ImpactEngine {
021
022    private ImpactEngine() {}
023
024    /**
025     * Computes routine impact: finds all routines and tables transitively
026     * affected by a change to the given routine, using forward BFS.
027     *
028     * @param callGraph  the call graph to traverse
029     * @param routineId  the source routine to analyze impact from
030     * @param maxDepth   maximum BFS depth (from config)
031     * @return impact result with real graph distances and reconstructed paths
032     */
033    public static ImpactResult getRoutineImpact(CallGraph callGraph, String routineId, int maxDepth) {
034        if (callGraph == null) {
035            return emptyResult();
036        }
037
038        // BFS forward from source
039        Map<String, Integer> distance = new LinkedHashMap<String, Integer>();
040        Map<String, String> predecessor = new LinkedHashMap<String, String>();
041        Deque<String> queue = new ArrayDeque<String>();
042
043        distance.put(routineId, 0);
044        queue.add(routineId);
045
046        while (!queue.isEmpty()) {
047            String current = queue.poll();
048            int currentDepth = distance.get(current);
049
050            if (currentDepth >= maxDepth) {
051                continue;
052            }
053
054            for (String callee : callGraph.getDirectCallees(current)) {
055                if (!distance.containsKey(callee)) {
056                    distance.put(callee, currentDepth + 1);
057                    predecessor.put(callee, current);
058                    queue.add(callee);
059                }
060            }
061        }
062
063        // Build results
064        List<ImpactEntry> impactedRoutines = new ArrayList<ImpactEntry>();
065        List<ImpactEntry> impactedTables = new ArrayList<ImpactEntry>();
066
067        // Include the source routine's own table accesses at depth 0
068        collectTableImpact(callGraph, routineId, 0,
069                Collections.singletonList(routineId), impactedTables);
070
071        for (Map.Entry<String, Integer> entry : distance.entrySet()) {
072            String nodeId = entry.getKey();
073            if (nodeId.equals(routineId)) {
074                continue; // skip source
075            }
076            int depth = entry.getValue();
077            List<String> path = reconstructPath(predecessor, routineId, nodeId);
078
079            CallGraphNode node = callGraph.getNode(nodeId);
080            String displayName = node != null ? node.getRoutineName() : nodeId;
081            SourceAnchor anchor = node != null ? node.getDeclarationAnchor() : null;
082
083            impactedRoutines.add(new ImpactEntry(
084                    nodeId, displayName, depth, path, anchor));
085
086            // Collect table accesses for this callee
087            collectTableImpact(callGraph, nodeId, depth, path, impactedTables);
088        }
089
090        return new ImpactResult(impactedRoutines, impactedTables);
091    }
092
093    /**
094     * Computes table impact: finds all routines affected by a change to the given table.
095     * Uses reverse BFS from each direct accessor to find transitive callers.
096     *
097     * @param callGraph    the call graph
098     * @param tableName    the table name to analyze
099     * @param accessFilter optional filter (null = all access types)
100     * @param maxDepth     maximum BFS depth
101     * @return impact result
102     */
103    public static ImpactResult getTableImpact(CallGraph callGraph, String tableName,
104                                               ETableAccessKind accessFilter, int maxDepth) {
105        if (callGraph == null) {
106            return emptyResult();
107        }
108
109        List<CallGraphNode> directAccessors =
110                callGraph.getNodesAccessingTable(tableName, accessFilter);
111
112        List<ImpactEntry> impactedRoutines = new ArrayList<ImpactEntry>();
113        List<ImpactEntry> impactedTables = new ArrayList<ImpactEntry>();
114
115        // Add the table itself at depth 0
116        impactedTables.add(new ImpactEntry(
117                tableName, tableName, 0,
118                Collections.singletonList(tableName), null));
119
120        Set<String> seen = new LinkedHashSet<String>();
121
122        for (CallGraphNode accessor : directAccessors) {
123            String accessorId = accessor.getRoutineId();
124            if (!seen.add(accessorId)) {
125                continue;
126            }
127
128            // Direct accessor at depth 1
129            List<String> directPath = new ArrayList<String>();
130            directPath.add(tableName);
131            directPath.add(accessorId);
132
133            impactedRoutines.add(new ImpactEntry(
134                    accessorId, accessor.getRoutineName(), 1,
135                    directPath, accessor.getDeclarationAnchor()));
136
137            // Reverse BFS from accessor to find transitive callers
138            Map<String, Integer> callerDistance = new LinkedHashMap<String, Integer>();
139            Map<String, String> callerPredecessor = new LinkedHashMap<String, String>();
140            Deque<String> queue = new ArrayDeque<String>();
141
142            callerDistance.put(accessorId, 0);
143            queue.add(accessorId);
144
145            while (!queue.isEmpty()) {
146                String current = queue.poll();
147                int currentDepth = callerDistance.get(current);
148
149                if (currentDepth >= maxDepth - 1) { // -1 because accessor is already depth 1
150                    continue;
151                }
152
153                for (String caller : callGraph.getDirectCallers(current)) {
154                    if (!callerDistance.containsKey(caller) && seen.add(caller)) {
155                        callerDistance.put(caller, currentDepth + 1);
156                        callerPredecessor.put(caller, current);
157                        queue.add(caller);
158
159                        int totalDepth = currentDepth + 2; // +2 = table→accessor + caller chain
160                        List<String> callerPath = new ArrayList<String>();
161                        callerPath.add(tableName);
162                        // Reconstruct accessor → ... → caller
163                        List<String> chainPath = reconstructPath(
164                                callerPredecessor, accessorId, caller);
165                        callerPath.addAll(chainPath);
166
167                        CallGraphNode callerNode = callGraph.getNode(caller);
168                        String name = callerNode != null ? callerNode.getRoutineName() : caller;
169                        SourceAnchor callerAnchor = callerNode != null
170                                ? callerNode.getDeclarationAnchor() : null;
171
172                        impactedRoutines.add(new ImpactEntry(
173                                caller, name, totalDepth, callerPath, callerAnchor));
174                    }
175                }
176            }
177        }
178
179        return new ImpactResult(impactedRoutines, impactedTables);
180    }
181
182    /**
183     * Reconstructs the shortest path from source to target using the predecessor map.
184     */
185    private static List<String> reconstructPath(Map<String, String> predecessor,
186                                                 String source, String target) {
187        List<String> path = new ArrayList<String>();
188        String current = target;
189        while (current != null && !current.equals(source)) {
190            path.add(current);
191            current = predecessor.get(current);
192        }
193        path.add(source);
194        Collections.reverse(path);
195        return path;
196    }
197
198    private static void collectTableImpact(CallGraph callGraph, String routineId,
199                                            int depth, List<String> basePath,
200                                            List<ImpactEntry> impactedTables) {
201        CallGraphNode node = callGraph.getNode(routineId);
202        if (node == null) {
203            return;
204        }
205        for (TableAccess ta : node.getTableAccesses()) {
206            List<String> tablePath = new ArrayList<String>(basePath);
207            tablePath.add(ta.getTableName());
208            impactedTables.add(new ImpactEntry(
209                    ta.getTableName(), ta.getTableName(), depth,
210                    tablePath, ta.getAnchor()));
211        }
212    }
213
214    private static ImpactResult emptyResult() {
215        return new ImpactResult(
216                Collections.<ImpactEntry>emptyList(),
217                Collections.<ImpactEntry>emptyList());
218    }
219}