44import io .github .randomcodespace .iq .analyzer .Analyzer ;
55import io .github .randomcodespace .iq .cache .AnalysisCache ;
66import io .github .randomcodespace .iq .config .CodeIqConfig ;
7+ import io .github .randomcodespace .iq .graph .GraphStore ;
78import io .github .randomcodespace .iq .model .CodeEdge ;
89import io .github .randomcodespace .iq .model .CodeNode ;
910import io .github .randomcodespace .iq .model .NodeKind ;
4243public class GraphController {
4344
4445 private final QueryService queryService ;
46+ private final GraphStore graphStore ;
4547 private final Analyzer analyzer ;
4648 private final CodeIqConfig config ;
4749 private final StatsService statsService ;
@@ -51,10 +53,12 @@ public class GraphController {
5153 private volatile List <CodeEdge > cachedEdges ;
5254
5355 public GraphController (@ org .springframework .beans .factory .annotation .Autowired (required = false ) QueryService queryService ,
56+ @ org .springframework .beans .factory .annotation .Autowired (required = false ) GraphStore graphStore ,
5457 Analyzer analyzer ,
5558 CodeIqConfig config , StatsService statsService ,
5659 TopologyService topologyService ) {
5760 this .queryService = queryService ;
61+ this .graphStore = graphStore ;
5862 this .analyzer = analyzer ;
5963 this .config = config ;
6064 this .statsService = statsService ;
@@ -87,32 +91,61 @@ private void invalidateCache() {
8791 cachedEdges = null ;
8892 }
8993
94+ /**
95+ * Get nodes from the best available source: Neo4j first, H2 fallback.
96+ * Returns null if neither source has data.
97+ */
98+ private List <CodeNode > getEffectiveNodes () {
99+ if (graphStore != null && useNeo4j ()) {
100+ return graphStore .findAll ();
101+ }
102+ ensureCacheLoaded ();
103+ return cachedNodes ;
104+ }
105+
106+ /**
107+ * Get edges from the best available source: Neo4j first, H2 fallback.
108+ * Returns null if neither source has data.
109+ */
110+ private List <CodeEdge > getEffectiveEdges () {
111+ if (graphStore != null && useNeo4j ()) {
112+ // Collect all edges from Neo4j nodes
113+ return graphStore .findAll ().stream ()
114+ .flatMap (n -> n .getEdges ().stream ())
115+ .toList ();
116+ }
117+ ensureCacheLoaded ();
118+ return cachedEdges ;
119+ }
120+
90121 @ GetMapping ("/stats" )
91122 public Map <String , Object > getStats () {
123+ if (useNeo4j ()) {
124+ return queryService .getStats ();
125+ }
92126 ensureCacheLoaded ();
93127 if (cachedNodes != null ) {
94128 return statsService .computeStats (cachedNodes , cachedEdges );
95129 }
96- if (queryService != null ) {
97- return queryService .getStats ();
98- }
99130 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
100131 "No analysis data available. Run analyze first." );
101132 }
102133
103134 @ GetMapping ("/stats/detailed" )
104135 public Map <String , Object > getDetailedStats (
105136 @ RequestParam (defaultValue = "all" ) String category ) {
106- ensureCacheLoaded ();
107- if (cachedNodes == null ) {
137+ // Use Neo4j-backed stats if available, fall back to H2
138+ List <CodeNode > nodes = getEffectiveNodes ();
139+ List <CodeEdge > edges = getEffectiveEdges ();
140+ if (nodes == null ) {
108141 throw new ResponseStatusException (HttpStatus .NOT_FOUND ,
109- "No analysis cache found. Run analyze first." );
142+ "No analysis data found. Run analyze first." );
110143 }
111144
112145 if ("all" .equalsIgnoreCase (category )) {
113- return statsService .computeStats (cachedNodes , cachedEdges );
146+ return statsService .computeStats (nodes , edges );
114147 }
115- Map <String , Object > catStats = statsService .computeCategory (cachedNodes , cachedEdges , category );
148+ Map <String , Object > catStats = statsService .computeCategory (nodes , edges , category );
116149 if (catStats == null ) {
117150 throw new ResponseStatusException (HttpStatus .BAD_REQUEST ,
118151 "Unknown category: " + category );
@@ -124,6 +157,9 @@ public Map<String, Object> getDetailedStats(
124157
125158 @ GetMapping ("/kinds" )
126159 public Map <String , Object > listKinds () {
160+ if (useNeo4j ()) {
161+ return queryService .listKinds ();
162+ }
127163 ensureCacheLoaded ();
128164 if (cachedNodes != null ) {
129165 Map <String , Long > kindCounts = cachedNodes .stream ()
@@ -144,7 +180,6 @@ public Map<String, Object> listKinds() {
144180 result .put ("total" , cachedNodes .size ());
145181 return result ;
146182 }
147- if (queryService != null ) return queryService .listKinds ();
148183 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
149184 "No data available. Run analyze first." );
150185 }
@@ -155,6 +190,9 @@ public Map<String, Object> nodesByKind(
155190 @ RequestParam (defaultValue = "50" ) int limit ,
156191 @ RequestParam (defaultValue = "0" ) int offset ) {
157192 int safeLimit = Math .min (limit , 1000 );
193+ if (useNeo4j ()) {
194+ return queryService .nodesByKind (kind , safeLimit , offset );
195+ }
158196 ensureCacheLoaded ();
159197 if (cachedNodes != null ) {
160198 List <CodeNode > filtered = cachedNodes .stream ()
@@ -170,7 +208,6 @@ public Map<String, Object> nodesByKind(
170208 result .put ("nodes" , filtered .subList (start , end ).stream ().map (this ::nodeToMap ).toList ());
171209 return result ;
172210 }
173- if (queryService != null ) return queryService .nodesByKind (kind , safeLimit , offset );
174211 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
175212 "No data available. Run analyze first." );
176213 }
@@ -181,6 +218,9 @@ public Map<String, Object> listNodes(
181218 @ RequestParam (defaultValue = "100" ) int limit ,
182219 @ RequestParam (defaultValue = "0" ) int offset ) {
183220 int safeLimit = Math .min (limit , 1000 );
221+ if (useNeo4j ()) {
222+ return queryService .listNodes (kind , safeLimit , offset );
223+ }
184224 ensureCacheLoaded ();
185225 if (cachedNodes != null ) {
186226 List <CodeNode > filtered = cachedNodes ;
@@ -198,13 +238,15 @@ public Map<String, Object> listNodes(
198238 result .put ("limit" , safeLimit );
199239 return result ;
200240 }
201- if (queryService != null ) return queryService .listNodes (kind , safeLimit , offset );
202241 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
203242 "No data available. Run analyze first." );
204243 }
205244
206245 @ GetMapping ("/nodes/find" )
207246 public List <Map <String , Object >> findNode (@ RequestParam String q ) {
247+ if (graphStore != null && useNeo4j ()) {
248+ return topologyService .findNode (q , graphStore .findAll ());
249+ }
208250 ensureCacheLoaded ();
209251 if (cachedNodes == null ) {
210252 return List .of ();
@@ -214,7 +256,12 @@ public List<Map<String, Object>> findNode(@RequestParam String q) {
214256
215257 @ GetMapping ("/nodes/{nodeId}/detail" )
216258 public Map <String , Object > nodeDetail (@ PathVariable String nodeId ) {
217- // Try H2 cache first
259+ // Try Neo4j first for rich detail with edges
260+ if (useNeo4j ()) {
261+ Map <String , Object > result = queryService .nodeDetailWithEdges (nodeId );
262+ if (result != null ) return result ;
263+ }
264+ // Fall back to H2 cache
218265 ensureCacheLoaded ();
219266 if (cachedNodes != null ) {
220267 return cachedNodes .stream ()
@@ -223,12 +270,7 @@ public Map<String, Object> nodeDetail(@PathVariable String nodeId) {
223270 .map (this ::nodeToMap )
224271 .orElseThrow (() -> new ResponseStatusException (HttpStatus .NOT_FOUND , "Node not found: " + nodeId ));
225272 }
226- if (queryService == null ) throw new ResponseStatusException (HttpStatus .NOT_FOUND , "Node not found: " + nodeId );
227- Map <String , Object > result = queryService .nodeDetailWithEdges (nodeId );
228- if (result == null ) {
229- throw new ResponseStatusException (HttpStatus .NOT_FOUND , "Node not found: " + nodeId );
230- }
231- return result ;
273+ throw new ResponseStatusException (HttpStatus .NOT_FOUND , "Node not found: " + nodeId );
232274 }
233275
234276 @ GetMapping ("/nodes/{nodeId}/neighbors" )
@@ -245,6 +287,9 @@ public Map<String, Object> listEdges(
245287 @ RequestParam (defaultValue = "100" ) int limit ,
246288 @ RequestParam (defaultValue = "0" ) int offset ) {
247289 int safeLimit = Math .min (limit , 1000 );
290+ if (useNeo4j ()) {
291+ return queryService .listEdges (kind , safeLimit , offset );
292+ }
248293 ensureCacheLoaded ();
249294 if (cachedEdges != null ) {
250295 List <CodeEdge > filtered = cachedEdges ;
@@ -261,8 +306,8 @@ public Map<String, Object> listEdges(
261306 result .put ("total" , filtered .size ());
262307 return result ;
263308 }
264- requireQueryService ();
265- return queryService . listEdges ( kind , safeLimit , offset );
309+ throw new ResponseStatusException ( HttpStatus . SERVICE_UNAVAILABLE ,
310+ "No data available. Run analyze first." );
266311 }
267312
268313 @ GetMapping ("/ego/{center}" )
@@ -329,7 +374,12 @@ public ResponseEntity<?> findDeadCode(
329374 @ RequestParam (defaultValue = "100" ) int limit ) {
330375 int safeLimit = Math .min (limit , 1000 );
331376
332- // Try H2 cache first for dead code analysis
377+ // Try Neo4j first
378+ if (useNeo4j ()) {
379+ return ResponseEntity .ok (queryService .findDeadCode (kind , safeLimit ));
380+ }
381+
382+ // Fall back to H2 cache
333383 ensureCacheLoaded ();
334384 if (cachedNodes != null && cachedEdges != null ) {
335385 List <CodeNode > candidates = cachedNodes ;
@@ -368,10 +418,6 @@ public ResponseEntity<?> findDeadCode(
368418 return ResponseEntity .ok (result );
369419 }
370420
371- // Fall back to QueryService (Neo4j)
372- if (queryService != null ) {
373- return ResponseEntity .ok (queryService .findDeadCode (kind , safeLimit ));
374- }
375421 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
376422 "No data available. Run analyze first." );
377423 }
@@ -396,16 +442,29 @@ public List<Map<String, Object>> searchGraph(
396442 @ RequestParam String q ,
397443 @ RequestParam (defaultValue = "50" ) int limit ) {
398444 int safeLimit = Math .min (limit , 1000 );
399- // Search from H2 cache
445+ // Search via Neo4j first
446+ if (useNeo4j ()) {
447+ return queryService .searchGraph (q , safeLimit );
448+ }
449+ // Fall back to H2 cache
400450 ensureCacheLoaded ();
401451 if (cachedNodes != null ) {
402452 return topologyService .findNode (q , cachedNodes );
403453 }
404- if (queryService != null ) return queryService .searchGraph (q , safeLimit );
405454 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
406455 "No data available. Run analyze first." );
407456 }
408457
458+ /**
459+ * Check whether Neo4j (via QueryService/GraphStore) is available for queries.
460+ * When true, Neo4j is the primary data source (enriched graph with SERVICE
461+ * nodes, layer classifications, linker edges).
462+ * When false, falls back to H2 cache (basic indexed data only).
463+ */
464+ private boolean useNeo4j () {
465+ return queryService != null ;
466+ }
467+
409468 private void requireQueryService () {
410469 if (queryService == null ) {
411470 throw new ResponseStatusException (HttpStatus .SERVICE_UNAVAILABLE ,
0 commit comments