Skip to main content

feagi_api/endpoints/
connectome.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Connectome API Endpoints - Exact port from Python `/v1/connectome/*`
5
6// Removed - using crate::common::State instead
7use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
9use crate::endpoints::cortical_area::synapse_details_for_neuron;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use tracing::warn;
13use utoipa::{IntoParams, ToSchema};
14
15/// Query for [`get_memory_neuron`].
16#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
17#[into_params(parameter_in = Query)]
18pub struct MemoryNeuronQuery {
19    /// Global memory neuron id (reserved range, typically 50_000_000+).
20    pub neuron_id: u32,
21}
22
23/// Full memory neuron detail: plasticity lifecycle fields plus connectome synapses.
24#[derive(Debug, Clone, Serialize, ToSchema)]
25pub struct MemoryNeuronDetailResponse {
26    pub neuron_id: u64,
27    pub cortical_idx: u32,
28    pub cortical_id: String,
29    pub cortical_name: String,
30    pub pattern_hash: Option<u64>,
31    pub is_longterm_memory: bool,
32    pub is_active: bool,
33    pub lifespan_current: u32,
34    pub lifespan_initial: u32,
35    pub lifespan_growth_rate: f32,
36    pub creation_burst: u64,
37    pub last_activation_burst: u64,
38    pub activation_count: u32,
39    pub outgoing_synapse_count: usize,
40    pub incoming_synapse_count: usize,
41    pub outgoing_synapses: serde_json::Value,
42    pub incoming_synapses: serde_json::Value,
43}
44
45/// GET /v1/connectome/cortical_areas/list/detailed
46#[utoipa::path(
47    get,
48    path = "/v1/connectome/cortical_areas/list/detailed",
49    tag = "connectome",
50    responses(
51        (status = 200, description = "Detailed cortical areas list", body = HashMap<String, serde_json::Value>),
52        (status = 500, description = "Internal server error")
53    )
54)]
55pub async fn get_cortical_areas_list_detailed(
56    State(state): State<ApiState>,
57) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
58    let connectome_service = state.connectome_service.as_ref();
59    match connectome_service.list_cortical_areas().await {
60        Ok(areas) => {
61            tracing::info!(target: "feagi-api",
62                "[DETAILED-LIST] Returning {} cortical areas", areas.len()
63            );
64
65            let detailed: HashMap<String, serde_json::Value> = areas
66                .into_iter()
67                .map(|area| {
68                    tracing::debug!(target: "feagi-api",
69                        "[DETAILED-LIST] Area {}: cortical_type='{}', is_mem_type={:?}",
70                        area.cortical_id, area.cortical_type,
71                        area.properties.get("is_mem_type")
72                    );
73
74                    let json_value = serde_json::to_value(&area).unwrap_or_default();
75
76                    tracing::debug!(target: "feagi-api",
77                        "[DETAILED-LIST] Serialized area {} has cortical_type: {}",
78                        area.cortical_id, json_value.get("cortical_type").is_some()
79                    );
80
81                    (area.cortical_id.clone(), json_value)
82                })
83                .collect();
84            Ok(Json(detailed))
85        }
86        Err(e) => Err(ApiError::internal(format!(
87            "Failed to get detailed list: {}",
88            e
89        ))),
90    }
91}
92
93/// GET /v1/connectome/properties/dimensions
94#[utoipa::path(get, path = "/v1/connectome/properties/dimensions", tag = "connectome")]
95pub async fn get_properties_dimensions(
96    State(_state): State<ApiState>,
97) -> ApiResult<Json<(usize, usize, usize)>> {
98    // Will use state when wired to NPU
99    // TODO: Get max dimensions from connectome manager
100    Ok(Json((0, 0, 0)))
101}
102
103/// GET /v1/connectome/properties/mappings
104#[utoipa::path(get, path = "/v1/connectome/properties/mappings", tag = "connectome")]
105pub async fn get_properties_mappings(
106    State(_state): State<ApiState>,
107) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
108    // TODO: Get all cortical mappings
109    Ok(Json(HashMap::new()))
110}
111
112/// GET /v1/connectome/snapshot
113#[utoipa::path(get, path = "/v1/connectome/snapshot", tag = "connectome", responses((status = 200, body = HashMap<String, serde_json::Value>)))]
114pub async fn get_snapshot(
115    State(state): State<ApiState>,
116) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
117    let connectome_service = state.connectome_service.as_ref();
118    let areas = connectome_service
119        .list_cortical_areas()
120        .await
121        .map_err(|e| ApiError::internal(format!("{}", e)))?;
122    let regions = connectome_service
123        .list_brain_regions()
124        .await
125        .map_err(|e| ApiError::internal(format!("{}", e)))?;
126    let mut response = HashMap::new();
127    response.insert(
128        "cortical_area_count".to_string(),
129        serde_json::json!(areas.len()),
130    );
131    response.insert(
132        "brain_region_count".to_string(),
133        serde_json::json!(regions.len()),
134    );
135    Ok(Json(response))
136}
137
138/// GET /v1/connectome/stats
139#[utoipa::path(get, path = "/v1/connectome/stats", tag = "connectome", responses((status = 200, body = HashMap<String, serde_json::Value>)))]
140pub async fn get_stats(
141    State(state): State<ApiState>,
142) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
143    let analytics_service = state.analytics_service.as_ref();
144    let health = analytics_service
145        .get_system_health()
146        .await
147        .map_err(|e| ApiError::internal(format!("{}", e)))?;
148    let mut response = HashMap::new();
149    response.insert(
150        "neuron_count".to_string(),
151        serde_json::json!(health.neuron_count),
152    );
153    response.insert(
154        "cortical_area_count".to_string(),
155        serde_json::json!(health.cortical_area_count),
156    );
157    Ok(Json(response))
158}
159
160/// POST /v1/connectome/batch_neuron_operations
161#[utoipa::path(
162    post,
163    path = "/v1/connectome/batch_neuron_operations",
164    tag = "connectome"
165)]
166pub async fn post_batch_neuron_operations(
167    State(_state): State<ApiState>,
168    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
169) -> ApiResult<Json<HashMap<String, i32>>> {
170    let mut response = HashMap::new();
171    response.insert("processed".to_string(), 0);
172    Ok(Json(response))
173}
174
175/// POST /v1/connectome/batch_synapse_operations
176#[utoipa::path(
177    post,
178    path = "/v1/connectome/batch_synapse_operations",
179    tag = "connectome"
180)]
181pub async fn post_batch_synapse_operations(
182    State(_state): State<ApiState>,
183    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
184) -> ApiResult<Json<HashMap<String, i32>>> {
185    let mut response = HashMap::new();
186    response.insert("processed".to_string(), 0);
187    Ok(Json(response))
188}
189
190/// GET /v1/connectome/neuron_count
191#[utoipa::path(get, path = "/v1/connectome/neuron_count", tag = "connectome")]
192pub async fn get_neuron_count(State(state): State<ApiState>) -> ApiResult<Json<i64>> {
193    let analytics = state.analytics_service.as_ref();
194    let health = analytics
195        .get_system_health()
196        .await
197        .map_err(|e| ApiError::internal(format!("{}", e)))?;
198    Ok(Json(health.neuron_count as i64))
199}
200
201/// GET /v1/connectome/synapse_count
202#[utoipa::path(get, path = "/v1/connectome/synapse_count", tag = "connectome")]
203pub async fn get_synapse_count(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
204    Ok(Json(0))
205}
206
207/// GET /v1/connectome/paths
208#[utoipa::path(get, path = "/v1/connectome/paths", tag = "connectome")]
209pub async fn get_paths(
210    State(_state): State<ApiState>,
211    Query(_params): Query<HashMap<String, String>>,
212) -> ApiResult<Json<Vec<Vec<String>>>> {
213    Ok(Json(Vec::new()))
214}
215
216/// GET /v1/connectome/cumulative_stats
217#[utoipa::path(get, path = "/v1/connectome/cumulative_stats", tag = "connectome")]
218pub async fn get_cumulative_stats(
219    State(_state): State<ApiState>,
220) -> ApiResult<Json<HashMap<String, i64>>> {
221    let mut response = HashMap::new();
222    response.insert("total_bursts".to_string(), 0);
223    Ok(Json(response))
224}
225
226/// GET /v1/connectome/area_details
227#[utoipa::path(get, path = "/v1/connectome/area_details", tag = "connectome")]
228pub async fn get_area_details(
229    State(state): State<ApiState>,
230    Query(params): Query<HashMap<String, String>>,
231) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
232    let area_ids_str = params
233        .get("area_ids")
234        .ok_or_else(|| ApiError::invalid_input("area_ids required"))?;
235    let area_ids: Vec<&str> = area_ids_str.split(',').collect();
236    let connectome_service = state.connectome_service.as_ref();
237    let mut details = HashMap::new();
238    for area_id in area_ids {
239        if let Ok(area) = connectome_service.get_cortical_area(area_id).await {
240            details.insert(
241                area_id.to_string(),
242                serde_json::json!({"cortical_id": area.cortical_id}),
243            );
244        }
245    }
246    Ok(Json(details))
247}
248
249/// POST /v1/connectome/rebuild
250#[utoipa::path(post, path = "/v1/connectome/rebuild", tag = "connectome")]
251pub async fn post_rebuild(
252    State(_state): State<ApiState>,
253) -> ApiResult<Json<HashMap<String, String>>> {
254    Ok(Json(HashMap::from([(
255        "message".to_string(),
256        "Not yet implemented".to_string(),
257    )])))
258}
259
260/// GET /v1/connectome/structure
261#[utoipa::path(get, path = "/v1/connectome/structure", tag = "connectome")]
262pub async fn get_structure(
263    State(state): State<ApiState>,
264) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
265    let connectome_service = state.connectome_service.as_ref();
266    let areas = connectome_service
267        .list_cortical_areas()
268        .await
269        .map_err(|e| ApiError::internal(format!("{}", e)))?;
270    let mut response = HashMap::new();
271    response.insert("cortical_areas".to_string(), serde_json::json!(areas.len()));
272    Ok(Json(response))
273}
274
275/// POST /v1/connectome/clear
276#[utoipa::path(post, path = "/v1/connectome/clear", tag = "connectome")]
277pub async fn post_clear(
278    State(_state): State<ApiState>,
279) -> ApiResult<Json<HashMap<String, String>>> {
280    Ok(Json(HashMap::from([(
281        "message".to_string(),
282        "Not yet implemented".to_string(),
283    )])))
284}
285
286/// GET /v1/connectome/validation
287#[utoipa::path(get, path = "/v1/connectome/validation", tag = "connectome")]
288pub async fn get_validation(
289    State(_state): State<ApiState>,
290) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
291    let mut response = HashMap::new();
292    response.insert("valid".to_string(), serde_json::json!(true));
293    Ok(Json(response))
294}
295
296/// GET /v1/connectome/topology
297#[utoipa::path(get, path = "/v1/connectome/topology", tag = "connectome")]
298pub async fn get_topology(
299    State(_state): State<ApiState>,
300) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
301    let mut response = HashMap::new();
302    response.insert("layers".to_string(), serde_json::json!(0));
303    Ok(Json(response))
304}
305
306/// POST /v1/connectome/optimize
307#[utoipa::path(post, path = "/v1/connectome/optimize", tag = "connectome")]
308pub async fn post_optimize(
309    State(_state): State<ApiState>,
310) -> ApiResult<Json<HashMap<String, String>>> {
311    Ok(Json(HashMap::from([(
312        "message".to_string(),
313        "Not yet implemented".to_string(),
314    )])))
315}
316
317/// GET /v1/connectome/connectivity_matrix
318#[utoipa::path(get, path = "/v1/connectome/connectivity_matrix", tag = "connectome")]
319pub async fn get_connectivity_matrix(
320    State(_state): State<ApiState>,
321) -> ApiResult<Json<HashMap<String, Vec<Vec<i32>>>>> {
322    let mut response = HashMap::new();
323    response.insert("matrix".to_string(), Vec::new());
324    Ok(Json(response))
325}
326
327/// POST /v1/connectome/neurons/batch
328#[utoipa::path(post, path = "/v1/connectome/neurons/batch", tag = "connectome")]
329pub async fn post_neurons_batch(
330    State(_state): State<ApiState>,
331    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
332) -> ApiResult<Json<HashMap<String, i32>>> {
333    let mut response = HashMap::new();
334    response.insert("processed".to_string(), 0);
335    Ok(Json(response))
336}
337
338/// POST /v1/connectome/synapses/batch
339#[utoipa::path(post, path = "/v1/connectome/synapses/batch", tag = "connectome")]
340pub async fn post_synapses_batch(
341    State(_state): State<ApiState>,
342    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
343) -> ApiResult<Json<HashMap<String, i32>>> {
344    let mut response = HashMap::new();
345    response.insert("processed".to_string(), 0);
346    Ok(Json(response))
347}
348
349// EXACT Python path matches:
350/// GET /v1/connectome/cortical_areas/list/summary
351#[utoipa::path(
352    get,
353    path = "/v1/connectome/cortical_areas/list/summary",
354    tag = "connectome"
355)]
356pub async fn get_cortical_areas_list_summary(
357    State(state): State<ApiState>,
358) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
359    let connectome_service = state.connectome_service.as_ref();
360    let areas = connectome_service
361        .list_cortical_areas()
362        .await
363        .map_err(|e| ApiError::internal(format!("{}", e)))?;
364    let summary: Vec<HashMap<String, serde_json::Value>> = areas
365        .iter()
366        .map(|a| {
367            let mut map = HashMap::new();
368            map.insert("cortical_id".to_string(), serde_json::json!(a.cortical_id));
369            map.insert("cortical_name".to_string(), serde_json::json!(a.name));
370            map
371        })
372        .collect();
373    Ok(Json(summary))
374}
375
376/// GET /v1/connectome/cortical_areas/list/transforming
377#[utoipa::path(
378    get,
379    path = "/v1/connectome/cortical_areas/list/transforming",
380    tag = "connectome"
381)]
382pub async fn get_cortical_areas_list_transforming(
383    State(_state): State<ApiState>,
384) -> ApiResult<Json<Vec<String>>> {
385    Ok(Json(Vec::new()))
386}
387
388/// GET /v1/connectome/cortical_area/{cortical_id}/neurons
389#[utoipa::path(
390    get,
391    path = "/v1/connectome/cortical_area/{cortical_id}/neurons",
392    tag = "connectome"
393)]
394pub async fn get_cortical_area_neurons(
395    State(state): State<ApiState>,
396    Path(cortical_id): Path<String>,
397) -> ApiResult<Json<Vec<u64>>> {
398    use tracing::debug;
399
400    let neuron_service = state.neuron_service.as_ref();
401
402    // CRITICAL FIX: Query actual neurons from NPU instead of returning empty stub
403    let neurons = neuron_service
404        .list_neurons_in_area(&cortical_id, None)
405        .await
406        .map_err(|e| {
407            ApiError::internal(format!(
408                "Failed to get neurons in area {}: {}",
409                cortical_id, e
410            ))
411        })?;
412
413    let neuron_ids: Vec<u64> = neurons.iter().map(|n| n.id).collect();
414
415    debug!(target: "feagi-api", "GET /connectome/cortical_area/{}/neurons - found {} neurons", cortical_id, neuron_ids.len());
416    Ok(Json(neuron_ids))
417}
418
419/// GET /v1/connectome/{cortical_area_id}/synapses
420///
421/// **Outgoing synapses only**: edges whose **source** is a neuron in
422/// `cortical_area_id`. To list **IPU→OPU** afferent synapses, use
423/// [`get_area_synapses_incoming`] instead; destination motor areas are usually
424/// dominated by the latter.
425#[utoipa::path(
426    get,
427    path = "/v1/connectome/{cortical_area_id}/synapses",
428    tag = "connectome"
429)]
430pub async fn get_area_synapses(
431    State(state): State<ApiState>,
432    Path(area_id): Path<String>,
433) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
434    use tracing::debug;
435
436    let connectome_service = state.connectome_service.as_ref();
437    let neuron_service = state.neuron_service.as_ref();
438
439    // CRITICAL FIX: Query actual synapses from NPU instead of returning empty stub
440    // Get cortical_idx for the area
441    let area_info = connectome_service
442        .get_cortical_area(&area_id)
443        .await
444        .map_err(|_| ApiError::not_found("CorticalArea", &area_id))?;
445
446    let cortical_idx = area_info.cortical_idx;
447
448    // Get all neurons in this cortical area
449    let neurons = neuron_service
450        .list_neurons_in_area(&area_id, None)
451        .await
452        .map_err(|e| ApiError::internal(format!("Failed to get neurons: {}", e)))?;
453
454    tracing::debug!(
455        target: "feagi-api",
456        "Getting synapses for area {} (idx={}): {} neurons",
457        area_id,
458        cortical_idx,
459        neurons.len()
460    );
461
462    // Collect all outgoing synapses from neurons in this area
463    // Access NPU through ConnectomeManager singleton
464    warn!(
465        "[API] /v1/connectome/cortical_area/{}/synapses endpoint called - this acquires NPU lock!",
466        area_id
467    );
468    let manager = feagi_brain_development::ConnectomeManager::instance();
469    let manager_lock = manager.read();
470    let npu_arc = manager_lock
471        .get_npu()
472        .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
473    let lock_start = std::time::Instant::now();
474    tracing::debug!("[NPU-LOCK] CONNECTOME-API: Acquiring NPU lock for synapse queries");
475    let npu_lock = npu_arc.lock().unwrap();
476    let lock_wait = lock_start.elapsed();
477    tracing::debug!(
478        "[NPU-LOCK] CONNECTOME-API: Lock acquired (waited {:.2}ms)",
479        lock_wait.as_secs_f64() * 1000.0
480    );
481
482    let mut all_synapses = Vec::new();
483    for neuron_info in &neurons {
484        let neuron_id = neuron_info.id as u32;
485        let outgoing = npu_lock.get_outgoing_synapses(neuron_id);
486
487        for (target_id, weight, psp, synapse_type) in outgoing {
488            let mut synapse_obj = HashMap::new();
489            synapse_obj.insert("source_neuron_id".to_string(), serde_json::json!(neuron_id));
490            synapse_obj.insert("target_neuron_id".to_string(), serde_json::json!(target_id));
491            synapse_obj.insert("weight".to_string(), serde_json::json!(weight));
492            synapse_obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
493            synapse_obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
494            all_synapses.push(synapse_obj);
495        }
496    }
497
498    debug!(target: "feagi-api", "Found {} synapses from area {}", all_synapses.len(), area_id);
499    Ok(Json(all_synapses))
500}
501
502/// GET /v1/connectome/{cortical_area_id}/synapses/incoming
503///
504/// Lists **afferent** synapses whose **post-synaptic** targets are neurons in
505/// `cortical_area_id`. Complements [`get_area_synapses`], which returns only
506/// **efferent** (outgoing) edges from this area. OPUs and other destinations
507/// are primarily driven by incoming IPU→OPU plastic synapses, so the outgoing
508/// endpoint can legitimately be empty at 200 with count 0.
509#[utoipa::path(
510    get,
511    path = "/v1/connectome/{cortical_area_id}/synapses/incoming",
512    tag = "connectome"
513)]
514pub async fn get_area_synapses_incoming(
515    State(state): State<ApiState>,
516    Path(area_id): Path<String>,
517) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
518    use tracing::debug;
519
520    let connectome_service = state.connectome_service.as_ref();
521    let neuron_service = state.neuron_service.as_ref();
522
523    let _area_info = connectome_service
524        .get_cortical_area(&area_id)
525        .await
526        .map_err(|_| ApiError::not_found("CorticalArea", &area_id))?;
527
528    let neurons = neuron_service
529        .list_neurons_in_area(&area_id, None)
530        .await
531        .map_err(|e| ApiError::internal(format!("Failed to get neurons: {}", e)))?;
532
533    debug!(
534        target: "feagi-api",
535        "Getting incoming synapses for area {}: {} neurons",
536        area_id,
537        neurons.len()
538    );
539
540    warn!(
541        "[API] /v1/connectome/cortical_area/{}/synapses/incoming - acquiring NPU lock",
542        area_id
543    );
544    let manager = feagi_brain_development::ConnectomeManager::instance();
545    let manager_lock = manager.read();
546    let npu_arc = manager_lock
547        .get_npu()
548        .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
549    let npu_lock = npu_arc.lock().unwrap();
550
551    let mut all_synapses = Vec::new();
552    for neuron_info in &neurons {
553        let neuron_id = neuron_info.id as u32;
554        let incoming = npu_lock.get_incoming_synapses(neuron_id);
555
556        for (source_id, weight, psp, synapse_type) in incoming {
557            let mut synapse_obj = HashMap::new();
558            synapse_obj.insert("source_neuron_id".to_string(), serde_json::json!(source_id));
559            synapse_obj.insert("target_neuron_id".to_string(), serde_json::json!(neuron_id));
560            synapse_obj.insert("weight".to_string(), serde_json::json!(weight));
561            synapse_obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
562            synapse_obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
563            all_synapses.push(synapse_obj);
564        }
565    }
566
567    debug!(
568        target: "feagi-api",
569        "Found {} incoming synapses to area {}",
570        all_synapses.len(),
571        area_id
572    );
573    Ok(Json(all_synapses))
574}
575
576/// GET /v1/connectome/cortical_info/{cortical_area}
577#[utoipa::path(
578    get,
579    path = "/v1/connectome/cortical_info/{cortical_area}",
580    tag = "connectome"
581)]
582pub async fn get_cortical_info(
583    State(state): State<ApiState>,
584    Path(cortical_area): Path<String>,
585) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
586    let connectome_service = state.connectome_service.as_ref();
587    let area = connectome_service
588        .get_cortical_area(&cortical_area)
589        .await
590        .map_err(|e| ApiError::not_found("area", &format!("{}", e)))?;
591    let mut response = HashMap::new();
592    response.insert(
593        "cortical_id".to_string(),
594        serde_json::json!(area.cortical_id),
595    );
596    response.insert("cortical_name".to_string(), serde_json::json!(area.name));
597    Ok(Json(response))
598}
599
600/// GET /v1/connectome/stats/cortical/cumulative/{cortical_area}
601#[utoipa::path(
602    get,
603    path = "/v1/connectome/stats/cortical/cumulative/{cortical_area}",
604    tag = "connectome"
605)]
606pub async fn get_stats_cortical_cumulative(
607    State(_state): State<ApiState>,
608    Path(_area): Path<String>,
609) -> ApiResult<Json<HashMap<String, i64>>> {
610    let mut response = HashMap::new();
611    response.insert("total_fires".to_string(), 0);
612    Ok(Json(response))
613}
614
615/// GET /v1/connectome/neuron/{neuron_id}/properties
616#[utoipa::path(
617    get,
618    path = "/v1/connectome/neuron/{neuron_id}/properties",
619    tag = "connectome"
620)]
621pub async fn get_neuron_properties_by_id(
622    State(state): State<ApiState>,
623    Path(neuron_id): Path<u64>,
624) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
625    let connectome_service = state.connectome_service.as_ref();
626    let props = connectome_service
627        .get_neuron_properties(neuron_id)
628        .await
629        .map_err(ApiError::from)?;
630    Ok(Json(props))
631}
632
633/// GET /v1/connectome/neuron_properties
634#[utoipa::path(get, path = "/v1/connectome/neuron_properties", tag = "connectome")]
635pub async fn get_neuron_properties_query(
636    State(state): State<ApiState>,
637    Query(params): Query<HashMap<String, String>>,
638) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
639    let neuron_id: u64 = params
640        .get("neuron_id")
641        .ok_or_else(|| ApiError::invalid_input("neuron_id required"))?
642        .parse()
643        .map_err(|_| ApiError::invalid_input("neuron_id must be an integer"))?;
644
645    let connectome_service = state.connectome_service.as_ref();
646    let props = connectome_service
647        .get_neuron_properties(neuron_id)
648        .await
649        .map_err(ApiError::from)?;
650    Ok(Json(props))
651}
652
653#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
654#[into_params(parameter_in = Query)]
655pub struct NeuronPropertiesAtQuery {
656    /// Cortical area ID (base64 string)
657    pub cortical_id: String,
658    /// X coordinate within the cortical area
659    pub x: u32,
660    /// Y coordinate within the cortical area
661    pub y: u32,
662    /// Z coordinate within the cortical area
663    pub z: u32,
664}
665
666/// GET /v1/connectome/neuron_properties_at
667///
668/// Resolve a neuron by `(cortical_id, x, y, z)` and return its live properties/state.
669///
670/// This is intended for clients (e.g., Brain Visualizer) that do not have neuron IDs.
671#[utoipa::path(
672    get,
673    path = "/v1/connectome/neuron_properties_at",
674    tag = "connectome",
675    params(NeuronPropertiesAtQuery)
676)]
677pub async fn get_neuron_properties_at_query(
678    State(state): State<ApiState>,
679    Query(params): Query<NeuronPropertiesAtQuery>,
680) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
681    let cortical_id = params.cortical_id;
682    let x = params.x;
683    let y = params.y;
684    let z = params.z;
685
686    // Resolve cortical_idx via service layer.
687    let connectome_service = state.connectome_service.as_ref();
688    let area = connectome_service
689        .get_cortical_area(&cortical_id)
690        .await
691        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
692
693    // Resolve neuron_id via NPU coordinate lookup (fast path).
694    //
695    // IMPORTANT (Axum): handler futures must be `Send`.
696    // Do NOT hold non-Send locks/guards across `.await`.
697    let neuron_id_u32: u32 = {
698        // Note: this uses the global ConnectomeManager singleton, consistent with existing connectome endpoints.
699        let manager = feagi_brain_development::ConnectomeManager::instance();
700        let manager_lock = manager.read();
701        let npu_arc = manager_lock
702            .get_npu()
703            .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
704        let npu_lock = npu_arc.lock().unwrap();
705
706        npu_lock
707            .get_neuron_id_at_coordinate(area.cortical_idx, x, y, z)
708            .ok_or_else(|| {
709                ApiError::not_found(
710                    "Neuron",
711                    &format!("cortical_id={} x={} y={} z={}", cortical_id, x, y, z),
712                )
713            })?
714    };
715
716    let mut props = connectome_service
717        .get_neuron_properties(neuron_id_u32 as u64)
718        .await
719        .map_err(ApiError::from)?;
720
721    // Always include resolved identity fields for clients.
722    props.insert(
723        "neuron_id".to_string(),
724        serde_json::json!(neuron_id_u32 as u64),
725    );
726    props.insert("cortical_id".to_string(), serde_json::json!(cortical_id));
727    props.insert(
728        "cortical_idx".to_string(),
729        serde_json::json!(area.cortical_idx),
730    );
731
732    Ok(Json(props))
733}
734
735/// GET /v1/connectome/area_neurons
736#[utoipa::path(get, path = "/v1/connectome/area_neurons", tag = "connectome")]
737pub async fn get_area_neurons_query(
738    State(_state): State<ApiState>,
739    Query(_params): Query<HashMap<String, String>>,
740) -> ApiResult<Json<Vec<u64>>> {
741    Ok(Json(Vec::new()))
742}
743
744/// GET /v1/connectome/fire_queue/{cortical_area}
745#[utoipa::path(
746    get,
747    path = "/v1/connectome/fire_queue/{cortical_area}",
748    tag = "connectome"
749)]
750pub async fn get_fire_queue_area(
751    State(_state): State<ApiState>,
752    Path(_area): Path<String>,
753) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
754    Ok(Json(HashMap::new()))
755}
756
757/// GET /v1/connectome/plasticity
758#[utoipa::path(get, path = "/v1/connectome/plasticity", tag = "connectome")]
759pub async fn get_plasticity_info(
760    State(_state): State<ApiState>,
761) -> ApiResult<Json<HashMap<String, bool>>> {
762    let mut response = HashMap::new();
763    response.insert("enabled".to_string(), true);
764    Ok(Json(response))
765}
766
767/// GET /v1/connectome/path
768#[utoipa::path(get, path = "/v1/connectome/path", tag = "connectome")]
769pub async fn get_path_query(
770    State(_state): State<ApiState>,
771    Query(_params): Query<HashMap<String, String>>,
772) -> ApiResult<Json<Vec<Vec<String>>>> {
773    Ok(Json(Vec::new()))
774}
775
776/// GET /v1/connectome/download
777#[utoipa::path(get, path = "/v1/connectome/download", tag = "connectome")]
778pub async fn get_download_connectome(
779    State(state): State<ApiState>,
780) -> ApiResult<Json<serde_json::Value>> {
781    // Export connectome via service layer (architecture-compliant)
782    let snapshot = state
783        .connectome_service
784        .export_connectome()
785        .await
786        .map_err(ApiError::from)?;
787
788    // Serialize snapshot to JSON
789    let json_value = serde_json::to_value(&snapshot)
790        .map_err(|e| ApiError::internal(format!("Failed to serialize connectome: {}", e)))?;
791
792    Ok(Json(json_value))
793}
794
795/// GET /v1/connectome/download-cortical-area/{cortical_area}
796#[utoipa::path(
797    get,
798    path = "/v1/connectome/download-cortical-area/{cortical_area}",
799    tag = "connectome"
800)]
801pub async fn get_download_cortical_area(
802    State(_state): State<ApiState>,
803    Path(_area): Path<String>,
804) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
805    Ok(Json(HashMap::new()))
806}
807
808/// POST /v1/connectome/upload
809#[utoipa::path(post, path = "/v1/connectome/upload", tag = "connectome")]
810pub async fn post_upload_connectome(
811    State(state): State<ApiState>,
812    Json(data): Json<serde_json::Value>,
813) -> ApiResult<Json<HashMap<String, String>>> {
814    // Deserialize snapshot from JSON
815    let snapshot: feagi_npu_neural::types::connectome::ConnectomeSnapshot =
816        serde_json::from_value(data).map_err(|e| {
817            ApiError::invalid_input(format!("Invalid connectome snapshot format: {}", e))
818        })?;
819
820    // Import connectome via service layer (architecture-compliant)
821    state
822        .connectome_service
823        .import_connectome(snapshot)
824        .await
825        .map_err(ApiError::from)?;
826
827    Ok(Json(HashMap::from([(
828        "message".to_string(),
829        "Connectome imported successfully".to_string(),
830    )])))
831}
832
833/// POST /v1/connectome/upload-cortical-area
834#[utoipa::path(post, path = "/v1/connectome/upload-cortical-area", tag = "connectome")]
835pub async fn post_upload_cortical_area(
836    State(_state): State<ApiState>,
837    Json(_data): Json<HashMap<String, serde_json::Value>>,
838) -> ApiResult<Json<HashMap<String, String>>> {
839    Ok(Json(HashMap::from([(
840        "message".to_string(),
841        "Upload not yet implemented".to_string(),
842    )])))
843}
844
845/// GET /v1/connectome/cortical_area/list/types
846#[utoipa::path(
847    get,
848    path = "/v1/connectome/cortical_area/list/types",
849    tag = "connectome",
850    responses(
851        (status = 200, description = "List of cortical types with their cortical IDs and group IDs", body = HashMap<String, serde_json::Value>),
852        (status = 500, description = "Internal server error")
853    )
854)]
855pub async fn get_cortical_area_list_types(
856    State(state): State<ApiState>,
857) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
858    // Note: decode_cortical_id removed - use CorticalID methods
859    use std::collections::{HashMap, HashSet};
860
861    let connectome_service = state.connectome_service.as_ref();
862    let areas = connectome_service
863        .list_cortical_areas()
864        .await
865        .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
866
867    // Helper function to map cortical subtype to human-readable title
868    fn get_cortical_type_title(subtype: &str) -> String {
869        match subtype {
870            "svi" => "segmented vision".to_string(),
871            "mot" => "motor".to_string(),
872            "bat" => "battery".to_string(),
873            "mis" => "miscellaneous".to_string(),
874            "gaz" => "gaze control".to_string(),
875            "pow" => "power".to_string(),
876            "dea" => "death".to_string(),
877            _ => {
878                // For unknown types, capitalize first letter and add spaces
879                if !subtype.is_empty() {
880                    let mut chars = subtype.chars();
881                    let first = chars.next().unwrap().to_uppercase().collect::<String>();
882                    let rest: String = chars.collect();
883                    format!("{}{}", first, rest)
884                } else {
885                    "unknown".to_string()
886                }
887            }
888        }
889    }
890
891    // Group areas by cortical subtype
892    let mut type_map: HashMap<String, (String, Vec<String>, HashSet<u8>)> = HashMap::new();
893
894    for area in areas {
895        // Parse cortical ID from base64
896        use feagi_structures::genomic::cortical_area::CorticalID;
897        if let Ok(cortical_id_typed) = CorticalID::try_from_base_64(&area.cortical_id) {
898            // Extract subtype and group_id using CorticalID methods
899            if let Some(subtype) = cortical_id_typed.extract_subtype() {
900                let entry = type_map.entry(subtype.clone()).or_insert_with(|| {
901                    let title = get_cortical_type_title(&subtype);
902                    (title, Vec::new(), HashSet::new())
903                });
904
905                // Add cortical ID in base64 format
906                entry.1.push(area.cortical_id.clone());
907
908                // Add group_id if available
909                if let Some(group_id) = cortical_id_typed.extract_group_id() {
910                    entry.2.insert(group_id);
911                }
912            }
913        }
914    }
915
916    // Convert to response format
917    let mut response: HashMap<String, serde_json::Value> = HashMap::new();
918    for (subtype, (title, mut cortical_ids, group_ids)) in type_map {
919        // Sort cortical_ids for consistent output
920        cortical_ids.sort();
921
922        let mut group_ids_vec: Vec<u8> = group_ids.into_iter().collect();
923        group_ids_vec.sort_unstable();
924
925        response.insert(
926            subtype,
927            serde_json::json!({
928                "title": title,
929                "cortical_ids": cortical_ids,
930                "group_ids": group_ids_vec
931            }),
932        );
933    }
934
935    Ok(Json(response))
936}
937
938/// GET /v1/connectome/memory_neuron — plasticity memory-neuron record and NPU synapse lists.
939#[utoipa::path(
940    get,
941    path = "/v1/connectome/memory_neuron",
942    tag = "connectome",
943    params(MemoryNeuronQuery),
944    responses(
945        (status = 200, description = "Memory neuron detail", body = MemoryNeuronDetailResponse),
946        (status = 400, description = "Invalid neuron id"),
947        (status = 404, description = "Not found"),
948        (status = 500, description = "Internal server error")
949    )
950)]
951pub async fn get_memory_neuron(
952    State(_state): State<ApiState>,
953    Query(q): Query<MemoryNeuronQuery>,
954) -> ApiResult<Json<MemoryNeuronDetailResponse>> {
955    if !feagi_npu_plasticity::NeuronIdManager::is_memory_neuron_id(q.neuron_id) {
956        return Err(ApiError::invalid_input(format!(
957            "neuron_id must be a memory neuron id in range {}..={}",
958            feagi_npu_plasticity::MEMORY_NEURON_ID_START,
959            feagi_npu_plasticity::MEMORY_NEURON_ID_MAX
960        )));
961    }
962
963    let manager = feagi_brain_development::ConnectomeManager::instance();
964    let mgr = manager.read();
965
966    // CRITICAL: Only hold the plasticity executor mutex while reading MemoryNeuronArray.
967    // Never hold it while acquiring the NPU lock (synapse queries), or the burst thread can
968    // deadlock (NPU held → plasticity vs plasticity held → NPU).
969    let detail = {
970        let exec = mgr
971            .get_plasticity_executor()
972            .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
973        let ex = exec
974            .lock()
975            .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
976        ex.memory_neuron_detail(q.neuron_id)
977            .ok_or_else(|| ApiError::not_found("Memory neuron", &q.neuron_id.to_string()))?
978    };
979
980    let cortical_idx = detail.cortical_area_idx;
981    let (cortical_id, cortical_name) = mgr
982        .get_cortical_id(cortical_idx)
983        .and_then(|cid| {
984            mgr.get_cortical_area(cid)
985                .map(|a| (cid.as_base_64(), a.name.clone()))
986        })
987        .unwrap_or_else(|| (String::new(), String::new()));
988
989    let outgoing_full = mgr.get_outgoing_synapses(q.neuron_id as u64);
990    let incoming_full = mgr.get_incoming_synapses(q.neuron_id as u64);
991    let oc = outgoing_full.len();
992    let ic = incoming_full.len();
993    let (out_json, in_json) =
994        synapse_details_for_neuron(&mgr, q.neuron_id, &outgoing_full, &incoming_full);
995
996    Ok(Json(MemoryNeuronDetailResponse {
997        neuron_id: q.neuron_id as u64,
998        cortical_idx,
999        cortical_id,
1000        cortical_name,
1001        pattern_hash: detail.pattern_hash,
1002        is_longterm_memory: detail.is_longterm_memory,
1003        is_active: detail.is_active,
1004        lifespan_current: detail.lifespan_current,
1005        lifespan_initial: detail.lifespan_initial,
1006        lifespan_growth_rate: detail.lifespan_growth_rate,
1007        creation_burst: detail.creation_burst,
1008        last_activation_burst: detail.last_activation_burst,
1009        activation_count: detail.activation_count,
1010        outgoing_synapse_count: oc,
1011        incoming_synapse_count: ic,
1012        outgoing_synapses: out_json,
1013        incoming_synapses: in_json,
1014    }))
1015}