Skip to main content

feagi_api/endpoints/
cortical_area.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Cortical Area API Endpoints - Exact port from Python `/v1/cortical_area/*`
5//!
6//! Reference: feagi-py/feagi/api/v1/cortical_area.py
7
8use base64::{engine::general_purpose, Engine as _};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::common::ApiState;
13use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
14use feagi_evolutionary::extract_memory_properties;
15use feagi_structures::genomic::cortical_area::descriptors::CorticalSubUnitIndex;
16use feagi_structures::genomic::cortical_area::CorticalID;
17use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
18use utoipa::{IntoParams, ToSchema};
19
20// ============================================================================
21// REQUEST/RESPONSE MODELS
22// ============================================================================
23
24#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
25pub struct CorticalAreaIdListResponse {
26    pub cortical_ids: Vec<String>,
27}
28
29#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
30pub struct CorticalAreaNameListResponse {
31    pub cortical_area_name_list: Vec<String>,
32}
33
34/// Body for `PUT /v1/cortical_area/reset` (matches Brain Visualizer `area_list` payload).
35#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
36pub struct CorticalAreaResetRequest {
37    pub area_list: Vec<String>,
38}
39
40/// Per-area outcome for cortical reset.
41#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
42pub struct CorticalAreaResetItem {
43    pub cortical_idx: u32,
44    pub neurons_reset: usize,
45}
46
47#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
48pub struct CorticalAreaResetResponse {
49    pub message: String,
50    pub results: Vec<CorticalAreaResetItem>,
51}
52
53#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
54pub struct UnitTopologyData {
55    pub relative_position: [i32; 3],
56    pub dimensions: [u32; 3],
57}
58
59#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
60pub struct CorticalTypeMetadata {
61    pub description: String,
62    pub encodings: Vec<String>,
63    pub formats: Vec<String>,
64    pub units: u32,
65    pub resolution: Vec<i32>,
66    pub structure: String,
67    pub unit_default_topology: HashMap<usize, UnitTopologyData>,
68}
69
70/// Maximum outgoing **or** incoming synapse detail objects per request page (100 synapse records max per neuron: 50 + 50).
71pub const VOXEL_NEURON_SYNAPSES_PER_DIRECTION_PER_PAGE: usize = 50;
72
73/// Returns `(range_start, range_end, has_more)` for a 0-based `page` over `total` items (`page_size` per page).
74fn synapse_page_window(total: usize, page: u32) -> (usize, usize, bool) {
75    let page = page as usize;
76    let page_size = VOXEL_NEURON_SYNAPSES_PER_DIRECTION_PER_PAGE;
77    let start = page.saturating_mul(page_size);
78    if start >= total {
79        return (0, 0, false);
80    }
81    let end = (start + page_size).min(total);
82    let has_more = total > end;
83    (start, end, has_more)
84}
85
86/// Query parameters for [`get_voxel_neurons`] (same coordinate space as `/v1/connectome/neuron_properties_at`).
87#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
88#[into_params(parameter_in = Query)]
89pub struct VoxelNeuronsQuery {
90    /// Cortical area ID (base64-encoded string, e.g. from genome).
91    pub cortical_id: String,
92    pub x: u32,
93    pub y: u32,
94    pub z: u32,
95    /// 0-based page for synapse detail lists: at most [`VOXEL_NEURON_SYNAPSES_PER_DIRECTION_PER_PAGE`] outgoing and the same count incoming per page.
96    #[serde(default)]
97    pub synapse_page: u32,
98}
99
100/// JSON body for [`post_voxel_neurons`] (same fields as [`VoxelNeuronsQuery`]).
101#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
102pub struct VoxelNeuronsBody {
103    pub cortical_id: String,
104    pub x: u32,
105    pub y: u32,
106    pub z: u32,
107    #[serde(default)]
108    pub synapse_page: u32,
109}
110
111/// Default page size for [`MemoryCorticalAreaQuery::page_size`].
112pub const MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_DEFAULT: u32 = 50;
113/// Maximum allowed page size for memory neuron id list pagination.
114pub const MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX: u32 = 500;
115
116fn default_memory_cortical_page_size() -> u32 {
117    MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_DEFAULT
118}
119
120/// Query parameters for [`get_memory_cortical_area`].
121#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
122#[into_params(parameter_in = Query)]
123pub struct MemoryCorticalAreaQuery {
124    /// Base64 cortical area id for a memory area.
125    pub cortical_id: String,
126    /// 0-based page index for `memory_neuron_ids`.
127    #[serde(default)]
128    pub page: u32,
129    /// Page size for `memory_neuron_ids` (clamped to [`MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX`]).
130    #[serde(default = "default_memory_cortical_page_size")]
131    pub page_size: u32,
132}
133
134/// Genome memory parameters for the cortical area (from `extract_memory_properties`).
135#[derive(Debug, Clone, Serialize, ToSchema)]
136pub struct MemoryCorticalAreaParamsResponse {
137    pub temporal_depth: u32,
138    pub longterm_mem_threshold: u32,
139    pub lifespan_growth_rate: f32,
140    pub init_lifespan: u32,
141}
142
143/// Response for [`get_memory_cortical_area`].
144#[derive(Debug, Clone, Serialize, ToSchema)]
145pub struct MemoryCorticalAreaResponse {
146    pub cortical_id: String,
147    pub cortical_idx: u32,
148    pub cortical_name: String,
149    pub short_term_neuron_count: usize,
150    pub long_term_neuron_count: usize,
151    pub memory_parameters: MemoryCorticalAreaParamsResponse,
152    /// Upstream cortical indices that feed pattern detection for this memory area.
153    pub upstream_cortical_area_indices: Vec<u32>,
154    pub upstream_cortical_area_count: usize,
155    /// Distinct temporal patterns cached in the pattern detector for this area.
156    pub upstream_pattern_cache_size: usize,
157    pub incoming_synapse_count: usize,
158    pub outgoing_synapse_count: usize,
159    pub total_memory_neuron_ids: usize,
160    pub page: u32,
161    pub page_size: u32,
162    pub memory_neuron_ids: Vec<u64>,
163    pub has_more: bool,
164}
165
166/// Per-peer cortical id (base64 string or null), genome cortical **name**, cortical index, and voxel `(x,y,z)` for the given neuron id.
167pub(crate) fn peer_cortical_voxel_fields(
168    mgr: &feagi_brain_development::ConnectomeManager,
169    peer_id: u64,
170    prefix: &str,
171) -> serde_json::Map<String, serde_json::Value> {
172    let mut m = serde_json::Map::new();
173    let peer_cortical = mgr.get_neuron_cortical_id(peer_id);
174    let cortical_id = peer_cortical.map(|c| c.as_base_64());
175    m.insert(
176        format!("{prefix}_cortical_id"),
177        cortical_id.map_or(serde_json::Value::Null, serde_json::Value::String),
178    );
179    let cortical_name = peer_cortical
180        .and_then(|cid| mgr.get_cortical_area(&cid))
181        .map(|a| a.name.clone());
182    m.insert(
183        format!("{prefix}_cortical_name"),
184        cortical_name.map_or(serde_json::Value::Null, serde_json::Value::String),
185    );
186    m.insert(
187        format!("{prefix}_cortical_idx"),
188        mgr.get_neuron_cortical_idx_opt(peer_id)
189            .map_or(serde_json::Value::Null, |v| serde_json::json!(v)),
190    );
191    let (vx, vy, vz) = mgr.get_neuron_coordinates(peer_id);
192    m.insert(format!("{prefix}_x"), serde_json::json!(vx));
193    m.insert(format!("{prefix}_y"), serde_json::json!(vy));
194    m.insert(format!("{prefix}_z"), serde_json::json!(vz));
195    m
196}
197
198/// Build JSON arrays for NPU synapse tuples, aligned with `/v1/connectome/{cortical_area_id}/synapses`,
199/// plus peer cortical id, `target_cortical_name` / `source_cortical_name` (genome name), and voxel for the **target** (outgoing) or **source** (incoming) neuron.
200pub(crate) fn synapse_details_for_neuron(
201    mgr: &feagi_brain_development::ConnectomeManager,
202    neuron_id: u32,
203    outgoing: &[(u32, f32, f32, u8)],
204    incoming: &[(u32, f32, f32, u8)],
205) -> (serde_json::Value, serde_json::Value) {
206    let outgoing_json: Vec<serde_json::Value> = outgoing
207        .iter()
208        .map(|&(target_id, weight, psp, synapse_type)| {
209            let mut obj = serde_json::Map::new();
210            obj.insert("source_neuron_id".to_string(), serde_json::json!(neuron_id));
211            obj.insert("target_neuron_id".to_string(), serde_json::json!(target_id));
212            obj.insert("weight".to_string(), serde_json::json!(weight));
213            obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
214            obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
215            obj.extend(peer_cortical_voxel_fields(mgr, target_id as u64, "target"));
216            serde_json::Value::Object(obj)
217        })
218        .collect();
219    let incoming_json: Vec<serde_json::Value> = incoming
220        .iter()
221        .map(|&(source_id, weight, psp, synapse_type)| {
222            let mut obj = serde_json::Map::new();
223            obj.insert("source_neuron_id".to_string(), serde_json::json!(source_id));
224            obj.insert("target_neuron_id".to_string(), serde_json::json!(neuron_id));
225            obj.insert("weight".to_string(), serde_json::json!(weight));
226            obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
227            obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
228            obj.extend(peer_cortical_voxel_fields(mgr, source_id as u64, "source"));
229            serde_json::Value::Object(obj)
230        })
231        .collect();
232    (
233        serde_json::Value::Array(outgoing_json),
234        serde_json::Value::Array(incoming_json),
235    )
236}
237
238/// All neurons whose 3D coordinate within the cortical area matches the requested voxel, with live properties.
239#[derive(Debug, Clone, Serialize, ToSchema)]
240pub struct VoxelNeuronsResponse {
241    pub cortical_id: String,
242    /// Genome / connectome human-readable name for this cortical area (same as `cortical_name` elsewhere).
243    pub cortical_name: String,
244    pub cortical_idx: u32,
245    /// Queried voxel within the cortical volume (same values as `x`, `y`, `z`).
246    pub voxel_coordinate: [u32; 3],
247    pub x: u32,
248    pub y: u32,
249    pub z: u32,
250    /// Echo of the requested synapse detail page (0-based).
251    pub synapse_page: u32,
252    pub neuron_count: usize,
253    pub neurons: Vec<serde_json::Value>,
254}
255
256/// Resolve every neuron at `(x, y, z)` inside `cortical_id` and attach `cortical_id` / `cortical_idx`,
257/// plus paginated `outgoing_synapses` / `incoming_synapses` (at most 50 each per `synapse_page`),
258/// full `outgoing_synapse_count` / `incoming_synapse_count`, and `*_synapses_has_more` flags.
259async fn resolve_voxel_neurons(
260    state: &ApiState,
261    cortical_id: String,
262    x: u32,
263    y: u32,
264    z: u32,
265    synapse_page: u32,
266) -> ApiResult<VoxelNeuronsResponse> {
267    let connectome_service = state.connectome_service.as_ref();
268    let area = connectome_service
269        .get_cortical_area(&cortical_id)
270        .await
271        .map_err(ApiError::from)?;
272
273    let cortical_idx = area.cortical_idx;
274    let cortical_name = area.name.clone();
275
276    let matching_ids: Vec<u32> = {
277        let manager = feagi_brain_development::ConnectomeManager::instance();
278        let manager_lock = manager.read();
279        let npu_arc = manager_lock
280            .get_npu()
281            .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
282        let npu_lock = npu_arc.lock().map_err(|_| {
283            ApiError::internal("NPU mutex poisoned; restart FEAGI or wait for burst recovery")
284        })?;
285
286        let mut ids: Vec<u32> = npu_lock
287            .get_neurons_in_cortical_area(cortical_idx)
288            .into_iter()
289            .filter(|&nid| {
290                npu_lock
291                    .get_neuron_coordinates(nid)
292                    .map(|(nx, ny, nz)| nx == x && ny == y && nz == z)
293                    .unwrap_or(false)
294            })
295            .collect();
296        ids.sort_unstable();
297        ids
298    };
299
300    let mut neurons: Vec<serde_json::Value> = Vec::with_capacity(matching_ids.len());
301    for neuron_id in &matching_ids {
302        let nid = *neuron_id;
303        let mut props = connectome_service
304            .get_neuron_properties(nid as u64)
305            .await
306            .map_err(ApiError::from)?;
307        props.insert(
308            "cortical_id".to_string(),
309            serde_json::Value::String(cortical_id.clone()),
310        );
311        props.insert("cortical_idx".to_string(), serde_json::json!(cortical_idx));
312
313        let (
314            outgoing_synapse_count,
315            incoming_synapse_count,
316            out_json,
317            in_json,
318            outgoing_synapses_has_more,
319            incoming_synapses_has_more,
320        ) = {
321            let manager = feagi_brain_development::ConnectomeManager::instance();
322            let mgr = manager.read();
323            let outgoing_full = mgr.get_outgoing_synapses(nid as u64);
324            let incoming_full = mgr.get_incoming_synapses(nid as u64);
325            let oc = outgoing_full.len();
326            let ic = incoming_full.len();
327            let (o_start, o_end, out_has_more) = synapse_page_window(oc, synapse_page);
328            let (i_start, i_end, in_has_more) = synapse_page_window(ic, synapse_page);
329            let out_slice = &outgoing_full[o_start..o_end];
330            let in_slice = &incoming_full[i_start..i_end];
331            let (out_json, in_json) = synapse_details_for_neuron(&mgr, nid, out_slice, in_slice);
332            (oc, ic, out_json, in_json, out_has_more, in_has_more)
333        };
334        props.insert("synapse_page".to_string(), serde_json::json!(synapse_page));
335        props.insert(
336            "outgoing_synapse_count".to_string(),
337            serde_json::json!(outgoing_synapse_count),
338        );
339        props.insert(
340            "incoming_synapse_count".to_string(),
341            serde_json::json!(incoming_synapse_count),
342        );
343        props.insert(
344            "outgoing_synapses_has_more".to_string(),
345            serde_json::json!(outgoing_synapses_has_more),
346        );
347        props.insert(
348            "incoming_synapses_has_more".to_string(),
349            serde_json::json!(incoming_synapses_has_more),
350        );
351        props.insert("outgoing_synapses".to_string(), out_json);
352        props.insert("incoming_synapses".to_string(), in_json);
353
354        neurons.push(serde_json::to_value(&props).map_err(|e| {
355            ApiError::internal(format!("Failed to serialize neuron properties: {}", e))
356        })?);
357    }
358
359    Ok(VoxelNeuronsResponse {
360        cortical_id,
361        cortical_name,
362        cortical_idx,
363        voxel_coordinate: [x, y, z],
364        x,
365        y,
366        z,
367        synapse_page,
368        neuron_count: neurons.len(),
369        neurons,
370    })
371}
372
373// ============================================================================
374// ENDPOINTS
375// ============================================================================
376
377/// List all IPU (Input Processing Unit) cortical area IDs. Returns IDs of all sensory cortical areas.
378#[utoipa::path(get, path = "/v1/cortical_area/ipu", tag = "cortical_area")]
379pub async fn get_ipu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
380    let connectome_service = state.connectome_service.as_ref();
381    match connectome_service.list_cortical_areas().await {
382        Ok(areas) => {
383            let ipu_areas: Vec<String> = areas
384                .into_iter()
385                .filter(|a| a.area_type == "sensory" || a.area_type == "IPU")
386                .map(|a| a.cortical_id)
387                .collect();
388            Ok(Json(ipu_areas))
389        }
390        Err(e) => Err(ApiError::internal(format!(
391            "Failed to get IPU areas: {}",
392            e
393        ))),
394    }
395}
396
397/// List all OPU (Output Processing Unit) cortical area IDs. Returns IDs of all motor cortical areas.
398#[utoipa::path(get, path = "/v1/cortical_area/opu", tag = "cortical_area")]
399pub async fn get_opu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
400    let connectome_service = state.connectome_service.as_ref();
401    match connectome_service.list_cortical_areas().await {
402        Ok(areas) => {
403            let opu_areas: Vec<String> = areas
404                .into_iter()
405                .filter(|a| a.area_type == "motor" || a.area_type == "OPU")
406                .map(|a| a.cortical_id)
407                .collect();
408            Ok(Json(opu_areas))
409        }
410        Err(e) => Err(ApiError::internal(format!(
411            "Failed to get OPU areas: {}",
412            e
413        ))),
414    }
415}
416
417/// Get a list of all cortical area IDs across the entire genome (IPU, OPU, custom, memory, and core areas).
418#[utoipa::path(
419    get,
420    path = "/v1/cortical_area/cortical_area_id_list",
421    tag = "cortical_area",
422    responses(
423        (status = 200, description = "Cortical area IDs retrieved successfully", body = CorticalAreaIdListResponse),
424        (status = 500, description = "Internal server error", body = ApiError)
425    )
426)]
427pub async fn get_cortical_area_id_list(
428    State(state): State<ApiState>,
429) -> ApiResult<Json<CorticalAreaIdListResponse>> {
430    tracing::debug!(target: "feagi-api", "🔍 GET /v1/cortical_area/cortical_area_id_list - handler called");
431    let connectome_service = state.connectome_service.as_ref();
432    match connectome_service.get_cortical_area_ids().await {
433        Ok(ids) => {
434            tracing::info!(target: "feagi-api", "✅ GET /v1/cortical_area/cortical_area_id_list - success, returning {} IDs", ids.len());
435            tracing::debug!(target: "feagi-api", "📋 Cortical area IDs: {:?}", ids.iter().take(20).collect::<Vec<_>>());
436            let response = CorticalAreaIdListResponse {
437                cortical_ids: ids.clone(),
438            };
439            match serde_json::to_string(&response) {
440                Ok(json_str) => {
441                    tracing::debug!(target: "feagi-api", "📤 Response JSON: {}", json_str);
442                }
443                Err(e) => {
444                    tracing::warn!(target: "feagi-api", "⚠️ Failed to serialize response: {}", e);
445                }
446            }
447            Ok(Json(response))
448        }
449        Err(e) => {
450            tracing::error!(target: "feagi-api", "❌ GET /v1/cortical_area/cortical_area_id_list - error: {}", e);
451            Err(ApiError::internal(format!(
452                "Failed to get cortical IDs: {}",
453                e
454            )))
455        }
456    }
457}
458
459/// Get a list of all cortical area names (human-readable labels for all cortical areas).
460#[utoipa::path(
461    get,
462    path = "/v1/cortical_area/cortical_area_name_list",
463    tag = "cortical_area",
464    responses(
465        (status = 200, description = "Cortical area names retrieved successfully", body = CorticalAreaNameListResponse),
466        (status = 500, description = "Internal server error")
467    )
468)]
469pub async fn get_cortical_area_name_list(
470    State(state): State<ApiState>,
471) -> ApiResult<Json<CorticalAreaNameListResponse>> {
472    let connectome_service = state.connectome_service.as_ref();
473    match connectome_service.list_cortical_areas().await {
474        Ok(areas) => {
475            let names: Vec<String> = areas.into_iter().map(|a| a.name).collect();
476            Ok(Json(CorticalAreaNameListResponse {
477                cortical_area_name_list: names,
478            }))
479        }
480        Err(e) => Err(ApiError::internal(format!(
481            "Failed to get cortical names: {}",
482            e
483        ))),
484    }
485}
486
487/// Get a map of cortical area IDs to their human-readable names. Returns {cortical_id: name} pairs.
488#[utoipa::path(
489    get,
490    path = "/v1/cortical_area/cortical_id_name_mapping",
491    tag = "cortical_area"
492)]
493pub async fn get_cortical_id_name_mapping(
494    State(state): State<ApiState>,
495) -> ApiResult<Json<HashMap<String, String>>> {
496    let connectome_service = state.connectome_service.as_ref();
497    let ids = connectome_service
498        .get_cortical_area_ids()
499        .await
500        .map_err(|e| ApiError::internal(format!("Failed to get IDs: {}", e)))?;
501
502    let mut mapping = HashMap::new();
503    for id in ids {
504        if let Ok(area) = connectome_service.get_cortical_area(&id).await {
505            mapping.insert(id, area.name);
506        }
507    }
508    Ok(Json(mapping))
509}
510
511/// Get available cortical area types: sensory, motor, memory, and custom.
512#[utoipa::path(get, path = "/v1/cortical_area/cortical_types", tag = "cortical_area")]
513pub async fn get_cortical_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
514    Ok(Json(vec![
515        "sensory".to_string(),
516        "motor".to_string(),
517        "memory".to_string(),
518        "custom".to_string(),
519    ]))
520}
521
522/// Get detailed cortical connectivity mappings showing source-to-destination connections with mapping rules.
523#[utoipa::path(
524    get,
525    path = "/v1/cortical_area/cortical_map_detailed",
526    tag = "cortical_area",
527    responses(
528        (status = 200, description = "Detailed cortical area mapping data", body = HashMap<String, serde_json::Value>),
529        (status = 500, description = "Internal server error")
530    )
531)]
532pub async fn get_cortical_map_detailed(
533    State(state): State<ApiState>,
534) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
535    let connectome_service = state.connectome_service.as_ref();
536    match connectome_service.list_cortical_areas().await {
537        Ok(areas) => {
538            let mut map: HashMap<String, serde_json::Value> = HashMap::new();
539
540            for area in areas {
541                // Extract cortical_mapping_dst from area properties
542                if let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") {
543                    if !cortical_mapping_dst.is_null()
544                        && cortical_mapping_dst
545                            .as_object()
546                            .is_some_and(|obj| !obj.is_empty())
547                    {
548                        map.insert(area.cortical_id.clone(), cortical_mapping_dst.clone());
549                    }
550                }
551            }
552
553            Ok(Json(map))
554        }
555        Err(e) => Err(ApiError::internal(format!(
556            "Failed to get detailed map: {}",
557            e
558        ))),
559    }
560}
561
562/// Get 2D positions of all cortical areas for visualization. Returns {cortical_id: (x, y)} coordinates.
563#[utoipa::path(
564    get,
565    path = "/v1/cortical_area/cortical_locations_2d",
566    tag = "cortical_area"
567)]
568pub async fn get_cortical_locations_2d(
569    State(state): State<ApiState>,
570) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
571    let connectome_service = state.connectome_service.as_ref();
572    match connectome_service.list_cortical_areas().await {
573        Ok(areas) => {
574            let locations: HashMap<String, (i32, i32)> = areas
575                .into_iter()
576                .map(|area| (area.cortical_id, (area.position.0, area.position.1)))
577                .collect();
578            Ok(Json(locations))
579        }
580        Err(e) => Err(ApiError::internal(format!(
581            "Failed to get 2D locations: {}",
582            e
583        ))),
584    }
585}
586
587/// Get complete cortical area data including geometry, neural parameters, and metadata. Used by Brain Visualizer.
588#[utoipa::path(
589    get,
590    path = "/v1/cortical_area/cortical_area/geometry",
591    tag = "cortical_area"
592)]
593pub async fn get_cortical_area_geometry(
594    State(state): State<ApiState>,
595) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
596    let connectome_service = state.connectome_service.as_ref();
597    match connectome_service.list_cortical_areas().await {
598        Ok(areas) => {
599            let geometry: HashMap<String, serde_json::Value> = areas.into_iter()
600                .map(|area| {
601                    // Return FULL cortical area data (matching Python format)
602                    // This is what Brain Visualizer expects for genome loading
603                    let coordinate_2d = area
604                        .properties
605                        .get("coordinate_2d")
606                        .or_else(|| area.properties.get("coordinates_2d"))
607                        .cloned()
608                        .unwrap_or_else(|| serde_json::json!([0, 0]));
609                    let data = serde_json::json!({
610                        "cortical_id": area.cortical_id,
611                        "cortical_name": area.name,
612                        "cortical_group": area.cortical_group,
613                        "cortical_type": area.cortical_type,  // NEW: Explicitly include cortical_type for BV
614                        "cortical_sub_group": area.sub_group.as_ref().unwrap_or(&String::new()),  // Return empty string instead of null
615                        "coordinates_3d": [area.position.0, area.position.1, area.position.2],
616                        "coordinates_2d": coordinate_2d,
617                        "cortical_dimensions": [area.dimensions.0, area.dimensions.1, area.dimensions.2],
618                        "cortical_neuron_per_vox_count": area.neurons_per_voxel,
619                        "visualization": area.visible,
620                        "visible": area.visible,
621                        // Also include dictionary-style for backward compatibility
622                        "dimensions": {
623                            "x": area.dimensions.0,
624                            "y": area.dimensions.1,
625                            "z": area.dimensions.2
626                        },
627                        "position": {
628                            "x": area.position.0,
629                            "y": area.position.1,
630                            "z": area.position.2
631                        },
632                        // Neural parameters
633                        "neuron_post_synaptic_potential": area.postsynaptic_current,
634                        // BV expects firing threshold and threshold limit as separate fields.
635                        "neuron_fire_threshold": area.firing_threshold,
636                        "neuron_firing_threshold_limit": area.firing_threshold_limit,
637                        "plasticity_constant": area.plasticity_constant,
638                        "degeneration": area.degeneration,
639                        "leak_coefficient": area.leak_coefficient,
640                        "refractory_period": area.refractory_period,
641                        "snooze_period": area.snooze_period,
642                        // Parent region ID (required by Brain Visualizer)
643                        "parent_region_id": area.parent_region_id,
644                        // Visualization voxel granularity for large-area rendering (optional)
645                        "visualization_voxel_granularity": area.visualization_voxel_granularity.map(|(x, y, z)| serde_json::json!([x, y, z])),
646                    });
647                    (area.cortical_id.clone(), data)
648                })
649                .collect();
650            Ok(Json(geometry))
651        }
652        Err(e) => Err(ApiError::internal(format!("Failed to get geometry: {}", e))),
653    }
654}
655
656/// Get visibility status of all cortical areas. Returns {cortical_id: visibility_flag}.
657#[utoipa::path(
658    get,
659    path = "/v1/cortical_area/cortical_visibility",
660    tag = "cortical_area"
661)]
662pub async fn get_cortical_visibility(
663    State(state): State<ApiState>,
664) -> ApiResult<Json<HashMap<String, bool>>> {
665    let connectome_service = state.connectome_service.as_ref();
666    match connectome_service.list_cortical_areas().await {
667        Ok(areas) => {
668            let visibility: HashMap<String, bool> = areas
669                .into_iter()
670                .map(|area| (area.cortical_id, area.visible))
671                .collect();
672            Ok(Json(visibility))
673        }
674        Err(e) => Err(ApiError::internal(format!(
675            "Failed to get visibility: {}",
676            e
677        ))),
678    }
679}
680
681/// Get the 2D location of a cortical area by its name. Request: {cortical_name: string}.
682#[utoipa::path(
683    post,
684    path = "/v1/cortical_area/cortical_name_location",
685    tag = "cortical_area"
686)]
687#[allow(unused_variables)] // In development
688pub async fn post_cortical_name_location(
689    State(state): State<ApiState>,
690    Json(request): Json<HashMap<String, String>>,
691) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
692    let connectome_service = state.connectome_service.as_ref();
693    let cortical_name = request
694        .get("cortical_name")
695        .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
696
697    match connectome_service.get_cortical_area(cortical_name).await {
698        Ok(area) => Ok(Json(HashMap::from([(
699            area.cortical_id,
700            (area.position.0, area.position.1),
701        )]))),
702        Err(e) => Err(ApiError::internal(format!("Failed to get location: {}", e))),
703    }
704}
705
706/// Get detailed properties of a single cortical area by ID. Request: {cortical_id: string}.
707#[utoipa::path(
708    post,
709    path = "/v1/cortical_area/cortical_area_properties",
710    tag = "cortical_area"
711)]
712#[allow(unused_variables)] // In development
713pub async fn post_cortical_area_properties(
714    State(state): State<ApiState>,
715    Json(request): Json<HashMap<String, String>>,
716) -> ApiResult<Json<serde_json::Value>> {
717    let connectome_service = state.connectome_service.as_ref();
718    let cortical_id = request
719        .get("cortical_id")
720        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
721
722    match connectome_service.get_cortical_area(cortical_id).await {
723        Ok(area_info) => {
724            tracing::debug!(target: "feagi-api", "Cortical area properties for {}: cortical_group={}, area_type={}, cortical_type={}", 
725                cortical_id, area_info.cortical_group, area_info.area_type, area_info.cortical_type);
726            tracing::info!(target: "feagi-api", "[API-RESPONSE] Returning mp_driven_psp={} for area {}", area_info.mp_driven_psp, cortical_id);
727            let json_value = serde_json::to_value(&area_info).unwrap_or_default();
728            tracing::debug!(target: "feagi-api", "Serialized JSON keys: {:?}", json_value.as_object().map(|o| o.keys().collect::<Vec<_>>()));
729            tracing::debug!(target: "feagi-api", "Serialized cortical_type value: {:?}", json_value.get("cortical_type"));
730            Ok(Json(json_value))
731        }
732        Err(e) => Err(ApiError::internal(format!(
733            "Failed to get properties: {}",
734            e
735        ))),
736    }
737}
738
739/// Get properties for multiple cortical areas. Accepts array [\"id1\", \"id2\"] or object {cortical_id_list: [...]}.
740#[utoipa::path(
741    post,
742    path = "/v1/cortical_area/multi/cortical_area_properties",
743    tag = "cortical_area"
744)]
745#[allow(unused_variables)] // In development
746pub async fn post_multi_cortical_area_properties(
747    State(state): State<ApiState>,
748    Json(request): Json<serde_json::Value>,
749) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
750    let connectome_service = state.connectome_service.as_ref();
751    let mut result = HashMap::new();
752
753    // Support both formats for backward compatibility
754    let cortical_ids: Vec<String> = if request.is_array() {
755        // Format 1: Direct array ["id1", "id2"] (Python SDK)
756        request
757            .as_array()
758            .unwrap()
759            .iter()
760            .filter_map(|v| v.as_str().map(|s| s.to_string()))
761            .collect()
762    } else if request.is_object() {
763        // Format 2: Object with cortical_id_list {"cortical_id_list": ["id1", "id2"]} (Brain Visualizer)
764        request
765            .get("cortical_id_list")
766            .and_then(|v| v.as_array())
767            .ok_or_else(|| ApiError::invalid_input("cortical_id_list required in object format"))?
768            .iter()
769            .filter_map(|v| v.as_str().map(|s| s.to_string()))
770            .collect()
771    } else {
772        return Err(ApiError::invalid_input(
773            "Request must be an array of IDs or object with cortical_id_list",
774        ));
775    };
776
777    for cortical_id in cortical_ids {
778        if let Ok(area_info) = connectome_service.get_cortical_area(&cortical_id).await {
779            tracing::trace!(target: "feagi-api",
780                "[MULTI] Area {}: cortical_type={}, cortical_group={}, is_mem_type={:?}",
781                cortical_id, area_info.cortical_type, area_info.cortical_group,
782                area_info.properties.get("is_mem_type")
783            );
784            let json_value = serde_json::to_value(&area_info).unwrap_or_default();
785            tracing::trace!(target: "feagi-api",
786                "[MULTI] Serialized has cortical_type: {}",
787                json_value.get("cortical_type").is_some()
788            );
789            result.insert(cortical_id, json_value);
790        }
791    }
792    Ok(Json(result))
793}
794
795/// Create IPU (sensory) or OPU (motor) cortical areas with proper topology and multi-unit support.
796#[utoipa::path(post, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
797#[allow(unused_variables)] // In development - parameters will be used when implemented
798pub async fn post_cortical_area(
799    State(state): State<ApiState>,
800    Json(request): Json<HashMap<String, serde_json::Value>>,
801) -> ApiResult<Json<serde_json::Value>> {
802    use feagi_services::types::CreateCorticalAreaParams;
803    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
804
805    // ARCHITECTURE: Use genome_service (proper entry point) instead of connectome_service
806    let genome_service = state.genome_service.as_ref();
807
808    // Extract required fields
809    let cortical_type_key = request
810        .get("cortical_id")
811        .and_then(|v| v.as_str())
812        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
813
814    let mut group_id = request
815        .get("group_id")
816        .and_then(|v| v.as_u64())
817        .unwrap_or(0) as u8;
818
819    let device_count = request
820        .get("device_count")
821        .and_then(|v| v.as_u64())
822        .ok_or_else(|| ApiError::invalid_input("device_count required"))?
823        as usize;
824
825    let coordinates_3d: Vec<i32> = request
826        .get("coordinates_3d")
827        .and_then(|v| v.as_array())
828        .and_then(|arr| {
829            if arr.len() == 3 {
830                Some(vec![
831                    arr[0].as_i64()? as i32,
832                    arr[1].as_i64()? as i32,
833                    arr[2].as_i64()? as i32,
834                ])
835            } else {
836                None
837            }
838        })
839        .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
840
841    let cortical_type_str = request
842        .get("cortical_type")
843        .and_then(|v| v.as_str())
844        .ok_or_else(|| ApiError::invalid_input("cortical_type required"))?;
845
846    let unit_id: Option<u8> = request
847        .get("unit_id")
848        .and_then(|v| v.as_u64())
849        .map(|value| {
850            value
851                .try_into()
852                .map_err(|_| ApiError::invalid_input("unit_id out of range"))
853        })
854        .transpose()?;
855    if let Some(unit_id) = unit_id {
856        group_id = unit_id;
857    }
858
859    // Extract neurons_per_voxel from request (default to 1 if not provided)
860    let neurons_per_voxel = request
861        .get("neurons_per_voxel")
862        .and_then(|v| v.as_u64())
863        .unwrap_or(1) as u32;
864
865    // Optional: Override per-device dimensions (especially Z for angle resolution)
866    // Format: [x, y, z] where x=joints per device, y=1, z=angle_resolution
867    // Example: [1, 1, 32] for single-joint servo with 32-angle resolution
868    let per_device_dimensions_override: Option<(usize, usize, usize)> = request
869        .get("per_device_dimensions")
870        .and_then(|v| v.as_array())
871        .and_then(|arr| {
872            if arr.len() == 3 {
873                Some((
874                    arr[0].as_u64()? as usize,
875                    arr[1].as_u64()? as usize,
876                    arr[2].as_u64()? as usize,
877                ))
878            } else {
879                None
880            }
881        });
882
883    // BREAKING CHANGE (unreleased API):
884    // `data_type_config` is now per-subunit, because some cortical units have heterogeneous
885    // subunits (e.g. Gaze: Percentage2D + Percentage).
886    //
887    // Request must provide:
888    //   data_type_configs_by_subunit: { "0": <u16>, "1": <u16>, ... }
889    let raw_configs = request
890        .get("data_type_configs_by_subunit")
891        .and_then(|v| v.as_object())
892        .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
893
894    let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
895
896    for (k, v) in raw_configs {
897        let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
898            ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
899        })?;
900        let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
901            ApiError::invalid_input("data_type_configs_by_subunit key out of range")
902        })?;
903
904        let parsed_u64 = if let Some(u) = v.as_u64() {
905            Some(u)
906        } else if let Some(i) = v.as_i64() {
907            if i >= 0 {
908                Some(i as u64)
909            } else {
910                None
911            }
912        } else if let Some(f) = v.as_f64() {
913            if f >= 0.0 {
914                Some(f.round() as u64)
915            } else {
916                None
917            }
918        } else if let Some(s) = v.as_str() {
919            s.parse::<u64>().ok()
920        } else {
921            None
922        }
923        .ok_or_else(|| {
924            ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
925        })?;
926
927        if parsed_u64 > u16::MAX as u64 {
928            return Err(ApiError::invalid_input(
929                "data_type_configs_by_subunit value exceeds u16::MAX",
930            ));
931        }
932
933        data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
934    }
935
936    tracing::info!(
937        target: "feagi-api",
938        "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
939        cortical_type_key,
940        neurons_per_voxel,
941        data_type_configs_by_subunit
942    );
943
944    // Determine number of units and get topology
945    let (num_units, unit_topology) = if cortical_type_str == "IPU" {
946        // Find the matching sensory cortical unit
947        let unit = SensoryCorticalUnit::list_all()
948            .iter()
949            .find(|u| {
950                let id_ref = u.get_cortical_id_unit_reference();
951                let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
952                key == cortical_type_key
953            })
954            .ok_or_else(|| {
955                ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
956            })?;
957
958        (
959            unit.get_number_cortical_areas(),
960            unit.get_unit_default_topology(),
961        )
962    } else if cortical_type_str == "OPU" {
963        // Find the matching motor cortical unit
964        let unit = MotorCorticalUnit::list_all()
965            .iter()
966            .find(|u| {
967                let id_ref = u.get_cortical_id_unit_reference();
968                let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
969                key == cortical_type_key
970            })
971            .ok_or_else(|| {
972                ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
973            })?;
974
975        (
976            unit.get_number_cortical_areas(),
977            unit.get_unit_default_topology(),
978        )
979    } else {
980        return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
981    };
982
983    tracing::info!(
984        "Creating {} units for cortical type: {}",
985        num_units,
986        cortical_type_key
987    );
988
989    // Build creation parameters for all units
990    let mut creation_params = Vec::new();
991    for unit_idx in 0..num_units {
992        let data_type_config = data_type_configs_by_subunit
993            .get(&(unit_idx as u8))
994            .copied()
995            .ok_or_else(|| {
996                ApiError::invalid_input(format!(
997                    "data_type_configs_by_subunit missing entry for subunit {}",
998                    unit_idx
999                ))
1000            })?;
1001
1002        // Split per-subunit data_type_config into two bytes for cortical ID
1003        let config_byte_4 = (data_type_config & 0xFF) as u8; // Lower byte
1004        let config_byte_5 = ((data_type_config >> 8) & 0xFF) as u8; // Upper byte
1005
1006        // Get per-device dimensions from topology, then scale X by device_count:
1007        // total_x = device_count * per_device_x
1008        // If per_device_dimensions_override is provided, use it instead of topology defaults
1009        let (per_device_dimensions, dimensions) = if let Some(override_dims) =
1010            per_device_dimensions_override
1011        {
1012            // Use custom per-device dimensions, scale X by device_count
1013            let total_x = override_dims.0.saturating_mul(device_count);
1014            (override_dims, (total_x, override_dims.1, override_dims.2))
1015        } else if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1016            // Use topology defaults, scale X by device_count
1017            let dims = topo.channel_dimensions_default;
1018            let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
1019            let total_x = per_device.0.saturating_mul(device_count);
1020            (per_device, (total_x, per_device.1, per_device.2))
1021        } else {
1022            ((1, 1, 1), (device_count.max(1), 1, 1)) // Fallback
1023        };
1024
1025        // Calculate position for this unit
1026        let position =
1027            if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1028                let rel_pos = topo.relative_position;
1029                (
1030                    coordinates_3d[0] + rel_pos[0],
1031                    coordinates_3d[1] + rel_pos[1],
1032                    coordinates_3d[2] + rel_pos[2],
1033                )
1034            } else {
1035                (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
1036            };
1037
1038        // Construct proper 8-byte cortical ID
1039        // Byte structure: [type(i/o), subtype[0], subtype[1], subtype[2], encoding_type, encoding_format, unit_idx, group_id]
1040        // Extract the 3-character subtype from cortical_type_key (e.g., "isvi" -> "svi")
1041        let subtype_bytes = if cortical_type_key.len() >= 4 {
1042            let subtype_str = &cortical_type_key[1..4]; // Skip the 'i' or 'o' prefix
1043            let mut bytes = [0u8; 3];
1044            for (i, c) in subtype_str.chars().take(3).enumerate() {
1045                bytes[i] = c as u8;
1046            }
1047            bytes
1048        } else {
1049            return Err(ApiError::invalid_input("Invalid cortical_type_key"));
1050        };
1051
1052        // Construct the 8-byte cortical ID
1053        let cortical_id_bytes = [
1054            if cortical_type_str == "IPU" {
1055                b'i'
1056            } else {
1057                b'o'
1058            }, // Byte 0: type
1059            subtype_bytes[0], // Byte 1: subtype[0]
1060            subtype_bytes[1], // Byte 2: subtype[1]
1061            subtype_bytes[2], // Byte 3: subtype[2]
1062            config_byte_4,    // Byte 4: data type config (lower byte)
1063            config_byte_5,    // Byte 5: data type config (upper byte)
1064            unit_idx as u8,   // Byte 6: unit index
1065            group_id,         // Byte 7: group ID
1066        ];
1067
1068        // Encode to base64 for use as cortical_id string
1069        let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1070
1071        tracing::debug!(target: "feagi-api",
1072            "  Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
1073            unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
1074            dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
1075        );
1076
1077        // Store device_count and per-device dimensions in properties for BV compatibility
1078        let mut properties = HashMap::new();
1079        properties.insert(
1080            "dev_count".to_string(),
1081            serde_json::Value::Number(serde_json::Number::from(device_count)),
1082        );
1083        properties.insert(
1084            "cortical_dimensions_per_device".to_string(),
1085            serde_json::json!([
1086                per_device_dimensions.0,
1087                per_device_dimensions.1,
1088                per_device_dimensions.2
1089            ]),
1090        );
1091
1092        let params = CreateCorticalAreaParams {
1093            cortical_id: cortical_id.clone(),
1094            name: format!("{} Unit {}", cortical_type_key, unit_idx),
1095            dimensions,
1096            position,
1097            area_type: cortical_type_str.to_string(),
1098            visible: Some(true),
1099            sub_group: None,
1100            neurons_per_voxel: Some(neurons_per_voxel),
1101            postsynaptic_current: Some(0.0),
1102            plasticity_constant: Some(0.0),
1103            degeneration: Some(0.0),
1104            psp_uniform_distribution: Some(false),
1105            firing_threshold_increment: Some(0.0),
1106            firing_threshold_limit: Some(0.0),
1107            consecutive_fire_count: Some(0),
1108            snooze_period: Some(0),
1109            refractory_period: Some(0),
1110            leak_coefficient: Some(0.0),
1111            leak_variability: Some(0.0),
1112            burst_engine_active: Some(true),
1113            properties: Some(properties),
1114        };
1115
1116        creation_params.push(params);
1117    }
1118
1119    tracing::info!(
1120        "Calling GenomeService to create {} cortical areas",
1121        creation_params.len()
1122    );
1123
1124    // ARCHITECTURE: Call genome_service.create_cortical_areas (proper flow)
1125    // This will: 1) Update runtime genome, 2) Call neuroembryogenesis, 3) Create neurons/synapses
1126    let areas_details = genome_service
1127        .create_cortical_areas(creation_params)
1128        .await
1129        .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
1130
1131    tracing::info!(
1132        "✅ Successfully created {} cortical areas via GenomeService",
1133        areas_details.len()
1134    );
1135
1136    // Serialize as JSON
1137    let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
1138
1139    // Extract cortical IDs from created areas
1140    let created_ids: Vec<String> = areas_details
1141        .iter()
1142        .map(|a| a.cortical_id.clone())
1143        .collect();
1144
1145    // Return comprehensive response
1146    let first_id = created_ids.first().cloned().unwrap_or_default();
1147    let mut response = serde_json::Map::new();
1148    response.insert(
1149        "message".to_string(),
1150        serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
1151    );
1152    response.insert(
1153        "cortical_id".to_string(),
1154        serde_json::Value::String(first_id),
1155    ); // For backward compatibility
1156    response.insert(
1157        "cortical_ids".to_string(),
1158        serde_json::Value::String(created_ids.join(", ")),
1159    );
1160    response.insert(
1161        "unit_count".to_string(),
1162        serde_json::Value::Number(created_ids.len().into()),
1163    );
1164    response.insert("areas".to_string(), areas_json); // Full details for all areas
1165
1166    Ok(Json(serde_json::Value::Object(response)))
1167}
1168
1169/// Update properties of an existing cortical area (position, dimensions, neural parameters, etc.).
1170#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
1171pub async fn put_cortical_area(
1172    State(state): State<ApiState>,
1173    Json(mut request): Json<HashMap<String, serde_json::Value>>,
1174) -> ApiResult<Json<HashMap<String, String>>> {
1175    let genome_service = state.genome_service.as_ref();
1176
1177    // Extract cortical_id
1178    let cortical_id = request
1179        .get("cortical_id")
1180        .and_then(|v| v.as_str())
1181        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
1182        .to_string();
1183
1184    tracing::debug!(
1185        target: "feagi-api",
1186        "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
1187        cortical_id,
1188        request.keys().collect::<Vec<_>>()
1189    );
1190
1191    // Remove cortical_id from changes (it's not a property to update)
1192    request.remove("cortical_id");
1193
1194    // Call GenomeService with raw changes (it handles classification and routing)
1195    match genome_service
1196        .update_cortical_area(&cortical_id, request)
1197        .await
1198    {
1199        Ok(area_info) => {
1200            let updated_id = area_info.cortical_id.clone();
1201            tracing::debug!(
1202                target: "feagi-api",
1203                "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
1204                cortical_id,
1205                updated_id
1206            );
1207            Ok(Json(HashMap::from([
1208                ("message".to_string(), "Cortical area updated".to_string()),
1209                ("cortical_id".to_string(), updated_id),
1210                ("previous_cortical_id".to_string(), cortical_id),
1211            ])))
1212        }
1213        Err(e) => {
1214            tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
1215            Err(ApiError::internal(format!("Failed to update: {}", e)))
1216        }
1217    }
1218}
1219
1220/// Delete a cortical area by ID. Removes the area and all associated neurons and synapses.
1221#[utoipa::path(
1222    delete,
1223    path = "/v1/cortical_area/cortical_area",
1224    tag = "cortical_area"
1225)]
1226#[allow(unused_variables)] // In development - parameters will be used when implemented
1227pub async fn delete_cortical_area(
1228    State(state): State<ApiState>,
1229    Json(request): Json<HashMap<String, String>>,
1230) -> ApiResult<Json<HashMap<String, String>>> {
1231    let connectome_service = state.connectome_service.as_ref();
1232    let cortical_id = request
1233        .get("cortical_id")
1234        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
1235
1236    match connectome_service.delete_cortical_area(cortical_id).await {
1237        Ok(_) => Ok(Json(HashMap::from([(
1238            "message".to_string(),
1239            "Cortical area deleted".to_string(),
1240        )]))),
1241        Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
1242    }
1243}
1244
1245/// Create a custom cortical area for internal processing with specified dimensions and position.
1246#[utoipa::path(
1247    post,
1248    path = "/v1/cortical_area/custom_cortical_area",
1249    tag = "cortical_area"
1250)]
1251pub async fn post_custom_cortical_area(
1252    State(state): State<ApiState>,
1253    Json(request): Json<HashMap<String, serde_json::Value>>,
1254) -> ApiResult<Json<HashMap<String, String>>> {
1255    use feagi_services::types::CreateCorticalAreaParams;
1256    use std::time::{SystemTime, UNIX_EPOCH};
1257
1258    // Helper: check whether BV is requesting a MEMORY cortical area (still routed through this endpoint).
1259    //
1260    // Brain Visualizer sends:
1261    //   sub_group_id: "MEMORY"
1262    //   cortical_group: "CUSTOM"
1263    //
1264    // In feagi-core, the authoritative cortical type is derived from the CorticalID prefix byte:
1265    // - b'c' => Custom
1266    // - b'm' => Memory
1267    //
1268    // So if sub_group_id indicates MEMORY, we must generate an 'm' prefixed CorticalID.
1269    let is_memory_area_requested = request
1270        .get("sub_group_id")
1271        .and_then(|v| v.as_str())
1272        .map(|s| s.eq_ignore_ascii_case("MEMORY"))
1273        .unwrap_or(false);
1274
1275    // Extract required fields from request
1276    let cortical_name = request
1277        .get("cortical_name")
1278        .and_then(|v| v.as_str())
1279        .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
1280
1281    let cortical_dimensions: Vec<u32> = request
1282        .get("cortical_dimensions")
1283        .and_then(|v| v.as_array())
1284        .and_then(|arr| {
1285            if arr.len() == 3 {
1286                Some(vec![
1287                    arr[0].as_u64()? as u32,
1288                    arr[1].as_u64()? as u32,
1289                    arr[2].as_u64()? as u32,
1290                ])
1291            } else {
1292                None
1293            }
1294        })
1295        .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
1296
1297    let coordinates_3d: Vec<i32> = request
1298        .get("coordinates_3d")
1299        .and_then(|v| v.as_array())
1300        .and_then(|arr| {
1301            if arr.len() == 3 {
1302                Some(vec![
1303                    arr[0].as_i64()? as i32,
1304                    arr[1].as_i64()? as i32,
1305                    arr[2].as_i64()? as i32,
1306                ])
1307            } else {
1308                None
1309            }
1310        })
1311        .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
1312
1313    // Custom and memory areas must belong to a brain region (circuit / sub-region), not be
1314    // created without regional membership (root is reserved for core, IPU, and OPU).
1315    let brain_region_id = request
1316        .get("brain_region_id")
1317        .and_then(|v| v.as_str())
1318        .map(str::trim)
1319        .filter(|s| !s.is_empty())
1320        .map(|s| s.to_string())
1321        .ok_or_else(|| {
1322            ApiError::invalid_input(
1323                "brain_region_id is required for custom and memory cortical areas",
1324            )
1325        })?;
1326
1327    let cortical_sub_group = request
1328        .get("cortical_sub_group")
1329        .and_then(|v| v.as_str())
1330        .filter(|s| !s.is_empty())
1331        .map(|s| s.to_string());
1332
1333    tracing::info!(target: "feagi-api",
1334        "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
1335        if is_memory_area_requested { "memory" } else { "custom" },
1336        cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
1337        coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
1338    );
1339
1340    // Generate unique cortical ID for custom cortical area
1341    // Format: [b'c', 6 random alphanumeric bytes, group_counter]
1342    // Use timestamp + counter to ensure uniqueness
1343    let timestamp = SystemTime::now()
1344        .duration_since(UNIX_EPOCH)
1345        .unwrap()
1346        .as_millis() as u64;
1347
1348    // Create 8-byte cortical ID for custom/memory area
1349    // Byte 0: 'c' for custom OR 'm' for memory (authoritative type discriminator)
1350    // Bytes 1-6: Derived from name (first 6 chars, padded with underscores)
1351    // Byte 7: Counter based on timestamp lower bits
1352    let mut cortical_id_bytes = [0u8; 8];
1353    cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
1354
1355    // Use the cortical name for bytes 1-6 (truncate or pad as needed)
1356    let name_bytes = cortical_name.as_bytes();
1357    for i in 1..7 {
1358        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1359            // Use alphanumeric ASCII only
1360            let c = name_bytes[i - 1];
1361            if c.is_ascii_alphanumeric() || c == b'_' {
1362                c
1363            } else {
1364                b'_'
1365            }
1366        } else {
1367            b'_' // Padding
1368        };
1369    }
1370
1371    // Byte 7: Use timestamp lower byte for uniqueness
1372    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1373
1374    // Encode to base64 for use as cortical_id string
1375    let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1376
1377    tracing::debug!(target: "feagi-api",
1378        "Generated cortical_id: {} (raw bytes: {:?})",
1379        cortical_id, cortical_id_bytes
1380    );
1381
1382    // parent_region_id is required (validated above) so the area is registered in the hierarchy.
1383    let mut properties = HashMap::new();
1384    properties.insert(
1385        "parent_region_id".to_string(),
1386        serde_json::Value::String(brain_region_id.clone()),
1387    );
1388
1389    // Create cortical area parameters
1390    let params = CreateCorticalAreaParams {
1391        cortical_id: cortical_id.clone(),
1392        name: cortical_name.to_string(),
1393        dimensions: (
1394            cortical_dimensions[0] as usize,
1395            cortical_dimensions[1] as usize,
1396            cortical_dimensions[2] as usize,
1397        ),
1398        position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1399        area_type: if is_memory_area_requested {
1400            "Memory".to_string()
1401        } else {
1402            "Custom".to_string()
1403        },
1404        visible: Some(true),
1405        sub_group: cortical_sub_group,
1406        neurons_per_voxel: Some(1),
1407        postsynaptic_current: None,
1408        plasticity_constant: Some(0.0),
1409        degeneration: Some(0.0),
1410        psp_uniform_distribution: Some(false),
1411        firing_threshold_increment: Some(0.0),
1412        firing_threshold_limit: Some(0.0),
1413        consecutive_fire_count: Some(0),
1414        snooze_period: Some(0),
1415        refractory_period: Some(0),
1416        leak_coefficient: Some(0.0),
1417        leak_variability: Some(0.0),
1418        burst_engine_active: Some(true),
1419        properties: Some(properties),
1420    };
1421
1422    let genome_service = state.genome_service.as_ref();
1423
1424    tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1425
1426    // Create the cortical area via GenomeService
1427    let areas_details = genome_service
1428        .create_cortical_areas(vec![params])
1429        .await
1430        .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1431
1432    let created_area = areas_details
1433        .first()
1434        .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1435
1436    tracing::info!(target: "feagi-api",
1437        "✅ Successfully created custom cortical area '{}' with ID: {}",
1438        cortical_name, created_area.cortical_id
1439    );
1440
1441    // Return response
1442    let mut response = HashMap::new();
1443    response.insert(
1444        "message".to_string(),
1445        "Custom cortical area created successfully".to_string(),
1446    );
1447    response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1448    response.insert("cortical_name".to_string(), cortical_name.to_string());
1449
1450    Ok(Json(response))
1451}
1452
1453/// Clone an existing cortical area with all its properties and structure. (Not yet implemented)
1454#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1455pub async fn post_clone(
1456    State(state): State<ApiState>,
1457    Json(request): Json<CloneCorticalAreaRequest>,
1458) -> ApiResult<Json<HashMap<String, String>>> {
1459    use base64::{engine::general_purpose, Engine as _};
1460    use feagi_services::types::CreateCorticalAreaParams;
1461    use feagi_structures::genomic::cortical_area::CorticalID;
1462    use serde_json::Value;
1463    use std::time::{SystemTime, UNIX_EPOCH};
1464
1465    let genome_service = state.genome_service.as_ref();
1466    let connectome_service = state.connectome_service.as_ref();
1467
1468    // Resolve + validate source cortical ID.
1469    let source_id = request.source_area_id.clone();
1470    let source_typed = CorticalID::try_from_base_64(&source_id)
1471        .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1472    let src_first_byte = source_typed.as_bytes()[0];
1473    if src_first_byte != b'c' && src_first_byte != b'm' {
1474        return Err(ApiError::invalid_input(format!(
1475            "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1476            src_first_byte
1477        )));
1478    }
1479
1480    // Fetch full source info (dimensions, neural params, properties, mappings).
1481    let source_area = connectome_service
1482        .get_cortical_area(&source_id)
1483        .await
1484        .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1485
1486    // FEAGI is the source of truth for brain-region membership.
1487    //
1488    // Do NOT trust the client/UI to provide parent_region_id correctly, because FEAGI already
1489    // knows the source area’s parent. We use FEAGI’s view of parent_region_id for persistence.
1490    //
1491    // If the client provides parent_region_id and it disagrees, fail fast to prevent ambiguity.
1492    let source_parent_region_id = source_area
1493        .parent_region_id
1494        .clone()
1495        .or_else(|| {
1496            source_area
1497                .properties
1498                .get("parent_region_id")
1499                .and_then(|v| v.as_str())
1500                .map(|s| s.to_string())
1501        })
1502        .ok_or_else(|| {
1503            ApiError::internal(format!(
1504                "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1505                source_id
1506            ))
1507        })?;
1508
1509    if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1510        if client_parent_region_id != &source_parent_region_id {
1511            return Err(ApiError::invalid_input(format!(
1512                "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1513                client_parent_region_id, source_id, source_parent_region_id
1514            )));
1515        }
1516    }
1517
1518    // Extract outgoing mappings (we will apply them after creation, via update_cortical_mapping).
1519    let outgoing_mapping_dst = source_area
1520        .properties
1521        .get("cortical_mapping_dst")
1522        .and_then(|v| v.as_object())
1523        .cloned();
1524
1525    // Generate unique cortical ID for the clone.
1526    //
1527    // Rules:
1528    // - Byte 0 keeps the source type discriminator (b'c' or b'm')
1529    // - Bytes 1-6 derived from new_name (alphanumeric/_ only)
1530    // - Byte 7 timestamp lower byte for uniqueness
1531    let timestamp = SystemTime::now()
1532        .duration_since(UNIX_EPOCH)
1533        .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1534        .as_millis() as u64;
1535
1536    let mut cortical_id_bytes = [0u8; 8];
1537    cortical_id_bytes[0] = src_first_byte;
1538
1539    let name_bytes = request.new_name.as_bytes();
1540    for i in 1..7 {
1541        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1542            let c = name_bytes[i - 1];
1543            if c.is_ascii_alphanumeric() || c == b'_' {
1544                c
1545            } else {
1546                b'_'
1547            }
1548        } else {
1549            b'_'
1550        };
1551    }
1552    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1553
1554    let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1555
1556    // Clone properties, but do NOT carry over cortical mapping properties directly.
1557    // Mappings must be created via update_cortical_mapping so synapses are regenerated.
1558    let mut cloned_properties = source_area.properties.clone();
1559    cloned_properties.remove("cortical_mapping_dst");
1560
1561    // Set parent region + 2D coordinate explicitly for the clone.
1562    cloned_properties.insert(
1563        "parent_region_id".to_string(),
1564        Value::String(source_parent_region_id),
1565    );
1566    cloned_properties.insert(
1567        "coordinate_2d".to_string(),
1568        serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1569    );
1570
1571    let params = CreateCorticalAreaParams {
1572        cortical_id: new_area_id.clone(),
1573        name: request.new_name.clone(),
1574        dimensions: source_area.dimensions,
1575        position: (
1576            request.coordinates_3d[0],
1577            request.coordinates_3d[1],
1578            request.coordinates_3d[2],
1579        ),
1580        area_type: source_area.area_type.clone(),
1581        visible: Some(source_area.visible),
1582        sub_group: source_area.sub_group.clone(),
1583        neurons_per_voxel: Some(source_area.neurons_per_voxel),
1584        postsynaptic_current: Some(source_area.postsynaptic_current),
1585        plasticity_constant: Some(source_area.plasticity_constant),
1586        degeneration: Some(source_area.degeneration),
1587        psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1588        // Note: FEAGI core currently accepts scalar firing_threshold_increment on create.
1589        // We preserve full source properties above; the service layer remains authoritative.
1590        firing_threshold_increment: None,
1591        firing_threshold_limit: Some(source_area.firing_threshold_limit),
1592        consecutive_fire_count: Some(source_area.consecutive_fire_count),
1593        snooze_period: Some(source_area.snooze_period),
1594        refractory_period: Some(source_area.refractory_period),
1595        leak_coefficient: Some(source_area.leak_coefficient),
1596        leak_variability: Some(source_area.leak_variability),
1597        burst_engine_active: Some(source_area.burst_engine_active),
1598        properties: Some(cloned_properties),
1599    };
1600
1601    // Create the cloned area via GenomeService (proper flow: genome update → neuroembryogenesis → NPU).
1602    let created_areas = genome_service
1603        .create_cortical_areas(vec![params])
1604        .await
1605        .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1606
1607    // DIAGNOSTIC: Log what coordinates were returned after creation
1608    if let Some(created_area) = created_areas.first() {
1609        tracing::info!(target: "feagi-api",
1610            "Clone created area {} with position {:?} (requested {:?})",
1611            new_area_id, created_area.position, request.coordinates_3d
1612        );
1613    }
1614
1615    // Optionally clone cortical mappings (AutoWiring).
1616    if request.clone_cortical_mapping {
1617        // 1) Outgoing mappings: source -> dst becomes new -> dst
1618        if let Some(dst_map) = outgoing_mapping_dst {
1619            for (dst_id, rules) in dst_map {
1620                let dst_effective = if dst_id == source_id {
1621                    // Self-loop on source should become self-loop on clone.
1622                    new_area_id.clone()
1623                } else {
1624                    dst_id.clone()
1625                };
1626
1627                let Some(rules_array) = rules.as_array() else {
1628                    return Err(ApiError::invalid_input(format!(
1629                        "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1630                        dst_id, rules
1631                    )));
1632                };
1633
1634                connectome_service
1635                    .update_cortical_mapping(
1636                        new_area_id.clone(),
1637                        dst_effective,
1638                        rules_array.clone(),
1639                    )
1640                    .await
1641                    .map_err(|e| {
1642                        ApiError::internal(format!(
1643                            "Failed to clone outgoing mapping from {}: {}",
1644                            source_id, e
1645                        ))
1646                    })?;
1647            }
1648        }
1649
1650        // 2) Incoming mappings: any src -> source becomes src -> new
1651        // We discover these by scanning all areas' cortical_mapping_dst maps.
1652        let all_areas = connectome_service
1653            .list_cortical_areas()
1654            .await
1655            .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1656
1657        for area in all_areas {
1658            // Skip the source area itself: source->* already handled by outgoing clone above.
1659            if area.cortical_id == source_id {
1660                continue;
1661            }
1662
1663            let Some(dst_map) = area
1664                .properties
1665                .get("cortical_mapping_dst")
1666                .and_then(|v| v.as_object())
1667            else {
1668                continue;
1669            };
1670
1671            let Some(rules) = dst_map.get(&source_id) else {
1672                continue;
1673            };
1674
1675            let Some(rules_array) = rules.as_array() else {
1676                return Err(ApiError::invalid_input(format!(
1677                    "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1678                    area.cortical_id, source_id, rules
1679                )));
1680            };
1681
1682            connectome_service
1683                .update_cortical_mapping(
1684                    area.cortical_id.clone(),
1685                    new_area_id.clone(),
1686                    rules_array.clone(),
1687                )
1688                .await
1689                .map_err(|e| {
1690                    ApiError::internal(format!(
1691                        "Failed to clone incoming mapping into {} from {}: {}",
1692                        source_id, area.cortical_id, e
1693                    ))
1694                })?;
1695        }
1696    }
1697
1698    Ok(Json(HashMap::from([
1699        ("message".to_string(), "Cortical area cloned".to_string()),
1700        ("new_area_id".to_string(), new_area_id),
1701    ])))
1702}
1703
1704/// Request payload for POST /v1/cortical_area/clone
1705#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1706pub struct CloneCorticalAreaRequest {
1707    /// Base64 cortical area ID to clone.
1708    pub source_area_id: String,
1709    /// New cortical area name (display name).
1710    pub new_name: String,
1711    /// New 3D coordinates for placement.
1712    pub coordinates_3d: [i32; 3],
1713    /// New 2D coordinates for visualization placement.
1714    pub coordinates_2d: [i32; 2],
1715    /// Target parent brain region ID to attach the clone under.
1716    ///
1717    /// NOTE: FEAGI does NOT rely on the client for this value; it derives the parent from the
1718    /// source area’s membership. If provided and mismatched, FEAGI rejects the request.
1719    #[serde(default)]
1720    pub parent_region_id: Option<String>,
1721    /// If true, clones cortical mappings (incoming + outgoing) to reproduce wiring.
1722    pub clone_cortical_mapping: bool,
1723}
1724
1725/// Update properties of multiple cortical areas in a single request. (Not yet implemented)
1726#[utoipa::path(
1727    put,
1728    path = "/v1/cortical_area/multi/cortical_area",
1729    tag = "cortical_area"
1730)]
1731pub async fn put_multi_cortical_area(
1732    State(state): State<ApiState>,
1733    Json(mut request): Json<HashMap<String, serde_json::Value>>,
1734) -> ApiResult<Json<HashMap<String, String>>> {
1735    let genome_service = state.genome_service.as_ref();
1736
1737    // Extract cortical_id_list
1738    let cortical_ids: Vec<String> = request
1739        .get("cortical_id_list")
1740        .and_then(|v| v.as_array())
1741        .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1742        .iter()
1743        .filter_map(|v| v.as_str().map(String::from))
1744        .collect();
1745
1746    if cortical_ids.is_empty() {
1747        return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1748    }
1749
1750    tracing::debug!(
1751        target: "feagi-api",
1752        "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1753        cortical_ids.len(),
1754        request.keys().collect::<Vec<_>>()
1755    );
1756
1757    // Remove cortical_id_list from changes (it's not a property to update)
1758    request.remove("cortical_id_list");
1759
1760    // Build shared properties (applies to all unless overridden per-id)
1761    let mut shared_properties = request.clone();
1762    for cortical_id in &cortical_ids {
1763        shared_properties.remove(cortical_id);
1764    }
1765
1766    // Update each cortical area, using per-id properties when provided
1767    for cortical_id in &cortical_ids {
1768        tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1769        let mut properties = shared_properties.clone();
1770        if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1771            for (key, value) in per_id_map {
1772                properties.insert(key.clone(), value.clone());
1773            }
1774        }
1775        match genome_service
1776            .update_cortical_area(cortical_id, properties)
1777            .await
1778        {
1779            Ok(_) => {
1780                tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1781            }
1782            Err(e) => {
1783                tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1784                return Err(ApiError::internal(format!(
1785                    "Failed to update cortical area {}: {}",
1786                    cortical_id, e
1787                )));
1788            }
1789        }
1790    }
1791
1792    Ok(Json(HashMap::from([
1793        (
1794            "message".to_string(),
1795            format!("Updated {} cortical areas", cortical_ids.len()),
1796        ),
1797        ("cortical_ids".to_string(), cortical_ids.join(", ")),
1798    ])))
1799}
1800
1801/// Delete multiple cortical areas by their IDs.
1802#[utoipa::path(
1803    delete,
1804    path = "/v1/cortical_area/multi/cortical_area",
1805    tag = "cortical_area"
1806)]
1807pub async fn delete_multi_cortical_area(
1808    State(state): State<ApiState>,
1809    Json(request): Json<Vec<String>>,
1810) -> ApiResult<Json<HashMap<String, String>>> {
1811    if request.is_empty() {
1812        return Err(ApiError::invalid_input(
1813            "Request must contain at least one cortical ID",
1814        ));
1815    }
1816
1817    let connectome_service = state.connectome_service.as_ref();
1818
1819    tracing::debug!(
1820        target: "feagi-api",
1821        "DELETE /v1/cortical_area/multi/cortical_area - deleting {} areas",
1822        request.len()
1823    );
1824
1825    for cortical_id in &request {
1826        match connectome_service.delete_cortical_area(cortical_id).await {
1827            Ok(_) => {
1828                tracing::debug!(
1829                    target: "feagi-api",
1830                    "DELETE /v1/cortical_area/multi/cortical_area - deleted {}",
1831                    cortical_id
1832                );
1833            }
1834            Err(e) => {
1835                tracing::error!(
1836                    target: "feagi-api",
1837                    "DELETE /v1/cortical_area/multi/cortical_area - failed for {}: {}",
1838                    cortical_id,
1839                    e
1840                );
1841                return Err(ApiError::internal(format!(
1842                    "Failed to delete cortical area {}: {}",
1843                    cortical_id, e
1844                )));
1845            }
1846        }
1847    }
1848
1849    Ok(Json(HashMap::from([
1850        (
1851            "message".to_string(),
1852            format!("Deleted {} cortical areas", request.len()),
1853        ),
1854        ("cortical_ids".to_string(), request.join(", ")),
1855    ])))
1856}
1857
1858/// Update the 2D visualization coordinates of a cortical area. (Not yet implemented)
1859#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1860#[allow(unused_variables)] // In development
1861pub async fn put_coord_2d(
1862    State(state): State<ApiState>,
1863    Json(request): Json<HashMap<String, serde_json::Value>>,
1864) -> ApiResult<Json<HashMap<String, String>>> {
1865    // TODO: Update 2D coordinates
1866    Err(ApiError::internal("Not yet implemented"))
1867}
1868
1869/// Hide/show cortical areas in visualizations. (Not yet implemented)
1870#[utoipa::path(
1871    put,
1872    path = "/v1/cortical_area/suppress_cortical_visibility",
1873    tag = "cortical_area"
1874)]
1875#[allow(unused_variables)] // In development
1876pub async fn put_suppress_cortical_visibility(
1877    State(state): State<ApiState>,
1878    Json(request): Json<HashMap<String, serde_json::Value>>,
1879) -> ApiResult<Json<HashMap<String, String>>> {
1880    // TODO: Suppress cortical visibility
1881    Err(ApiError::internal("Not yet implemented"))
1882}
1883
1884/// Reset runtime neural state for one or more cortical areas (membrane potential, refractory
1885/// counters, FCL candidates). Genome, connections, and parameters are unchanged.
1886#[utoipa::path(
1887    put,
1888    path = "/v1/cortical_area/reset",
1889    tag = "cortical_area",
1890    request_body = CorticalAreaResetRequest,
1891    responses(
1892        (status = 200, description = "Reset applied", body = CorticalAreaResetResponse),
1893    )
1894)]
1895pub async fn put_reset(
1896    State(state): State<ApiState>,
1897    Json(request): Json<CorticalAreaResetRequest>,
1898) -> ApiResult<Json<CorticalAreaResetResponse>> {
1899    use tracing::info;
1900
1901    if request.area_list.is_empty() {
1902        return Err(ApiError::invalid_input("area_list cannot be empty"));
1903    }
1904
1905    info!(
1906        target: "feagi-api",
1907        "[RESET] Received reset request for {} cortical areas: {:?}",
1908        request.area_list.len(),
1909        request.area_list
1910    );
1911
1912    let connectome_service = state.connectome_service.as_ref();
1913    let mut cortical_indices: Vec<u32> = Vec::with_capacity(request.area_list.len());
1914    for id in &request.area_list {
1915        let area = connectome_service
1916            .get_cortical_area(id)
1917            .await
1918            .map_err(ApiError::from)?;
1919        cortical_indices.push(area.cortical_idx);
1920        info!(
1921            target: "feagi-api",
1922            "[RESET] Resolved cortical ID '{}' to index {}",
1923            id,
1924            area.cortical_idx
1925        );
1926    }
1927
1928    info!(
1929        target: "feagi-api",
1930        "[RESET] Calling runtime service to reset indices: {:?}",
1931        cortical_indices
1932    );
1933
1934    let reset_pairs = state
1935        .runtime_service
1936        .reset_cortical_area_states(&cortical_indices)
1937        .await
1938        .map_err(ApiError::from)?;
1939
1940    let results: Vec<CorticalAreaResetItem> = reset_pairs
1941        .into_iter()
1942        .map(|(cortical_idx, neurons_reset)| {
1943            info!(
1944                target: "feagi-api",
1945                "[RESET] Cortical area {} reset: {} neurons cleared",
1946                cortical_idx,
1947                neurons_reset
1948            );
1949            CorticalAreaResetItem {
1950                cortical_idx,
1951                neurons_reset,
1952            }
1953        })
1954        .collect();
1955
1956    info!(
1957        target: "feagi-api",
1958        "[RESET] Reset complete for {} areas",
1959        results.len()
1960    );
1961
1962    Ok(Json(CorticalAreaResetResponse {
1963        message: "ok".to_string(),
1964        results,
1965    }))
1966}
1967
1968/// Check if visualization is enabled for the system.
1969#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1970pub async fn get_visualization(
1971    State(_state): State<ApiState>,
1972) -> ApiResult<Json<HashMap<String, bool>>> {
1973    let mut response = HashMap::new();
1974    response.insert("enabled".to_string(), true);
1975    Ok(Json(response))
1976}
1977
1978/// Execute multiple cortical area operations (create, update, delete) in a single batch.
1979#[utoipa::path(
1980    post,
1981    path = "/v1/cortical_area/batch_operations",
1982    tag = "cortical_area"
1983)]
1984pub async fn post_batch_operations(
1985    State(_state): State<ApiState>,
1986    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1987) -> ApiResult<Json<HashMap<String, i32>>> {
1988    let mut response = HashMap::new();
1989    response.insert("processed".to_string(), 0);
1990    Ok(Json(response))
1991}
1992
1993/// Alias for /v1/cortical_area/ipu - list all IPU cortical area IDs.
1994#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1995pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1996    get_ipu(State(state)).await
1997}
1998
1999/// Alias for /v1/cortical_area/opu - list all OPU cortical area IDs.
2000#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
2001pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
2002    get_opu(State(state)).await
2003}
2004
2005/// Update the 3D position of a cortical area. (Not yet implemented)
2006#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
2007pub async fn put_coordinates_3d(
2008    State(_state): State<ApiState>,
2009    Json(_req): Json<HashMap<String, serde_json::Value>>,
2010) -> ApiResult<Json<HashMap<String, String>>> {
2011    Ok(Json(HashMap::from([(
2012        "message".to_string(),
2013        "Not yet implemented".to_string(),
2014    )])))
2015}
2016
2017/// Delete multiple cortical areas by their IDs in a single operation.
2018#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
2019pub async fn delete_bulk(
2020    State(_state): State<ApiState>,
2021    Json(_ids): Json<Vec<String>>,
2022) -> ApiResult<Json<HashMap<String, i32>>> {
2023    let mut response = HashMap::new();
2024    response.insert("deleted_count".to_string(), 0);
2025    Ok(Json(response))
2026}
2027
2028/// Resize a cortical area by changing its dimensions. (Not yet implemented)
2029#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
2030pub async fn post_resize(
2031    State(_state): State<ApiState>,
2032    Json(_req): Json<HashMap<String, serde_json::Value>>,
2033) -> ApiResult<Json<HashMap<String, String>>> {
2034    Ok(Json(HashMap::from([(
2035        "message".to_string(),
2036        "Not yet implemented".to_string(),
2037    )])))
2038}
2039
2040/// Move a cortical area to a new position. (Not yet implemented)
2041#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
2042pub async fn post_reposition(
2043    State(_state): State<ApiState>,
2044    Json(_req): Json<HashMap<String, serde_json::Value>>,
2045) -> ApiResult<Json<HashMap<String, String>>> {
2046    Ok(Json(HashMap::from([(
2047        "message".to_string(),
2048        "Not yet implemented".to_string(),
2049    )])))
2050}
2051
2052/// List all neurons at a voxel `(x, y, z)` within a cortical area, with the same live property snapshot as `/v1/connectome/neuron_properties`.
2053#[utoipa::path(
2054    get,
2055    path = "/v1/cortical_area/voxel_neurons",
2056    tag = "cortical_area",
2057    params(VoxelNeuronsQuery),
2058    responses(
2059        (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2060        (status = 404, description = "Cortical area or neuron data not found"),
2061        (status = 500, description = "Internal server error")
2062    )
2063)]
2064pub async fn get_voxel_neurons(
2065    State(state): State<ApiState>,
2066    Query(params): Query<VoxelNeuronsQuery>,
2067) -> ApiResult<Json<VoxelNeuronsResponse>> {
2068    resolve_voxel_neurons(
2069        &state,
2070        params.cortical_id,
2071        params.x,
2072        params.y,
2073        params.z,
2074        params.synapse_page,
2075    )
2076    .await
2077    .map(Json)
2078}
2079
2080/// Same as [`get_voxel_neurons`] but accepts a JSON body (for clients that cannot use query strings).
2081#[utoipa::path(
2082    post,
2083    path = "/v1/cortical_area/voxel_neurons",
2084    tag = "cortical_area",
2085    request_body = VoxelNeuronsBody,
2086    responses(
2087        (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2088        (status = 404, description = "Cortical area or neuron data not found"),
2089        (status = 500, description = "Internal server error")
2090    )
2091)]
2092pub async fn post_voxel_neurons(
2093    State(state): State<ApiState>,
2094    Json(body): Json<VoxelNeuronsBody>,
2095) -> ApiResult<Json<VoxelNeuronsResponse>> {
2096    resolve_voxel_neurons(
2097        &state,
2098        body.cortical_id,
2099        body.x,
2100        body.y,
2101        body.z,
2102        body.synapse_page,
2103    )
2104    .await
2105    .map(Json)
2106}
2107
2108/// GET /v1/cortical_area/memory — plasticity runtime stats, genome memory parameters, upstream wiring, synapse counts, and paginated memory neuron ids.
2109#[utoipa::path(
2110    get,
2111    path = "/v1/cortical_area/memory",
2112    tag = "cortical_area",
2113    params(MemoryCorticalAreaQuery),
2114    responses(
2115        (status = 200, description = "Memory cortical area details", body = MemoryCorticalAreaResponse),
2116        (status = 400, description = "Invalid cortical id or not a memory area"),
2117        (status = 500, description = "Internal server error")
2118    )
2119)]
2120pub async fn get_memory_cortical_area(
2121    State(state): State<ApiState>,
2122    Query(params): Query<MemoryCorticalAreaQuery>,
2123) -> ApiResult<Json<MemoryCorticalAreaResponse>> {
2124    let connectome_service = state.connectome_service.as_ref();
2125    let area = connectome_service
2126        .get_cortical_area(&params.cortical_id)
2127        .await
2128        .map_err(ApiError::from)?;
2129
2130    let mem_props = extract_memory_properties(&area.properties).ok_or_else(|| {
2131        ApiError::invalid_input(
2132            "cortical area is not a memory area (expected is_mem_type memory properties)",
2133        )
2134    })?;
2135
2136    let cortical_idx = area.cortical_idx;
2137    let cortical_name = area.name.clone();
2138
2139    let cid = CorticalID::try_from_base_64(&params.cortical_id)
2140        .map_err(|e| ApiError::invalid_input(format!("Invalid cortical_id: {}", e)))?;
2141
2142    let page_size_u32 = params
2143        .page_size
2144        .clamp(1, MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX);
2145    let page_size = page_size_u32 as usize;
2146    let offset = (params.page as usize).saturating_mul(page_size);
2147
2148    let manager = feagi_brain_development::ConnectomeManager::instance();
2149    let mgr = manager.read();
2150
2151    let upstream_cortical_area_indices = mgr.get_upstream_cortical_areas(&cid);
2152    let upstream_cortical_area_count = upstream_cortical_area_indices.len();
2153
2154    let exec = mgr
2155        .get_plasticity_executor()
2156        .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
2157    let ex = exec
2158        .lock()
2159        .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
2160
2161    let runtime = ex
2162        .memory_cortical_area_runtime_info(cortical_idx)
2163        .ok_or_else(|| ApiError::internal("Plasticity service not initialized"))?;
2164
2165    let (memory_neuron_ids_u32, total_memory_neuron_ids) = ex
2166        .paginated_memory_neuron_ids_in_area(cortical_idx, offset, page_size)
2167        .unwrap_or((Vec::new(), 0));
2168
2169    let has_more = offset.saturating_add(memory_neuron_ids_u32.len()) < total_memory_neuron_ids;
2170
2171    let memory_neuron_ids: Vec<u64> = memory_neuron_ids_u32
2172        .into_iter()
2173        .map(|id| id as u64)
2174        .collect();
2175
2176    Ok(Json(MemoryCorticalAreaResponse {
2177        cortical_id: params.cortical_id,
2178        cortical_idx,
2179        cortical_name,
2180        short_term_neuron_count: runtime.short_term_neuron_count,
2181        long_term_neuron_count: runtime.long_term_neuron_count,
2182        memory_parameters: MemoryCorticalAreaParamsResponse {
2183            temporal_depth: mem_props.temporal_depth,
2184            longterm_mem_threshold: mem_props.longterm_threshold,
2185            lifespan_growth_rate: mem_props.lifespan_growth_rate,
2186            init_lifespan: mem_props.init_lifespan,
2187        },
2188        upstream_cortical_area_indices,
2189        upstream_cortical_area_count,
2190        upstream_pattern_cache_size: runtime.upstream_pattern_cache_size,
2191        incoming_synapse_count: area.incoming_synapse_count,
2192        outgoing_synapse_count: area.outgoing_synapse_count,
2193        total_memory_neuron_ids,
2194        page: params.page,
2195        page_size: page_size_u32,
2196        memory_neuron_ids,
2197        has_more,
2198    }))
2199}
2200
2201/// Get metadata for all available IPU types (vision, infrared, etc.). Includes encodings, formats, units, and topology.
2202#[utoipa::path(
2203    get,
2204    path = "/v1/cortical_area/ipu/types",
2205    tag = "cortical_area",
2206    responses(
2207        (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2208        (status = 500, description = "Internal server error")
2209    )
2210)]
2211pub async fn get_ipu_types(
2212    State(_state): State<ApiState>,
2213) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2214    let mut types = HashMap::new();
2215
2216    // Dynamically generate metadata from feagi_data_structures templates
2217    for unit in SensoryCorticalUnit::list_all() {
2218        let id_ref = unit.get_cortical_id_unit_reference();
2219        let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2220
2221        // All IPU types support both absolute and incremental encodings
2222        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2223
2224        // Determine if formats are supported based on snake_case_name
2225        // Vision and SegmentedVision use CartesianPlane (no formats)
2226        // MiscData uses Misc (no formats)
2227        // All others use Percentage-based types (have formats)
2228        let snake_name = unit.get_snake_case_name();
2229        let formats = if snake_name == "vision"
2230            || snake_name == "segmented_vision"
2231            || snake_name == "miscellaneous"
2232        {
2233            vec![]
2234        } else {
2235            vec!["linear".to_string(), "fractional".to_string()]
2236        };
2237
2238        // Default resolution based on type
2239        let resolution = if snake_name == "vision" {
2240            vec![64, 64, 1] // Vision sensors typically 64x64
2241        } else if snake_name == "segmented_vision" {
2242            vec![32, 32, 1] // Segmented vision segments are smaller
2243        } else {
2244            vec![1, 1, 1] // Most sensors are scalar (1x1x1)
2245        };
2246
2247        // Most sensors are asymmetric
2248        let structure = "asymmetric".to_string();
2249
2250        // Get unit default topology
2251        let topology_map = unit.get_unit_default_topology();
2252        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2253            .into_iter()
2254            .map(|(idx, topo)| {
2255                (
2256                    *idx as usize,
2257                    UnitTopologyData {
2258                        relative_position: topo.relative_position,
2259                        dimensions: topo.channel_dimensions_default,
2260                    },
2261                )
2262            })
2263            .collect();
2264
2265        types.insert(
2266            key,
2267            CorticalTypeMetadata {
2268                description: unit.get_friendly_name().to_string(),
2269                encodings,
2270                formats,
2271                units: unit.get_number_cortical_areas() as u32,
2272                resolution,
2273                structure,
2274                unit_default_topology,
2275            },
2276        );
2277    }
2278
2279    Ok(Json(types))
2280}
2281
2282/// Get metadata for all available OPU types (motors, servos, etc.). Includes encodings, formats, units, and topology.
2283#[utoipa::path(
2284    get,
2285    path = "/v1/cortical_area/opu/types",
2286    tag = "cortical_area",
2287    responses(
2288        (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2289        (status = 500, description = "Internal server error")
2290    )
2291)]
2292pub async fn get_opu_types(
2293    State(_state): State<ApiState>,
2294) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2295    let mut types = HashMap::new();
2296
2297    // Dynamically generate metadata from feagi_data_structures templates
2298    for unit in MotorCorticalUnit::list_all() {
2299        let id_ref = unit.get_cortical_id_unit_reference();
2300        let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2301
2302        // All OPU types support both absolute and incremental encodings
2303        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2304
2305        // Determine if formats are supported based on snake_case_name
2306        // MiscData uses Misc (no formats)
2307        // All others use Percentage-based types (have formats)
2308        let snake_name = unit.get_snake_case_name();
2309        let formats = if snake_name == "miscellaneous" {
2310            vec![]
2311        } else {
2312            vec!["linear".to_string(), "fractional".to_string()]
2313        };
2314
2315        // Default resolution - all motors/actuators are typically scalar
2316        let resolution = vec![1, 1, 1];
2317
2318        // All actuators are asymmetric
2319        let structure = "asymmetric".to_string();
2320
2321        // Get unit default topology
2322        let topology_map = unit.get_unit_default_topology();
2323        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2324            .into_iter()
2325            .map(|(idx, topo)| {
2326                (
2327                    *idx as usize,
2328                    UnitTopologyData {
2329                        relative_position: topo.relative_position,
2330                        dimensions: topo.channel_dimensions_default,
2331                    },
2332                )
2333            })
2334            .collect();
2335
2336        types.insert(
2337            key,
2338            CorticalTypeMetadata {
2339                description: unit.get_friendly_name().to_string(),
2340                encodings,
2341                formats,
2342                units: unit.get_number_cortical_areas() as u32,
2343                resolution,
2344                structure,
2345                unit_default_topology,
2346            },
2347        );
2348    }
2349
2350    Ok(Json(types))
2351}
2352
2353/// Get list of all cortical area indices (numerical indices used internally for indexing).
2354#[utoipa::path(
2355    get,
2356    path = "/v1/cortical_area/cortical_area_index_list",
2357    tag = "cortical_area"
2358)]
2359pub async fn get_cortical_area_index_list(
2360    State(state): State<ApiState>,
2361) -> ApiResult<Json<Vec<u32>>> {
2362    let connectome_service = state.connectome_service.as_ref();
2363    let areas = connectome_service
2364        .list_cortical_areas()
2365        .await
2366        .map_err(|e| ApiError::internal(format!("{}", e)))?;
2367    // CRITICAL FIX: Return the actual cortical_idx values, not fabricated sequential indices
2368    let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
2369    Ok(Json(indices))
2370}
2371
2372/// Get mapping from cortical area IDs to their internal indices. Returns {cortical_id: index}.
2373#[utoipa::path(
2374    get,
2375    path = "/v1/cortical_area/cortical_idx_mapping",
2376    tag = "cortical_area"
2377)]
2378pub async fn get_cortical_idx_mapping(
2379    State(state): State<ApiState>,
2380) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
2381    use std::collections::BTreeMap;
2382
2383    let connectome_service = state.connectome_service.as_ref();
2384    let areas = connectome_service
2385        .list_cortical_areas()
2386        .await
2387        .map_err(|e| ApiError::internal(format!("{}", e)))?;
2388    // CRITICAL FIX: Use the actual cortical_idx from CorticalArea, NOT enumerate() which ignores reserved indices!
2389    // Use BTreeMap for consistent alphabetical ordering
2390    let mapping: BTreeMap<String, u32> = areas
2391        .iter()
2392        .map(|a| (a.cortical_id.clone(), a.cortical_idx))
2393        .collect();
2394    Ok(Json(mapping))
2395}
2396
2397/// Get restrictions on which cortical areas can connect to which (connection validation rules).
2398#[utoipa::path(
2399    get,
2400    path = "/v1/cortical_area/mapping_restrictions",
2401    tag = "cortical_area"
2402)]
2403pub async fn get_mapping_restrictions_query(
2404    State(_state): State<ApiState>,
2405    Query(_params): Query<HashMap<String, String>>,
2406) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2407    Ok(Json(HashMap::new()))
2408}
2409
2410/// Get memory usage of a specific cortical area in bytes (calculated from neuron count).
2411#[utoipa::path(
2412    get,
2413    path = "/v1/cortical_area/{cortical_id}/memory_usage",
2414    tag = "cortical_area"
2415)]
2416pub async fn get_memory_usage(
2417    State(state): State<ApiState>,
2418    Path(cortical_id): Path<String>,
2419) -> ApiResult<Json<HashMap<String, i64>>> {
2420    let connectome_service = state.connectome_service.as_ref();
2421
2422    // CRITICAL FIX: Calculate actual memory usage based on neuron count instead of hardcoded 0
2423    let area_info = connectome_service
2424        .get_cortical_area(&cortical_id)
2425        .await
2426        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2427
2428    // Calculate memory usage: neuron_count × bytes per neuron
2429    // Each neuron in NeuronArray uses ~48 bytes (membrane_potential, threshold, refractory, etc.)
2430    const BYTES_PER_NEURON: i64 = 48;
2431    let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
2432
2433    let mut response = HashMap::new();
2434    response.insert("memory_bytes".to_string(), memory_bytes);
2435    Ok(Json(response))
2436}
2437
2438/// Get the total number of neurons in a specific cortical area.
2439#[utoipa::path(
2440    get,
2441    path = "/v1/cortical_area/{cortical_id}/neuron_count",
2442    tag = "cortical_area"
2443)]
2444pub async fn get_area_neuron_count(
2445    State(state): State<ApiState>,
2446    Path(cortical_id): Path<String>,
2447) -> ApiResult<Json<i64>> {
2448    let connectome_service = state.connectome_service.as_ref();
2449
2450    // CRITICAL FIX: Get actual neuron count from ConnectomeService instead of hardcoded 0
2451    let area_info = connectome_service
2452        .get_cortical_area(&cortical_id)
2453        .await
2454        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2455
2456    Ok(Json(area_info.neuron_count as i64))
2457}
2458
2459/// Get available cortical type options for UI selection: Sensory, Motor, Custom, Memory.
2460#[utoipa::path(
2461    post,
2462    path = "/v1/cortical_area/cortical_type_options",
2463    tag = "cortical_area"
2464)]
2465pub async fn post_cortical_type_options(
2466    State(_state): State<ApiState>,
2467) -> ApiResult<Json<Vec<String>>> {
2468    Ok(Json(vec![
2469        "Sensory".to_string(),
2470        "Motor".to_string(),
2471        "Custom".to_string(),
2472        "Memory".to_string(),
2473    ]))
2474}
2475
2476/// Get mapping restrictions for specific cortical areas (POST version with request body).
2477#[utoipa::path(
2478    post,
2479    path = "/v1/cortical_area/mapping_restrictions",
2480    tag = "cortical_area"
2481)]
2482pub async fn post_mapping_restrictions(
2483    State(_state): State<ApiState>,
2484    Json(_req): Json<HashMap<String, String>>,
2485) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2486    Ok(Json(HashMap::new()))
2487}
2488
2489/// Get mapping restrictions between two specific cortical areas (connection validation).
2490#[utoipa::path(
2491    post,
2492    path = "/v1/cortical_area/mapping_restrictions_between_areas",
2493    tag = "cortical_area"
2494)]
2495pub async fn post_mapping_restrictions_between_areas(
2496    State(_state): State<ApiState>,
2497    Json(_req): Json<HashMap<String, String>>,
2498) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2499    Ok(Json(HashMap::new()))
2500}
2501
2502/// Update 3D coordinates of a cortical area (alternative endpoint). (Not yet implemented)
2503#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
2504pub async fn put_coord_3d(
2505    State(_state): State<ApiState>,
2506    Json(_req): Json<HashMap<String, serde_json::Value>>,
2507) -> ApiResult<Json<HashMap<String, String>>> {
2508    Ok(Json(HashMap::from([(
2509        "message".to_string(),
2510        "Not yet implemented".to_string(),
2511    )])))
2512}
2513
2514#[cfg(test)]
2515mod voxel_neurons_dto_tests {
2516    use super::{
2517        synapse_details_for_neuron, synapse_page_window, VoxelNeuronsBody, VoxelNeuronsResponse,
2518    };
2519
2520    #[test]
2521    fn synapse_page_window_paginates_fifty_per_direction() {
2522        let (s, e, more) = synapse_page_window(120, 0);
2523        assert_eq!((s, e, more), (0, 50, true));
2524        let (s, e, more) = synapse_page_window(120, 1);
2525        assert_eq!((s, e, more), (50, 100, true));
2526        let (s, e, more) = synapse_page_window(120, 2);
2527        assert_eq!((s, e, more), (100, 120, false));
2528        let (s, e, more) = synapse_page_window(120, 3);
2529        assert_eq!((s, e, more), (0, 0, false));
2530    }
2531
2532    #[test]
2533    fn synapse_details_matches_connectome_shape() {
2534        let mgr = feagi_brain_development::ConnectomeManager::new_for_testing();
2535        let out_full = vec![(10, 2.0, 5.0, 1)];
2536        let inc_full = vec![(3, 4.0, 6.0, 0)];
2537        let (out, inc) = synapse_details_for_neuron(&mgr, 7, &out_full, &inc_full);
2538        let out_a = out.as_array().expect("outgoing array");
2539        assert_eq!(out_a[0]["source_neuron_id"], serde_json::json!(7));
2540        assert_eq!(out_a[0]["target_neuron_id"], serde_json::json!(10));
2541        assert_eq!(out_a[0]["weight"], serde_json::json!(2.0));
2542        assert_eq!(out_a[0]["postsynaptic_potential"], serde_json::json!(5.0));
2543        assert_eq!(out_a[0]["synapse_type"], serde_json::json!(1));
2544        assert!(out_a[0].get("target_cortical_id").is_some());
2545        assert!(out_a[0].get("target_cortical_name").is_some());
2546        assert!(out_a[0].get("target_x").is_some());
2547        let in_a = inc.as_array().expect("incoming array");
2548        assert_eq!(in_a[0]["source_neuron_id"], serde_json::json!(3));
2549        assert_eq!(in_a[0]["target_neuron_id"], serde_json::json!(7));
2550        assert!(in_a[0].get("source_cortical_id").is_some());
2551        assert!(in_a[0].get("source_cortical_name").is_some());
2552        assert!(in_a[0].get("source_x").is_some());
2553    }
2554
2555    #[test]
2556    fn voxel_neurons_body_deserializes_from_json() {
2557        let j = r#"{"cortical_id":"X19fcG93ZXI=","x":0,"y":0,"z":0}"#;
2558        let b: VoxelNeuronsBody = serde_json::from_str(j).expect("deserialize body");
2559        assert_eq!(b.cortical_id, "X19fcG93ZXI=");
2560        assert_eq!((b.x, b.y, b.z), (0, 0, 0));
2561        assert_eq!(b.synapse_page, 0);
2562    }
2563
2564    #[test]
2565    fn voxel_neurons_response_serializes() {
2566        let r = VoxelNeuronsResponse {
2567            cortical_id: "id".to_string(),
2568            cortical_name: "test_area".to_string(),
2569            cortical_idx: 2,
2570            voxel_coordinate: [1, 2, 3],
2571            x: 1,
2572            y: 2,
2573            z: 3,
2574            synapse_page: 0,
2575            neuron_count: 0,
2576            neurons: vec![],
2577        };
2578        let v = serde_json::to_value(&r).expect("serialize");
2579        assert_eq!(v["cortical_name"], serde_json::json!("test_area"));
2580        assert_eq!(v["voxel_coordinate"], serde_json::json!([1, 2, 3]));
2581        assert_eq!(v["neuron_count"], serde_json::json!(0));
2582        assert_eq!(v["synapse_page"], serde_json::json!(0));
2583        assert_eq!(v["neurons"], serde_json::json!([]));
2584    }
2585}