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#[utoipa::path(
421    get,
422    path = "/v1/connectome/{cortical_area_id}/synapses",
423    tag = "connectome"
424)]
425pub async fn get_area_synapses(
426    State(state): State<ApiState>,
427    Path(area_id): Path<String>,
428) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
429    use tracing::debug;
430
431    let connectome_service = state.connectome_service.as_ref();
432    let neuron_service = state.neuron_service.as_ref();
433
434    // CRITICAL FIX: Query actual synapses from NPU instead of returning empty stub
435    // Get cortical_idx for the area
436    let area_info = connectome_service
437        .get_cortical_area(&area_id)
438        .await
439        .map_err(|_| ApiError::not_found("CorticalArea", &area_id))?;
440
441    let cortical_idx = area_info.cortical_idx;
442
443    // Get all neurons in this cortical area
444    let neurons = neuron_service
445        .list_neurons_in_area(&area_id, None)
446        .await
447        .map_err(|e| ApiError::internal(format!("Failed to get neurons: {}", e)))?;
448
449    tracing::debug!(
450        target: "feagi-api",
451        "Getting synapses for area {} (idx={}): {} neurons",
452        area_id,
453        cortical_idx,
454        neurons.len()
455    );
456
457    // Collect all outgoing synapses from neurons in this area
458    // Access NPU through ConnectomeManager singleton
459    warn!(
460        "[API] /v1/connectome/cortical_area/{}/synapses endpoint called - this acquires NPU lock!",
461        area_id
462    );
463    let manager = feagi_brain_development::ConnectomeManager::instance();
464    let manager_lock = manager.read();
465    let npu_arc = manager_lock
466        .get_npu()
467        .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
468    let lock_start = std::time::Instant::now();
469    tracing::debug!("[NPU-LOCK] CONNECTOME-API: Acquiring NPU lock for synapse queries");
470    let npu_lock = npu_arc.lock().unwrap();
471    let lock_wait = lock_start.elapsed();
472    tracing::debug!(
473        "[NPU-LOCK] CONNECTOME-API: Lock acquired (waited {:.2}ms)",
474        lock_wait.as_secs_f64() * 1000.0
475    );
476
477    let mut all_synapses = Vec::new();
478    for neuron_info in &neurons {
479        let neuron_id = neuron_info.id as u32;
480        let outgoing = npu_lock.get_outgoing_synapses(neuron_id);
481
482        for (target_id, weight, psp, synapse_type) in outgoing {
483            let mut synapse_obj = HashMap::new();
484            synapse_obj.insert("source_neuron_id".to_string(), serde_json::json!(neuron_id));
485            synapse_obj.insert("target_neuron_id".to_string(), serde_json::json!(target_id));
486            synapse_obj.insert("weight".to_string(), serde_json::json!(weight));
487            synapse_obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
488            synapse_obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
489            all_synapses.push(synapse_obj);
490        }
491    }
492
493    debug!(target: "feagi-api", "Found {} synapses from area {}", all_synapses.len(), area_id);
494    Ok(Json(all_synapses))
495}
496
497/// GET /v1/connectome/cortical_info/{cortical_area}
498#[utoipa::path(
499    get,
500    path = "/v1/connectome/cortical_info/{cortical_area}",
501    tag = "connectome"
502)]
503pub async fn get_cortical_info(
504    State(state): State<ApiState>,
505    Path(cortical_area): Path<String>,
506) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
507    let connectome_service = state.connectome_service.as_ref();
508    let area = connectome_service
509        .get_cortical_area(&cortical_area)
510        .await
511        .map_err(|e| ApiError::not_found("area", &format!("{}", e)))?;
512    let mut response = HashMap::new();
513    response.insert(
514        "cortical_id".to_string(),
515        serde_json::json!(area.cortical_id),
516    );
517    response.insert("cortical_name".to_string(), serde_json::json!(area.name));
518    Ok(Json(response))
519}
520
521/// GET /v1/connectome/stats/cortical/cumulative/{cortical_area}
522#[utoipa::path(
523    get,
524    path = "/v1/connectome/stats/cortical/cumulative/{cortical_area}",
525    tag = "connectome"
526)]
527pub async fn get_stats_cortical_cumulative(
528    State(_state): State<ApiState>,
529    Path(_area): Path<String>,
530) -> ApiResult<Json<HashMap<String, i64>>> {
531    let mut response = HashMap::new();
532    response.insert("total_fires".to_string(), 0);
533    Ok(Json(response))
534}
535
536/// GET /v1/connectome/neuron/{neuron_id}/properties
537#[utoipa::path(
538    get,
539    path = "/v1/connectome/neuron/{neuron_id}/properties",
540    tag = "connectome"
541)]
542pub async fn get_neuron_properties_by_id(
543    State(state): State<ApiState>,
544    Path(neuron_id): Path<u64>,
545) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
546    let connectome_service = state.connectome_service.as_ref();
547    let props = connectome_service
548        .get_neuron_properties(neuron_id)
549        .await
550        .map_err(ApiError::from)?;
551    Ok(Json(props))
552}
553
554/// GET /v1/connectome/neuron_properties
555#[utoipa::path(get, path = "/v1/connectome/neuron_properties", tag = "connectome")]
556pub async fn get_neuron_properties_query(
557    State(state): State<ApiState>,
558    Query(params): Query<HashMap<String, String>>,
559) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
560    let neuron_id: u64 = params
561        .get("neuron_id")
562        .ok_or_else(|| ApiError::invalid_input("neuron_id required"))?
563        .parse()
564        .map_err(|_| ApiError::invalid_input("neuron_id must be an integer"))?;
565
566    let connectome_service = state.connectome_service.as_ref();
567    let props = connectome_service
568        .get_neuron_properties(neuron_id)
569        .await
570        .map_err(ApiError::from)?;
571    Ok(Json(props))
572}
573
574#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
575#[into_params(parameter_in = Query)]
576pub struct NeuronPropertiesAtQuery {
577    /// Cortical area ID (base64 string)
578    pub cortical_id: String,
579    /// X coordinate within the cortical area
580    pub x: u32,
581    /// Y coordinate within the cortical area
582    pub y: u32,
583    /// Z coordinate within the cortical area
584    pub z: u32,
585}
586
587/// GET /v1/connectome/neuron_properties_at
588///
589/// Resolve a neuron by `(cortical_id, x, y, z)` and return its live properties/state.
590///
591/// This is intended for clients (e.g., Brain Visualizer) that do not have neuron IDs.
592#[utoipa::path(
593    get,
594    path = "/v1/connectome/neuron_properties_at",
595    tag = "connectome",
596    params(NeuronPropertiesAtQuery)
597)]
598pub async fn get_neuron_properties_at_query(
599    State(state): State<ApiState>,
600    Query(params): Query<NeuronPropertiesAtQuery>,
601) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
602    let cortical_id = params.cortical_id;
603    let x = params.x;
604    let y = params.y;
605    let z = params.z;
606
607    // Resolve cortical_idx via service layer.
608    let connectome_service = state.connectome_service.as_ref();
609    let area = connectome_service
610        .get_cortical_area(&cortical_id)
611        .await
612        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
613
614    // Resolve neuron_id via NPU coordinate lookup (fast path).
615    //
616    // IMPORTANT (Axum): handler futures must be `Send`.
617    // Do NOT hold non-Send locks/guards across `.await`.
618    let neuron_id_u32: u32 = {
619        // Note: this uses the global ConnectomeManager singleton, consistent with existing connectome endpoints.
620        let manager = feagi_brain_development::ConnectomeManager::instance();
621        let manager_lock = manager.read();
622        let npu_arc = manager_lock
623            .get_npu()
624            .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
625        let npu_lock = npu_arc.lock().unwrap();
626
627        npu_lock
628            .get_neuron_id_at_coordinate(area.cortical_idx, x, y, z)
629            .ok_or_else(|| {
630                ApiError::not_found(
631                    "Neuron",
632                    &format!("cortical_id={} x={} y={} z={}", cortical_id, x, y, z),
633                )
634            })?
635    };
636
637    let mut props = connectome_service
638        .get_neuron_properties(neuron_id_u32 as u64)
639        .await
640        .map_err(ApiError::from)?;
641
642    // Always include resolved identity fields for clients.
643    props.insert(
644        "neuron_id".to_string(),
645        serde_json::json!(neuron_id_u32 as u64),
646    );
647    props.insert("cortical_id".to_string(), serde_json::json!(cortical_id));
648    props.insert(
649        "cortical_idx".to_string(),
650        serde_json::json!(area.cortical_idx),
651    );
652
653    Ok(Json(props))
654}
655
656/// GET /v1/connectome/area_neurons
657#[utoipa::path(get, path = "/v1/connectome/area_neurons", tag = "connectome")]
658pub async fn get_area_neurons_query(
659    State(_state): State<ApiState>,
660    Query(_params): Query<HashMap<String, String>>,
661) -> ApiResult<Json<Vec<u64>>> {
662    Ok(Json(Vec::new()))
663}
664
665/// GET /v1/connectome/fire_queue/{cortical_area}
666#[utoipa::path(
667    get,
668    path = "/v1/connectome/fire_queue/{cortical_area}",
669    tag = "connectome"
670)]
671pub async fn get_fire_queue_area(
672    State(_state): State<ApiState>,
673    Path(_area): Path<String>,
674) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
675    Ok(Json(HashMap::new()))
676}
677
678/// GET /v1/connectome/plasticity
679#[utoipa::path(get, path = "/v1/connectome/plasticity", tag = "connectome")]
680pub async fn get_plasticity_info(
681    State(_state): State<ApiState>,
682) -> ApiResult<Json<HashMap<String, bool>>> {
683    let mut response = HashMap::new();
684    response.insert("enabled".to_string(), true);
685    Ok(Json(response))
686}
687
688/// GET /v1/connectome/path
689#[utoipa::path(get, path = "/v1/connectome/path", tag = "connectome")]
690pub async fn get_path_query(
691    State(_state): State<ApiState>,
692    Query(_params): Query<HashMap<String, String>>,
693) -> ApiResult<Json<Vec<Vec<String>>>> {
694    Ok(Json(Vec::new()))
695}
696
697/// GET /v1/connectome/download
698#[utoipa::path(get, path = "/v1/connectome/download", tag = "connectome")]
699pub async fn get_download_connectome(
700    State(state): State<ApiState>,
701) -> ApiResult<Json<serde_json::Value>> {
702    // Export connectome via service layer (architecture-compliant)
703    let snapshot = state
704        .connectome_service
705        .export_connectome()
706        .await
707        .map_err(ApiError::from)?;
708
709    // Serialize snapshot to JSON
710    let json_value = serde_json::to_value(&snapshot)
711        .map_err(|e| ApiError::internal(format!("Failed to serialize connectome: {}", e)))?;
712
713    Ok(Json(json_value))
714}
715
716/// GET /v1/connectome/download-cortical-area/{cortical_area}
717#[utoipa::path(
718    get,
719    path = "/v1/connectome/download-cortical-area/{cortical_area}",
720    tag = "connectome"
721)]
722pub async fn get_download_cortical_area(
723    State(_state): State<ApiState>,
724    Path(_area): Path<String>,
725) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
726    Ok(Json(HashMap::new()))
727}
728
729/// POST /v1/connectome/upload
730#[utoipa::path(post, path = "/v1/connectome/upload", tag = "connectome")]
731pub async fn post_upload_connectome(
732    State(state): State<ApiState>,
733    Json(data): Json<serde_json::Value>,
734) -> ApiResult<Json<HashMap<String, String>>> {
735    // Deserialize snapshot from JSON
736    let snapshot: feagi_npu_neural::types::connectome::ConnectomeSnapshot =
737        serde_json::from_value(data).map_err(|e| {
738            ApiError::invalid_input(format!("Invalid connectome snapshot format: {}", e))
739        })?;
740
741    // Import connectome via service layer (architecture-compliant)
742    state
743        .connectome_service
744        .import_connectome(snapshot)
745        .await
746        .map_err(ApiError::from)?;
747
748    Ok(Json(HashMap::from([(
749        "message".to_string(),
750        "Connectome imported successfully".to_string(),
751    )])))
752}
753
754/// POST /v1/connectome/upload-cortical-area
755#[utoipa::path(post, path = "/v1/connectome/upload-cortical-area", tag = "connectome")]
756pub async fn post_upload_cortical_area(
757    State(_state): State<ApiState>,
758    Json(_data): Json<HashMap<String, serde_json::Value>>,
759) -> ApiResult<Json<HashMap<String, String>>> {
760    Ok(Json(HashMap::from([(
761        "message".to_string(),
762        "Upload not yet implemented".to_string(),
763    )])))
764}
765
766/// GET /v1/connectome/cortical_area/list/types
767#[utoipa::path(
768    get,
769    path = "/v1/connectome/cortical_area/list/types",
770    tag = "connectome",
771    responses(
772        (status = 200, description = "List of cortical types with their cortical IDs and group IDs", body = HashMap<String, serde_json::Value>),
773        (status = 500, description = "Internal server error")
774    )
775)]
776pub async fn get_cortical_area_list_types(
777    State(state): State<ApiState>,
778) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
779    // Note: decode_cortical_id removed - use CorticalID methods
780    use std::collections::{HashMap, HashSet};
781
782    let connectome_service = state.connectome_service.as_ref();
783    let areas = connectome_service
784        .list_cortical_areas()
785        .await
786        .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
787
788    // Helper function to map cortical subtype to human-readable title
789    fn get_cortical_type_title(subtype: &str) -> String {
790        match subtype {
791            "svi" => "segmented vision".to_string(),
792            "mot" => "motor".to_string(),
793            "bat" => "battery".to_string(),
794            "mis" => "miscellaneous".to_string(),
795            "gaz" => "gaze control".to_string(),
796            "pow" => "power".to_string(),
797            "dea" => "death".to_string(),
798            _ => {
799                // For unknown types, capitalize first letter and add spaces
800                if !subtype.is_empty() {
801                    let mut chars = subtype.chars();
802                    let first = chars.next().unwrap().to_uppercase().collect::<String>();
803                    let rest: String = chars.collect();
804                    format!("{}{}", first, rest)
805                } else {
806                    "unknown".to_string()
807                }
808            }
809        }
810    }
811
812    // Group areas by cortical subtype
813    let mut type_map: HashMap<String, (String, Vec<String>, HashSet<u8>)> = HashMap::new();
814
815    for area in areas {
816        // Parse cortical ID from base64
817        use feagi_structures::genomic::cortical_area::CorticalID;
818        if let Ok(cortical_id_typed) = CorticalID::try_from_base_64(&area.cortical_id) {
819            // Extract subtype and group_id using CorticalID methods
820            if let Some(subtype) = cortical_id_typed.extract_subtype() {
821                let entry = type_map.entry(subtype.clone()).or_insert_with(|| {
822                    let title = get_cortical_type_title(&subtype);
823                    (title, Vec::new(), HashSet::new())
824                });
825
826                // Add cortical ID in base64 format
827                entry.1.push(area.cortical_id.clone());
828
829                // Add group_id if available
830                if let Some(group_id) = cortical_id_typed.extract_group_id() {
831                    entry.2.insert(group_id);
832                }
833            }
834        }
835    }
836
837    // Convert to response format
838    let mut response: HashMap<String, serde_json::Value> = HashMap::new();
839    for (subtype, (title, mut cortical_ids, group_ids)) in type_map {
840        // Sort cortical_ids for consistent output
841        cortical_ids.sort();
842
843        let mut group_ids_vec: Vec<u8> = group_ids.into_iter().collect();
844        group_ids_vec.sort_unstable();
845
846        response.insert(
847            subtype,
848            serde_json::json!({
849                "title": title,
850                "cortical_ids": cortical_ids,
851                "group_ids": group_ids_vec
852            }),
853        );
854    }
855
856    Ok(Json(response))
857}
858
859/// GET /v1/connectome/memory_neuron — plasticity memory-neuron record and NPU synapse lists.
860#[utoipa::path(
861    get,
862    path = "/v1/connectome/memory_neuron",
863    tag = "connectome",
864    params(MemoryNeuronQuery),
865    responses(
866        (status = 200, description = "Memory neuron detail", body = MemoryNeuronDetailResponse),
867        (status = 400, description = "Invalid neuron id"),
868        (status = 404, description = "Not found"),
869        (status = 500, description = "Internal server error")
870    )
871)]
872pub async fn get_memory_neuron(
873    State(_state): State<ApiState>,
874    Query(q): Query<MemoryNeuronQuery>,
875) -> ApiResult<Json<MemoryNeuronDetailResponse>> {
876    if !feagi_npu_plasticity::NeuronIdManager::is_memory_neuron_id(q.neuron_id) {
877        return Err(ApiError::invalid_input(format!(
878            "neuron_id must be a memory neuron id in range {}..={}",
879            feagi_npu_plasticity::MEMORY_NEURON_ID_START,
880            feagi_npu_plasticity::MEMORY_NEURON_ID_MAX
881        )));
882    }
883
884    let manager = feagi_brain_development::ConnectomeManager::instance();
885    let mgr = manager.read();
886
887    // CRITICAL: Only hold the plasticity executor mutex while reading MemoryNeuronArray.
888    // Never hold it while acquiring the NPU lock (synapse queries), or the burst thread can
889    // deadlock (NPU held → plasticity vs plasticity held → NPU).
890    let detail = {
891        let exec = mgr
892            .get_plasticity_executor()
893            .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
894        let ex = exec
895            .lock()
896            .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
897        ex.memory_neuron_detail(q.neuron_id)
898            .ok_or_else(|| ApiError::not_found("Memory neuron", &q.neuron_id.to_string()))?
899    };
900
901    let cortical_idx = detail.cortical_area_idx;
902    let (cortical_id, cortical_name) = mgr
903        .get_cortical_id(cortical_idx)
904        .and_then(|cid| {
905            mgr.get_cortical_area(cid)
906                .map(|a| (cid.as_base_64(), a.name.clone()))
907        })
908        .unwrap_or_else(|| (String::new(), String::new()));
909
910    let outgoing_full = mgr.get_outgoing_synapses(q.neuron_id as u64);
911    let incoming_full = mgr.get_incoming_synapses(q.neuron_id as u64);
912    let oc = outgoing_full.len();
913    let ic = incoming_full.len();
914    let (out_json, in_json) =
915        synapse_details_for_neuron(&mgr, q.neuron_id, &outgoing_full, &incoming_full);
916
917    Ok(Json(MemoryNeuronDetailResponse {
918        neuron_id: q.neuron_id as u64,
919        cortical_idx,
920        cortical_id,
921        cortical_name,
922        pattern_hash: detail.pattern_hash,
923        is_longterm_memory: detail.is_longterm_memory,
924        is_active: detail.is_active,
925        lifespan_current: detail.lifespan_current,
926        lifespan_initial: detail.lifespan_initial,
927        lifespan_growth_rate: detail.lifespan_growth_rate,
928        creation_burst: detail.creation_burst,
929        last_activation_burst: detail.last_activation_burst,
930        activation_count: detail.activation_count,
931        outgoing_synapse_count: oc,
932        incoming_synapse_count: ic,
933        outgoing_synapses: out_json,
934        incoming_synapses: in_json,
935    }))
936}