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_structures::genomic::cortical_area::descriptors::CorticalSubUnitIndex;
15use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
16
17// ============================================================================
18// REQUEST/RESPONSE MODELS
19// ============================================================================
20
21#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
22pub struct CorticalAreaIdListResponse {
23    pub cortical_ids: Vec<String>,
24}
25
26#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
27pub struct CorticalAreaNameListResponse {
28    pub cortical_area_name_list: Vec<String>,
29}
30
31#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
32pub struct UnitTopologyData {
33    pub relative_position: [i32; 3],
34    pub dimensions: [u32; 3],
35}
36
37#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
38pub struct CorticalTypeMetadata {
39    pub description: String,
40    pub encodings: Vec<String>,
41    pub formats: Vec<String>,
42    pub units: u32,
43    pub resolution: Vec<i32>,
44    pub structure: String,
45    pub unit_default_topology: HashMap<usize, UnitTopologyData>,
46}
47
48// ============================================================================
49// ENDPOINTS
50// ============================================================================
51
52/// List all IPU (Input Processing Unit) cortical area IDs. Returns IDs of all sensory cortical areas.
53#[utoipa::path(get, path = "/v1/cortical_area/ipu", tag = "cortical_area")]
54pub async fn get_ipu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
55    let connectome_service = state.connectome_service.as_ref();
56    match connectome_service.list_cortical_areas().await {
57        Ok(areas) => {
58            let ipu_areas: Vec<String> = areas
59                .into_iter()
60                .filter(|a| a.area_type == "sensory" || a.area_type == "IPU")
61                .map(|a| a.cortical_id)
62                .collect();
63            Ok(Json(ipu_areas))
64        }
65        Err(e) => Err(ApiError::internal(format!(
66            "Failed to get IPU areas: {}",
67            e
68        ))),
69    }
70}
71
72/// List all OPU (Output Processing Unit) cortical area IDs. Returns IDs of all motor cortical areas.
73#[utoipa::path(get, path = "/v1/cortical_area/opu", tag = "cortical_area")]
74pub async fn get_opu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
75    let connectome_service = state.connectome_service.as_ref();
76    match connectome_service.list_cortical_areas().await {
77        Ok(areas) => {
78            let opu_areas: Vec<String> = areas
79                .into_iter()
80                .filter(|a| a.area_type == "motor" || a.area_type == "OPU")
81                .map(|a| a.cortical_id)
82                .collect();
83            Ok(Json(opu_areas))
84        }
85        Err(e) => Err(ApiError::internal(format!(
86            "Failed to get OPU areas: {}",
87            e
88        ))),
89    }
90}
91
92/// Get a list of all cortical area IDs across the entire genome (IPU, OPU, custom, memory, and core areas).
93#[utoipa::path(
94    get,
95    path = "/v1/cortical_area/cortical_area_id_list",
96    tag = "cortical_area",
97    responses(
98        (status = 200, description = "Cortical area IDs retrieved successfully", body = CorticalAreaIdListResponse),
99        (status = 500, description = "Internal server error", body = ApiError)
100    )
101)]
102pub async fn get_cortical_area_id_list(
103    State(state): State<ApiState>,
104) -> ApiResult<Json<CorticalAreaIdListResponse>> {
105    tracing::debug!(target: "feagi-api", "πŸ” GET /v1/cortical_area/cortical_area_id_list - handler called");
106    let connectome_service = state.connectome_service.as_ref();
107    match connectome_service.get_cortical_area_ids().await {
108        Ok(ids) => {
109            tracing::info!(target: "feagi-api", "βœ… GET /v1/cortical_area/cortical_area_id_list - success, returning {} IDs", ids.len());
110            tracing::debug!(target: "feagi-api", "πŸ“‹ Cortical area IDs: {:?}", ids.iter().take(20).collect::<Vec<_>>());
111            let response = CorticalAreaIdListResponse {
112                cortical_ids: ids.clone(),
113            };
114            match serde_json::to_string(&response) {
115                Ok(json_str) => {
116                    tracing::debug!(target: "feagi-api", "πŸ“€ Response JSON: {}", json_str);
117                }
118                Err(e) => {
119                    tracing::warn!(target: "feagi-api", "⚠️ Failed to serialize response: {}", e);
120                }
121            }
122            Ok(Json(response))
123        }
124        Err(e) => {
125            tracing::error!(target: "feagi-api", "❌ GET /v1/cortical_area/cortical_area_id_list - error: {}", e);
126            Err(ApiError::internal(format!(
127                "Failed to get cortical IDs: {}",
128                e
129            )))
130        }
131    }
132}
133
134/// Get a list of all cortical area names (human-readable labels for all cortical areas).
135#[utoipa::path(
136    get,
137    path = "/v1/cortical_area/cortical_area_name_list",
138    tag = "cortical_area",
139    responses(
140        (status = 200, description = "Cortical area names retrieved successfully", body = CorticalAreaNameListResponse),
141        (status = 500, description = "Internal server error")
142    )
143)]
144pub async fn get_cortical_area_name_list(
145    State(state): State<ApiState>,
146) -> ApiResult<Json<CorticalAreaNameListResponse>> {
147    let connectome_service = state.connectome_service.as_ref();
148    match connectome_service.list_cortical_areas().await {
149        Ok(areas) => {
150            let names: Vec<String> = areas.into_iter().map(|a| a.name).collect();
151            Ok(Json(CorticalAreaNameListResponse {
152                cortical_area_name_list: names,
153            }))
154        }
155        Err(e) => Err(ApiError::internal(format!(
156            "Failed to get cortical names: {}",
157            e
158        ))),
159    }
160}
161
162/// Get a map of cortical area IDs to their human-readable names. Returns {cortical_id: name} pairs.
163#[utoipa::path(
164    get,
165    path = "/v1/cortical_area/cortical_id_name_mapping",
166    tag = "cortical_area"
167)]
168pub async fn get_cortical_id_name_mapping(
169    State(state): State<ApiState>,
170) -> ApiResult<Json<HashMap<String, String>>> {
171    let connectome_service = state.connectome_service.as_ref();
172    let ids = connectome_service
173        .get_cortical_area_ids()
174        .await
175        .map_err(|e| ApiError::internal(format!("Failed to get IDs: {}", e)))?;
176
177    let mut mapping = HashMap::new();
178    for id in ids {
179        if let Ok(area) = connectome_service.get_cortical_area(&id).await {
180            mapping.insert(id, area.name);
181        }
182    }
183    Ok(Json(mapping))
184}
185
186/// Get available cortical area types: sensory, motor, memory, and custom.
187#[utoipa::path(get, path = "/v1/cortical_area/cortical_types", tag = "cortical_area")]
188pub async fn get_cortical_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
189    Ok(Json(vec![
190        "sensory".to_string(),
191        "motor".to_string(),
192        "memory".to_string(),
193        "custom".to_string(),
194    ]))
195}
196
197/// Get detailed cortical connectivity mappings showing source-to-destination connections with mapping rules.
198#[utoipa::path(
199    get,
200    path = "/v1/cortical_area/cortical_map_detailed",
201    tag = "cortical_area",
202    responses(
203        (status = 200, description = "Detailed cortical area mapping data", body = HashMap<String, serde_json::Value>),
204        (status = 500, description = "Internal server error")
205    )
206)]
207pub async fn get_cortical_map_detailed(
208    State(state): State<ApiState>,
209) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
210    let connectome_service = state.connectome_service.as_ref();
211    match connectome_service.list_cortical_areas().await {
212        Ok(areas) => {
213            let mut map: HashMap<String, serde_json::Value> = HashMap::new();
214
215            for area in areas {
216                // Extract cortical_mapping_dst from area properties
217                if let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") {
218                    if !cortical_mapping_dst.is_null()
219                        && cortical_mapping_dst
220                            .as_object()
221                            .is_some_and(|obj| !obj.is_empty())
222                    {
223                        map.insert(area.cortical_id.clone(), cortical_mapping_dst.clone());
224                    }
225                }
226            }
227
228            Ok(Json(map))
229        }
230        Err(e) => Err(ApiError::internal(format!(
231            "Failed to get detailed map: {}",
232            e
233        ))),
234    }
235}
236
237/// Get 2D positions of all cortical areas for visualization. Returns {cortical_id: (x, y)} coordinates.
238#[utoipa::path(
239    get,
240    path = "/v1/cortical_area/cortical_locations_2d",
241    tag = "cortical_area"
242)]
243pub async fn get_cortical_locations_2d(
244    State(state): State<ApiState>,
245) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
246    let connectome_service = state.connectome_service.as_ref();
247    match connectome_service.list_cortical_areas().await {
248        Ok(areas) => {
249            let locations: HashMap<String, (i32, i32)> = areas
250                .into_iter()
251                .map(|area| (area.cortical_id, (area.position.0, area.position.1)))
252                .collect();
253            Ok(Json(locations))
254        }
255        Err(e) => Err(ApiError::internal(format!(
256            "Failed to get 2D locations: {}",
257            e
258        ))),
259    }
260}
261
262/// Get complete cortical area data including geometry, neural parameters, and metadata. Used by Brain Visualizer.
263#[utoipa::path(
264    get,
265    path = "/v1/cortical_area/cortical_area/geometry",
266    tag = "cortical_area"
267)]
268pub async fn get_cortical_area_geometry(
269    State(state): State<ApiState>,
270) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
271    let connectome_service = state.connectome_service.as_ref();
272    match connectome_service.list_cortical_areas().await {
273        Ok(areas) => {
274            let geometry: HashMap<String, serde_json::Value> = areas.into_iter()
275                .map(|area| {
276                    // Return FULL cortical area data (matching Python format)
277                    // This is what Brain Visualizer expects for genome loading
278                    let coordinate_2d = area
279                        .properties
280                        .get("coordinate_2d")
281                        .or_else(|| area.properties.get("coordinates_2d"))
282                        .cloned()
283                        .unwrap_or_else(|| serde_json::json!([0, 0]));
284                    let data = serde_json::json!({
285                        "cortical_id": area.cortical_id,
286                        "cortical_name": area.name,
287                        "cortical_group": area.cortical_group,
288                        "cortical_type": area.cortical_type,  // NEW: Explicitly include cortical_type for BV
289                        "cortical_sub_group": area.sub_group.as_ref().unwrap_or(&String::new()),  // Return empty string instead of null
290                        "coordinates_3d": [area.position.0, area.position.1, area.position.2],
291                        "coordinates_2d": coordinate_2d,
292                        "cortical_dimensions": [area.dimensions.0, area.dimensions.1, area.dimensions.2],
293                        "cortical_neuron_per_vox_count": area.neurons_per_voxel,
294                        "visualization": area.visible,
295                        "visible": area.visible,
296                        // Also include dictionary-style for backward compatibility
297                        "dimensions": {
298                            "x": area.dimensions.0,
299                            "y": area.dimensions.1,
300                            "z": area.dimensions.2
301                        },
302                        "position": {
303                            "x": area.position.0,
304                            "y": area.position.1,
305                            "z": area.position.2
306                        },
307                        // Neural parameters
308                        "neuron_post_synaptic_potential": area.postsynaptic_current,
309                        // BV expects firing threshold and threshold limit as separate fields.
310                        "neuron_fire_threshold": area.firing_threshold,
311                        "neuron_firing_threshold_limit": area.firing_threshold_limit,
312                        "plasticity_constant": area.plasticity_constant,
313                        "degeneration": area.degeneration,
314                        "leak_coefficient": area.leak_coefficient,
315                        "refractory_period": area.refractory_period,
316                        "snooze_period": area.snooze_period,
317                        // Parent region ID (required by Brain Visualizer)
318                        "parent_region_id": area.parent_region_id,
319                        // Visualization voxel granularity for large-area rendering (optional)
320                        "visualization_voxel_granularity": area.visualization_voxel_granularity.map(|(x, y, z)| serde_json::json!([x, y, z])),
321                    });
322                    (area.cortical_id.clone(), data)
323                })
324                .collect();
325            Ok(Json(geometry))
326        }
327        Err(e) => Err(ApiError::internal(format!("Failed to get geometry: {}", e))),
328    }
329}
330
331/// Get visibility status of all cortical areas. Returns {cortical_id: visibility_flag}.
332#[utoipa::path(
333    get,
334    path = "/v1/cortical_area/cortical_visibility",
335    tag = "cortical_area"
336)]
337pub async fn get_cortical_visibility(
338    State(state): State<ApiState>,
339) -> ApiResult<Json<HashMap<String, bool>>> {
340    let connectome_service = state.connectome_service.as_ref();
341    match connectome_service.list_cortical_areas().await {
342        Ok(areas) => {
343            let visibility: HashMap<String, bool> = areas
344                .into_iter()
345                .map(|area| (area.cortical_id, area.visible))
346                .collect();
347            Ok(Json(visibility))
348        }
349        Err(e) => Err(ApiError::internal(format!(
350            "Failed to get visibility: {}",
351            e
352        ))),
353    }
354}
355
356/// Get the 2D location of a cortical area by its name. Request: {cortical_name: string}.
357#[utoipa::path(
358    post,
359    path = "/v1/cortical_area/cortical_name_location",
360    tag = "cortical_area"
361)]
362#[allow(unused_variables)] // In development
363pub async fn post_cortical_name_location(
364    State(state): State<ApiState>,
365    Json(request): Json<HashMap<String, String>>,
366) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
367    let connectome_service = state.connectome_service.as_ref();
368    let cortical_name = request
369        .get("cortical_name")
370        .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
371
372    match connectome_service.get_cortical_area(cortical_name).await {
373        Ok(area) => Ok(Json(HashMap::from([(
374            area.cortical_id,
375            (area.position.0, area.position.1),
376        )]))),
377        Err(e) => Err(ApiError::internal(format!("Failed to get location: {}", e))),
378    }
379}
380
381/// Get detailed properties of a single cortical area by ID. Request: {cortical_id: string}.
382#[utoipa::path(
383    post,
384    path = "/v1/cortical_area/cortical_area_properties",
385    tag = "cortical_area"
386)]
387#[allow(unused_variables)] // In development
388pub async fn post_cortical_area_properties(
389    State(state): State<ApiState>,
390    Json(request): Json<HashMap<String, String>>,
391) -> ApiResult<Json<serde_json::Value>> {
392    let connectome_service = state.connectome_service.as_ref();
393    let cortical_id = request
394        .get("cortical_id")
395        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
396
397    match connectome_service.get_cortical_area(cortical_id).await {
398        Ok(area_info) => {
399            tracing::debug!(target: "feagi-api", "Cortical area properties for {}: cortical_group={}, area_type={}, cortical_type={}", 
400                cortical_id, area_info.cortical_group, area_info.area_type, area_info.cortical_type);
401            tracing::info!(target: "feagi-api", "[API-RESPONSE] Returning mp_driven_psp={} for area {}", area_info.mp_driven_psp, cortical_id);
402            let json_value = serde_json::to_value(&area_info).unwrap_or_default();
403            tracing::debug!(target: "feagi-api", "Serialized JSON keys: {:?}", json_value.as_object().map(|o| o.keys().collect::<Vec<_>>()));
404            tracing::debug!(target: "feagi-api", "Serialized cortical_type value: {:?}", json_value.get("cortical_type"));
405            Ok(Json(json_value))
406        }
407        Err(e) => Err(ApiError::internal(format!(
408            "Failed to get properties: {}",
409            e
410        ))),
411    }
412}
413
414/// Get properties for multiple cortical areas. Accepts array [\"id1\", \"id2\"] or object {cortical_id_list: [...]}.
415#[utoipa::path(
416    post,
417    path = "/v1/cortical_area/multi/cortical_area_properties",
418    tag = "cortical_area"
419)]
420#[allow(unused_variables)] // In development
421pub async fn post_multi_cortical_area_properties(
422    State(state): State<ApiState>,
423    Json(request): Json<serde_json::Value>,
424) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
425    let connectome_service = state.connectome_service.as_ref();
426    let mut result = HashMap::new();
427
428    // Support both formats for backward compatibility
429    let cortical_ids: Vec<String> = if request.is_array() {
430        // Format 1: Direct array ["id1", "id2"] (Python SDK)
431        request
432            .as_array()
433            .unwrap()
434            .iter()
435            .filter_map(|v| v.as_str().map(|s| s.to_string()))
436            .collect()
437    } else if request.is_object() {
438        // Format 2: Object with cortical_id_list {"cortical_id_list": ["id1", "id2"]} (Brain Visualizer)
439        request
440            .get("cortical_id_list")
441            .and_then(|v| v.as_array())
442            .ok_or_else(|| ApiError::invalid_input("cortical_id_list required in object format"))?
443            .iter()
444            .filter_map(|v| v.as_str().map(|s| s.to_string()))
445            .collect()
446    } else {
447        return Err(ApiError::invalid_input(
448            "Request must be an array of IDs or object with cortical_id_list",
449        ));
450    };
451
452    for cortical_id in cortical_ids {
453        if let Ok(area_info) = connectome_service.get_cortical_area(&cortical_id).await {
454            tracing::debug!(target: "feagi-api",
455                "[MULTI] Area {}: cortical_type={}, cortical_group={}, is_mem_type={:?}",
456                cortical_id, area_info.cortical_type, area_info.cortical_group,
457                area_info.properties.get("is_mem_type")
458            );
459            let json_value = serde_json::to_value(&area_info).unwrap_or_default();
460            tracing::debug!(target: "feagi-api",
461                "[MULTI] Serialized has cortical_type: {}",
462                json_value.get("cortical_type").is_some()
463            );
464            result.insert(cortical_id, json_value);
465        }
466    }
467    Ok(Json(result))
468}
469
470/// Create IPU (sensory) or OPU (motor) cortical areas with proper topology and multi-unit support.
471#[utoipa::path(post, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
472#[allow(unused_variables)] // In development - parameters will be used when implemented
473pub async fn post_cortical_area(
474    State(state): State<ApiState>,
475    Json(request): Json<HashMap<String, serde_json::Value>>,
476) -> ApiResult<Json<serde_json::Value>> {
477    use feagi_services::types::CreateCorticalAreaParams;
478    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
479
480    // ARCHITECTURE: Use genome_service (proper entry point) instead of connectome_service
481    let genome_service = state.genome_service.as_ref();
482
483    // Extract required fields
484    let cortical_type_key = request
485        .get("cortical_id")
486        .and_then(|v| v.as_str())
487        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
488
489    let mut group_id = request
490        .get("group_id")
491        .and_then(|v| v.as_u64())
492        .unwrap_or(0) as u8;
493
494    let device_count = request
495        .get("device_count")
496        .and_then(|v| v.as_u64())
497        .ok_or_else(|| ApiError::invalid_input("device_count required"))?
498        as usize;
499
500    let coordinates_3d: Vec<i32> = request
501        .get("coordinates_3d")
502        .and_then(|v| v.as_array())
503        .and_then(|arr| {
504            if arr.len() == 3 {
505                Some(vec![
506                    arr[0].as_i64()? as i32,
507                    arr[1].as_i64()? as i32,
508                    arr[2].as_i64()? as i32,
509                ])
510            } else {
511                None
512            }
513        })
514        .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
515
516    let cortical_type_str = request
517        .get("cortical_type")
518        .and_then(|v| v.as_str())
519        .ok_or_else(|| ApiError::invalid_input("cortical_type required"))?;
520
521    let unit_id: Option<u8> = request
522        .get("unit_id")
523        .and_then(|v| v.as_u64())
524        .map(|value| {
525            value
526                .try_into()
527                .map_err(|_| ApiError::invalid_input("unit_id out of range"))
528        })
529        .transpose()?;
530    if let Some(unit_id) = unit_id {
531        group_id = unit_id;
532    }
533
534    // Extract neurons_per_voxel from request (default to 1 if not provided)
535    let neurons_per_voxel = request
536        .get("neurons_per_voxel")
537        .and_then(|v| v.as_u64())
538        .unwrap_or(1) as u32;
539
540    // BREAKING CHANGE (unreleased API):
541    // `data_type_config` is now per-subunit, because some cortical units have heterogeneous
542    // subunits (e.g. Gaze: Percentage2D + Percentage).
543    //
544    // Request must provide:
545    //   data_type_configs_by_subunit: { "0": <u16>, "1": <u16>, ... }
546    let raw_configs = request
547        .get("data_type_configs_by_subunit")
548        .and_then(|v| v.as_object())
549        .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
550
551    let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
552
553    for (k, v) in raw_configs {
554        let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
555            ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
556        })?;
557        let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
558            ApiError::invalid_input("data_type_configs_by_subunit key out of range")
559        })?;
560
561        let parsed_u64 = if let Some(u) = v.as_u64() {
562            Some(u)
563        } else if let Some(i) = v.as_i64() {
564            if i >= 0 {
565                Some(i as u64)
566            } else {
567                None
568            }
569        } else if let Some(f) = v.as_f64() {
570            if f >= 0.0 {
571                Some(f.round() as u64)
572            } else {
573                None
574            }
575        } else if let Some(s) = v.as_str() {
576            s.parse::<u64>().ok()
577        } else {
578            None
579        }
580        .ok_or_else(|| {
581            ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
582        })?;
583
584        if parsed_u64 > u16::MAX as u64 {
585            return Err(ApiError::invalid_input(
586                "data_type_configs_by_subunit value exceeds u16::MAX",
587            ));
588        }
589
590        data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
591    }
592
593    tracing::info!(
594        target: "feagi-api",
595        "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
596        cortical_type_key,
597        neurons_per_voxel,
598        data_type_configs_by_subunit
599    );
600
601    // Determine number of units and get topology
602    let (num_units, unit_topology) = if cortical_type_str == "IPU" {
603        // Find the matching sensory cortical unit
604        let unit = SensoryCorticalUnit::list_all()
605            .iter()
606            .find(|u| {
607                let id_ref = u.get_cortical_id_unit_reference();
608                let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
609                key == cortical_type_key
610            })
611            .ok_or_else(|| {
612                ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
613            })?;
614
615        (
616            unit.get_number_cortical_areas(),
617            unit.get_unit_default_topology(),
618        )
619    } else if cortical_type_str == "OPU" {
620        // Find the matching motor cortical unit
621        let unit = MotorCorticalUnit::list_all()
622            .iter()
623            .find(|u| {
624                let id_ref = u.get_cortical_id_unit_reference();
625                let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
626                key == cortical_type_key
627            })
628            .ok_or_else(|| {
629                ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
630            })?;
631
632        (
633            unit.get_number_cortical_areas(),
634            unit.get_unit_default_topology(),
635        )
636    } else {
637        return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
638    };
639
640    tracing::info!(
641        "Creating {} units for cortical type: {}",
642        num_units,
643        cortical_type_key
644    );
645
646    // Build creation parameters for all units
647    let mut creation_params = Vec::new();
648    for unit_idx in 0..num_units {
649        let data_type_config = data_type_configs_by_subunit
650            .get(&(unit_idx as u8))
651            .copied()
652            .ok_or_else(|| {
653                ApiError::invalid_input(format!(
654                    "data_type_configs_by_subunit missing entry for subunit {}",
655                    unit_idx
656                ))
657            })?;
658
659        // Split per-subunit data_type_config into two bytes for cortical ID
660        let config_byte_4 = (data_type_config & 0xFF) as u8; // Lower byte
661        let config_byte_5 = ((data_type_config >> 8) & 0xFF) as u8; // Upper byte
662
663        // Get per-device dimensions from topology, then scale X by device_count:
664        // total_x = device_count * per_device_x
665        let (per_device_dimensions, dimensions) =
666            if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
667                let dims = topo.channel_dimensions_default;
668                let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
669                let total_x = per_device.0.saturating_mul(device_count);
670                (per_device, (total_x, per_device.1, per_device.2))
671            } else {
672                ((1, 1, 1), (device_count.max(1), 1, 1)) // Fallback
673            };
674
675        // Calculate position for this unit
676        let position =
677            if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
678                let rel_pos = topo.relative_position;
679                (
680                    coordinates_3d[0] + rel_pos[0],
681                    coordinates_3d[1] + rel_pos[1],
682                    coordinates_3d[2] + rel_pos[2],
683                )
684            } else {
685                (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
686            };
687
688        // Construct proper 8-byte cortical ID
689        // Byte structure: [type(i/o), subtype[0], subtype[1], subtype[2], encoding_type, encoding_format, unit_idx, group_id]
690        // Extract the 3-character subtype from cortical_type_key (e.g., "isvi" -> "svi")
691        let subtype_bytes = if cortical_type_key.len() >= 4 {
692            let subtype_str = &cortical_type_key[1..4]; // Skip the 'i' or 'o' prefix
693            let mut bytes = [0u8; 3];
694            for (i, c) in subtype_str.chars().take(3).enumerate() {
695                bytes[i] = c as u8;
696            }
697            bytes
698        } else {
699            return Err(ApiError::invalid_input("Invalid cortical_type_key"));
700        };
701
702        // Construct the 8-byte cortical ID
703        let cortical_id_bytes = [
704            if cortical_type_str == "IPU" {
705                b'i'
706            } else {
707                b'o'
708            }, // Byte 0: type
709            subtype_bytes[0], // Byte 1: subtype[0]
710            subtype_bytes[1], // Byte 2: subtype[1]
711            subtype_bytes[2], // Byte 3: subtype[2]
712            config_byte_4,    // Byte 4: data type config (lower byte)
713            config_byte_5,    // Byte 5: data type config (upper byte)
714            unit_idx as u8,   // Byte 6: unit index
715            group_id,         // Byte 7: group ID
716        ];
717
718        // Encode to base64 for use as cortical_id string
719        let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
720
721        tracing::debug!(target: "feagi-api",
722            "  Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
723            unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
724            dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
725        );
726
727        // Store device_count and per-device dimensions in properties for BV compatibility
728        let mut properties = HashMap::new();
729        properties.insert(
730            "dev_count".to_string(),
731            serde_json::Value::Number(serde_json::Number::from(device_count)),
732        );
733        properties.insert(
734            "cortical_dimensions_per_device".to_string(),
735            serde_json::json!([
736                per_device_dimensions.0,
737                per_device_dimensions.1,
738                per_device_dimensions.2
739            ]),
740        );
741
742        let params = CreateCorticalAreaParams {
743            cortical_id: cortical_id.clone(),
744            name: format!("{} Unit {}", cortical_type_key, unit_idx),
745            dimensions,
746            position,
747            area_type: cortical_type_str.to_string(),
748            visible: Some(true),
749            sub_group: None,
750            neurons_per_voxel: Some(neurons_per_voxel),
751            postsynaptic_current: Some(0.0),
752            plasticity_constant: Some(0.0),
753            degeneration: Some(0.0),
754            psp_uniform_distribution: Some(false),
755            firing_threshold_increment: Some(0.0),
756            firing_threshold_limit: Some(0.0),
757            consecutive_fire_count: Some(0),
758            snooze_period: Some(0),
759            refractory_period: Some(0),
760            leak_coefficient: Some(0.0),
761            leak_variability: Some(0.0),
762            burst_engine_active: Some(true),
763            properties: Some(properties),
764        };
765
766        creation_params.push(params);
767    }
768
769    tracing::info!(
770        "Calling GenomeService to create {} cortical areas",
771        creation_params.len()
772    );
773
774    // ARCHITECTURE: Call genome_service.create_cortical_areas (proper flow)
775    // This will: 1) Update runtime genome, 2) Call neuroembryogenesis, 3) Create neurons/synapses
776    let areas_details = genome_service
777        .create_cortical_areas(creation_params)
778        .await
779        .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
780
781    tracing::info!(
782        "βœ… Successfully created {} cortical areas via GenomeService",
783        areas_details.len()
784    );
785
786    // Serialize as JSON
787    let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
788
789    // Extract cortical IDs from created areas
790    let created_ids: Vec<String> = areas_details
791        .iter()
792        .map(|a| a.cortical_id.clone())
793        .collect();
794
795    // Return comprehensive response
796    let first_id = created_ids.first().cloned().unwrap_or_default();
797    let mut response = serde_json::Map::new();
798    response.insert(
799        "message".to_string(),
800        serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
801    );
802    response.insert(
803        "cortical_id".to_string(),
804        serde_json::Value::String(first_id),
805    ); // For backward compatibility
806    response.insert(
807        "cortical_ids".to_string(),
808        serde_json::Value::String(created_ids.join(", ")),
809    );
810    response.insert(
811        "unit_count".to_string(),
812        serde_json::Value::Number(created_ids.len().into()),
813    );
814    response.insert("areas".to_string(), areas_json); // Full details for all areas
815
816    Ok(Json(serde_json::Value::Object(response)))
817}
818
819/// Update properties of an existing cortical area (position, dimensions, neural parameters, etc.).
820#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
821pub async fn put_cortical_area(
822    State(state): State<ApiState>,
823    Json(mut request): Json<HashMap<String, serde_json::Value>>,
824) -> ApiResult<Json<HashMap<String, String>>> {
825    let genome_service = state.genome_service.as_ref();
826
827    // Extract cortical_id
828    let cortical_id = request
829        .get("cortical_id")
830        .and_then(|v| v.as_str())
831        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
832        .to_string();
833
834    tracing::debug!(
835        target: "feagi-api",
836        "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
837        cortical_id,
838        request.keys().collect::<Vec<_>>()
839    );
840
841    // Remove cortical_id from changes (it's not a property to update)
842    request.remove("cortical_id");
843
844    // Call GenomeService with raw changes (it handles classification and routing)
845    match genome_service
846        .update_cortical_area(&cortical_id, request)
847        .await
848    {
849        Ok(area_info) => {
850            let updated_id = area_info.cortical_id.clone();
851            tracing::debug!(
852                target: "feagi-api",
853                "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
854                cortical_id,
855                updated_id
856            );
857            Ok(Json(HashMap::from([
858                ("message".to_string(), "Cortical area updated".to_string()),
859                ("cortical_id".to_string(), updated_id),
860                ("previous_cortical_id".to_string(), cortical_id),
861            ])))
862        }
863        Err(e) => {
864            tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
865            Err(ApiError::internal(format!("Failed to update: {}", e)))
866        }
867    }
868}
869
870/// Delete a cortical area by ID. Removes the area and all associated neurons and synapses.
871#[utoipa::path(
872    delete,
873    path = "/v1/cortical_area/cortical_area",
874    tag = "cortical_area"
875)]
876#[allow(unused_variables)] // In development - parameters will be used when implemented
877pub async fn delete_cortical_area(
878    State(state): State<ApiState>,
879    Json(request): Json<HashMap<String, String>>,
880) -> ApiResult<Json<HashMap<String, String>>> {
881    let connectome_service = state.connectome_service.as_ref();
882    let cortical_id = request
883        .get("cortical_id")
884        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
885
886    match connectome_service.delete_cortical_area(cortical_id).await {
887        Ok(_) => Ok(Json(HashMap::from([(
888            "message".to_string(),
889            "Cortical area deleted".to_string(),
890        )]))),
891        Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
892    }
893}
894
895/// Create a custom cortical area for internal processing with specified dimensions and position.
896#[utoipa::path(
897    post,
898    path = "/v1/cortical_area/custom_cortical_area",
899    tag = "cortical_area"
900)]
901pub async fn post_custom_cortical_area(
902    State(state): State<ApiState>,
903    Json(request): Json<HashMap<String, serde_json::Value>>,
904) -> ApiResult<Json<HashMap<String, String>>> {
905    use feagi_services::types::CreateCorticalAreaParams;
906    use std::time::{SystemTime, UNIX_EPOCH};
907
908    // Helper: check whether BV is requesting a MEMORY cortical area (still routed through this endpoint).
909    //
910    // Brain Visualizer sends:
911    //   sub_group_id: "MEMORY"
912    //   cortical_group: "CUSTOM"
913    //
914    // In feagi-core, the authoritative cortical type is derived from the CorticalID prefix byte:
915    // - b'c' => Custom
916    // - b'm' => Memory
917    //
918    // So if sub_group_id indicates MEMORY, we must generate an 'm' prefixed CorticalID.
919    let is_memory_area_requested = request
920        .get("sub_group_id")
921        .and_then(|v| v.as_str())
922        .map(|s| s.eq_ignore_ascii_case("MEMORY"))
923        .unwrap_or(false);
924
925    // Extract required fields from request
926    let cortical_name = request
927        .get("cortical_name")
928        .and_then(|v| v.as_str())
929        .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
930
931    let cortical_dimensions: Vec<u32> = request
932        .get("cortical_dimensions")
933        .and_then(|v| v.as_array())
934        .and_then(|arr| {
935            if arr.len() == 3 {
936                Some(vec![
937                    arr[0].as_u64()? as u32,
938                    arr[1].as_u64()? as u32,
939                    arr[2].as_u64()? as u32,
940                ])
941            } else {
942                None
943            }
944        })
945        .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
946
947    let coordinates_3d: Vec<i32> = request
948        .get("coordinates_3d")
949        .and_then(|v| v.as_array())
950        .and_then(|arr| {
951            if arr.len() == 3 {
952                Some(vec![
953                    arr[0].as_i64()? as i32,
954                    arr[1].as_i64()? as i32,
955                    arr[2].as_i64()? as i32,
956                ])
957            } else {
958                None
959            }
960        })
961        .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
962
963    let brain_region_id = request
964        .get("brain_region_id")
965        .and_then(|v| v.as_str())
966        .map(|s| s.to_string());
967
968    let cortical_sub_group = request
969        .get("cortical_sub_group")
970        .and_then(|v| v.as_str())
971        .filter(|s| !s.is_empty())
972        .map(|s| s.to_string());
973
974    tracing::info!(target: "feagi-api",
975        "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
976        if is_memory_area_requested { "memory" } else { "custom" },
977        cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
978        coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
979    );
980
981    // Generate unique cortical ID for custom cortical area
982    // Format: [b'c', 6 random alphanumeric bytes, group_counter]
983    // Use timestamp + counter to ensure uniqueness
984    let timestamp = SystemTime::now()
985        .duration_since(UNIX_EPOCH)
986        .unwrap()
987        .as_millis() as u64;
988
989    // Create 8-byte cortical ID for custom/memory area
990    // Byte 0: 'c' for custom OR 'm' for memory (authoritative type discriminator)
991    // Bytes 1-6: Derived from name (first 6 chars, padded with underscores)
992    // Byte 7: Counter based on timestamp lower bits
993    let mut cortical_id_bytes = [0u8; 8];
994    cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
995
996    // Use the cortical name for bytes 1-6 (truncate or pad as needed)
997    let name_bytes = cortical_name.as_bytes();
998    for i in 1..7 {
999        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1000            // Use alphanumeric ASCII only
1001            let c = name_bytes[i - 1];
1002            if c.is_ascii_alphanumeric() || c == b'_' {
1003                c
1004            } else {
1005                b'_'
1006            }
1007        } else {
1008            b'_' // Padding
1009        };
1010    }
1011
1012    // Byte 7: Use timestamp lower byte for uniqueness
1013    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1014
1015    // Encode to base64 for use as cortical_id string
1016    let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1017
1018    tracing::debug!(target: "feagi-api",
1019        "Generated cortical_id: {} (raw bytes: {:?})",
1020        cortical_id, cortical_id_bytes
1021    );
1022
1023    // Build properties with brain_region_id if provided
1024    let mut properties = HashMap::new();
1025    if let Some(region_id) = brain_region_id.clone() {
1026        properties.insert(
1027            "parent_region_id".to_string(),
1028            serde_json::Value::String(region_id),
1029        );
1030    }
1031
1032    // Create cortical area parameters
1033    let params = CreateCorticalAreaParams {
1034        cortical_id: cortical_id.clone(),
1035        name: cortical_name.to_string(),
1036        dimensions: (
1037            cortical_dimensions[0] as usize,
1038            cortical_dimensions[1] as usize,
1039            cortical_dimensions[2] as usize,
1040        ),
1041        position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1042        area_type: if is_memory_area_requested {
1043            "Memory".to_string()
1044        } else {
1045            "Custom".to_string()
1046        },
1047        visible: Some(true),
1048        sub_group: cortical_sub_group,
1049        neurons_per_voxel: Some(1),
1050        postsynaptic_current: None,
1051        plasticity_constant: Some(0.0),
1052        degeneration: Some(0.0),
1053        psp_uniform_distribution: Some(false),
1054        firing_threshold_increment: Some(0.0),
1055        firing_threshold_limit: Some(0.0),
1056        consecutive_fire_count: Some(0),
1057        snooze_period: Some(0),
1058        refractory_period: Some(0),
1059        leak_coefficient: Some(0.0),
1060        leak_variability: Some(0.0),
1061        burst_engine_active: Some(true),
1062        properties: Some(properties),
1063    };
1064
1065    let genome_service = state.genome_service.as_ref();
1066
1067    tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1068
1069    // Create the cortical area via GenomeService
1070    let areas_details = genome_service
1071        .create_cortical_areas(vec![params])
1072        .await
1073        .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1074
1075    let created_area = areas_details
1076        .first()
1077        .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1078
1079    tracing::info!(target: "feagi-api",
1080        "βœ… Successfully created custom cortical area '{}' with ID: {}",
1081        cortical_name, created_area.cortical_id
1082    );
1083
1084    // Return response
1085    let mut response = HashMap::new();
1086    response.insert(
1087        "message".to_string(),
1088        "Custom cortical area created successfully".to_string(),
1089    );
1090    response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1091    response.insert("cortical_name".to_string(), cortical_name.to_string());
1092
1093    Ok(Json(response))
1094}
1095
1096/// Clone an existing cortical area with all its properties and structure. (Not yet implemented)
1097#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1098pub async fn post_clone(
1099    State(state): State<ApiState>,
1100    Json(request): Json<CloneCorticalAreaRequest>,
1101) -> ApiResult<Json<HashMap<String, String>>> {
1102    use base64::{engine::general_purpose, Engine as _};
1103    use feagi_services::types::CreateCorticalAreaParams;
1104    use feagi_structures::genomic::cortical_area::CorticalID;
1105    use serde_json::Value;
1106    use std::time::{SystemTime, UNIX_EPOCH};
1107
1108    let genome_service = state.genome_service.as_ref();
1109    let connectome_service = state.connectome_service.as_ref();
1110
1111    // Resolve + validate source cortical ID.
1112    let source_id = request.source_area_id.clone();
1113    let source_typed = CorticalID::try_from_base_64(&source_id)
1114        .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1115    let src_first_byte = source_typed.as_bytes()[0];
1116    if src_first_byte != b'c' && src_first_byte != b'm' {
1117        return Err(ApiError::invalid_input(format!(
1118            "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1119            src_first_byte
1120        )));
1121    }
1122
1123    // Fetch full source info (dimensions, neural params, properties, mappings).
1124    let source_area = connectome_service
1125        .get_cortical_area(&source_id)
1126        .await
1127        .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1128
1129    // FEAGI is the source of truth for brain-region membership.
1130    //
1131    // Do NOT trust the client/UI to provide parent_region_id correctly, because FEAGI already
1132    // knows the source area’s parent. We use FEAGI’s view of parent_region_id for persistence.
1133    //
1134    // If the client provides parent_region_id and it disagrees, fail fast to prevent ambiguity.
1135    let source_parent_region_id = source_area
1136        .parent_region_id
1137        .clone()
1138        .or_else(|| {
1139            source_area
1140                .properties
1141                .get("parent_region_id")
1142                .and_then(|v| v.as_str())
1143                .map(|s| s.to_string())
1144        })
1145        .ok_or_else(|| {
1146            ApiError::internal(format!(
1147                "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1148                source_id
1149            ))
1150        })?;
1151
1152    if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1153        if client_parent_region_id != &source_parent_region_id {
1154            return Err(ApiError::invalid_input(format!(
1155                "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1156                client_parent_region_id, source_id, source_parent_region_id
1157            )));
1158        }
1159    }
1160
1161    // Extract outgoing mappings (we will apply them after creation, via update_cortical_mapping).
1162    let outgoing_mapping_dst = source_area
1163        .properties
1164        .get("cortical_mapping_dst")
1165        .and_then(|v| v.as_object())
1166        .cloned();
1167
1168    // Generate unique cortical ID for the clone.
1169    //
1170    // Rules:
1171    // - Byte 0 keeps the source type discriminator (b'c' or b'm')
1172    // - Bytes 1-6 derived from new_name (alphanumeric/_ only)
1173    // - Byte 7 timestamp lower byte for uniqueness
1174    let timestamp = SystemTime::now()
1175        .duration_since(UNIX_EPOCH)
1176        .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1177        .as_millis() as u64;
1178
1179    let mut cortical_id_bytes = [0u8; 8];
1180    cortical_id_bytes[0] = src_first_byte;
1181
1182    let name_bytes = request.new_name.as_bytes();
1183    for i in 1..7 {
1184        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1185            let c = name_bytes[i - 1];
1186            if c.is_ascii_alphanumeric() || c == b'_' {
1187                c
1188            } else {
1189                b'_'
1190            }
1191        } else {
1192            b'_'
1193        };
1194    }
1195    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1196
1197    let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1198
1199    // Clone properties, but do NOT carry over cortical mapping properties directly.
1200    // Mappings must be created via update_cortical_mapping so synapses are regenerated.
1201    let mut cloned_properties = source_area.properties.clone();
1202    cloned_properties.remove("cortical_mapping_dst");
1203
1204    // Set parent region + 2D coordinate explicitly for the clone.
1205    cloned_properties.insert(
1206        "parent_region_id".to_string(),
1207        Value::String(source_parent_region_id),
1208    );
1209    cloned_properties.insert(
1210        "coordinate_2d".to_string(),
1211        serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1212    );
1213
1214    let params = CreateCorticalAreaParams {
1215        cortical_id: new_area_id.clone(),
1216        name: request.new_name.clone(),
1217        dimensions: source_area.dimensions,
1218        position: (
1219            request.coordinates_3d[0],
1220            request.coordinates_3d[1],
1221            request.coordinates_3d[2],
1222        ),
1223        area_type: source_area.area_type.clone(),
1224        visible: Some(source_area.visible),
1225        sub_group: source_area.sub_group.clone(),
1226        neurons_per_voxel: Some(source_area.neurons_per_voxel),
1227        postsynaptic_current: Some(source_area.postsynaptic_current),
1228        plasticity_constant: Some(source_area.plasticity_constant),
1229        degeneration: Some(source_area.degeneration),
1230        psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1231        // Note: FEAGI core currently accepts scalar firing_threshold_increment on create.
1232        // We preserve full source properties above; the service layer remains authoritative.
1233        firing_threshold_increment: None,
1234        firing_threshold_limit: Some(source_area.firing_threshold_limit),
1235        consecutive_fire_count: Some(source_area.consecutive_fire_count),
1236        snooze_period: Some(source_area.snooze_period),
1237        refractory_period: Some(source_area.refractory_period),
1238        leak_coefficient: Some(source_area.leak_coefficient),
1239        leak_variability: Some(source_area.leak_variability),
1240        burst_engine_active: Some(source_area.burst_engine_active),
1241        properties: Some(cloned_properties),
1242    };
1243
1244    // Create the cloned area via GenomeService (proper flow: genome update β†’ neuroembryogenesis β†’ NPU).
1245    let created_areas = genome_service
1246        .create_cortical_areas(vec![params])
1247        .await
1248        .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1249
1250    // DIAGNOSTIC: Log what coordinates were returned after creation
1251    if let Some(created_area) = created_areas.first() {
1252        tracing::info!(target: "feagi-api",
1253            "Clone created area {} with position {:?} (requested {:?})",
1254            new_area_id, created_area.position, request.coordinates_3d
1255        );
1256    }
1257
1258    // Optionally clone cortical mappings (AutoWiring).
1259    if request.clone_cortical_mapping {
1260        // 1) Outgoing mappings: source -> dst becomes new -> dst
1261        if let Some(dst_map) = outgoing_mapping_dst {
1262            for (dst_id, rules) in dst_map {
1263                let dst_effective = if dst_id == source_id {
1264                    // Self-loop on source should become self-loop on clone.
1265                    new_area_id.clone()
1266                } else {
1267                    dst_id.clone()
1268                };
1269
1270                let Some(rules_array) = rules.as_array() else {
1271                    return Err(ApiError::invalid_input(format!(
1272                        "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1273                        dst_id, rules
1274                    )));
1275                };
1276
1277                connectome_service
1278                    .update_cortical_mapping(
1279                        new_area_id.clone(),
1280                        dst_effective,
1281                        rules_array.clone(),
1282                    )
1283                    .await
1284                    .map_err(|e| {
1285                        ApiError::internal(format!(
1286                            "Failed to clone outgoing mapping from {}: {}",
1287                            source_id, e
1288                        ))
1289                    })?;
1290            }
1291        }
1292
1293        // 2) Incoming mappings: any src -> source becomes src -> new
1294        // We discover these by scanning all areas' cortical_mapping_dst maps.
1295        let all_areas = connectome_service
1296            .list_cortical_areas()
1297            .await
1298            .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1299
1300        for area in all_areas {
1301            // Skip the source area itself: source->* already handled by outgoing clone above.
1302            if area.cortical_id == source_id {
1303                continue;
1304            }
1305
1306            let Some(dst_map) = area
1307                .properties
1308                .get("cortical_mapping_dst")
1309                .and_then(|v| v.as_object())
1310            else {
1311                continue;
1312            };
1313
1314            let Some(rules) = dst_map.get(&source_id) else {
1315                continue;
1316            };
1317
1318            let Some(rules_array) = rules.as_array() else {
1319                return Err(ApiError::invalid_input(format!(
1320                    "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1321                    area.cortical_id, source_id, rules
1322                )));
1323            };
1324
1325            connectome_service
1326                .update_cortical_mapping(
1327                    area.cortical_id.clone(),
1328                    new_area_id.clone(),
1329                    rules_array.clone(),
1330                )
1331                .await
1332                .map_err(|e| {
1333                    ApiError::internal(format!(
1334                        "Failed to clone incoming mapping into {} from {}: {}",
1335                        source_id, area.cortical_id, e
1336                    ))
1337                })?;
1338        }
1339    }
1340
1341    Ok(Json(HashMap::from([
1342        ("message".to_string(), "Cortical area cloned".to_string()),
1343        ("new_area_id".to_string(), new_area_id),
1344    ])))
1345}
1346
1347/// Request payload for POST /v1/cortical_area/clone
1348#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1349pub struct CloneCorticalAreaRequest {
1350    /// Base64 cortical area ID to clone.
1351    pub source_area_id: String,
1352    /// New cortical area name (display name).
1353    pub new_name: String,
1354    /// New 3D coordinates for placement.
1355    pub coordinates_3d: [i32; 3],
1356    /// New 2D coordinates for visualization placement.
1357    pub coordinates_2d: [i32; 2],
1358    /// Target parent brain region ID to attach the clone under.
1359    ///
1360    /// NOTE: FEAGI does NOT rely on the client for this value; it derives the parent from the
1361    /// source area’s membership. If provided and mismatched, FEAGI rejects the request.
1362    #[serde(default)]
1363    pub parent_region_id: Option<String>,
1364    /// If true, clones cortical mappings (incoming + outgoing) to reproduce wiring.
1365    pub clone_cortical_mapping: bool,
1366}
1367
1368/// Update properties of multiple cortical areas in a single request. (Not yet implemented)
1369#[utoipa::path(
1370    put,
1371    path = "/v1/cortical_area/multi/cortical_area",
1372    tag = "cortical_area"
1373)]
1374pub async fn put_multi_cortical_area(
1375    State(state): State<ApiState>,
1376    Json(mut request): Json<HashMap<String, serde_json::Value>>,
1377) -> ApiResult<Json<HashMap<String, String>>> {
1378    let genome_service = state.genome_service.as_ref();
1379
1380    // Extract cortical_id_list
1381    let cortical_ids: Vec<String> = request
1382        .get("cortical_id_list")
1383        .and_then(|v| v.as_array())
1384        .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1385        .iter()
1386        .filter_map(|v| v.as_str().map(String::from))
1387        .collect();
1388
1389    if cortical_ids.is_empty() {
1390        return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1391    }
1392
1393    tracing::debug!(
1394        target: "feagi-api",
1395        "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1396        cortical_ids.len(),
1397        request.keys().collect::<Vec<_>>()
1398    );
1399
1400    // Remove cortical_id_list from changes (it's not a property to update)
1401    request.remove("cortical_id_list");
1402
1403    // Build shared properties (applies to all unless overridden per-id)
1404    let mut shared_properties = request.clone();
1405    for cortical_id in &cortical_ids {
1406        shared_properties.remove(cortical_id);
1407    }
1408
1409    // Update each cortical area, using per-id properties when provided
1410    for cortical_id in &cortical_ids {
1411        tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1412        let mut properties = shared_properties.clone();
1413        if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1414            for (key, value) in per_id_map {
1415                properties.insert(key.clone(), value.clone());
1416            }
1417        }
1418        match genome_service
1419            .update_cortical_area(cortical_id, properties)
1420            .await
1421        {
1422            Ok(_) => {
1423                tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1424            }
1425            Err(e) => {
1426                tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1427                return Err(ApiError::internal(format!(
1428                    "Failed to update cortical area {}: {}",
1429                    cortical_id, e
1430                )));
1431            }
1432        }
1433    }
1434
1435    Ok(Json(HashMap::from([
1436        (
1437            "message".to_string(),
1438            format!("Updated {} cortical areas", cortical_ids.len()),
1439        ),
1440        ("cortical_ids".to_string(), cortical_ids.join(", ")),
1441    ])))
1442}
1443
1444/// Delete multiple cortical areas by their IDs. (Not yet implemented)
1445#[utoipa::path(
1446    delete,
1447    path = "/v1/cortical_area/multi/cortical_area",
1448    tag = "cortical_area"
1449)]
1450#[allow(unused_variables)] // In development
1451pub async fn delete_multi_cortical_area(
1452    State(state): State<ApiState>,
1453    Json(request): Json<Vec<String>>,
1454) -> ApiResult<Json<HashMap<String, String>>> {
1455    // TODO: Delete multiple cortical areas
1456    Err(ApiError::internal("Not yet implemented"))
1457}
1458
1459/// Update the 2D visualization coordinates of a cortical area. (Not yet implemented)
1460#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1461#[allow(unused_variables)] // In development
1462pub async fn put_coord_2d(
1463    State(state): State<ApiState>,
1464    Json(request): Json<HashMap<String, serde_json::Value>>,
1465) -> ApiResult<Json<HashMap<String, String>>> {
1466    // TODO: Update 2D coordinates
1467    Err(ApiError::internal("Not yet implemented"))
1468}
1469
1470/// Hide/show cortical areas in visualizations. (Not yet implemented)
1471#[utoipa::path(
1472    put,
1473    path = "/v1/cortical_area/suppress_cortical_visibility",
1474    tag = "cortical_area"
1475)]
1476#[allow(unused_variables)] // In development
1477pub async fn put_suppress_cortical_visibility(
1478    State(state): State<ApiState>,
1479    Json(request): Json<HashMap<String, serde_json::Value>>,
1480) -> ApiResult<Json<HashMap<String, String>>> {
1481    // TODO: Suppress cortical visibility
1482    Err(ApiError::internal("Not yet implemented"))
1483}
1484
1485/// Reset a cortical area to its default state (clear neuron states, etc.). (Not yet implemented)
1486#[utoipa::path(put, path = "/v1/cortical_area/reset", tag = "cortical_area")]
1487#[allow(unused_variables)] // In development
1488pub async fn put_reset(
1489    State(state): State<ApiState>,
1490    Json(request): Json<HashMap<String, String>>,
1491) -> ApiResult<Json<HashMap<String, String>>> {
1492    // TODO: Reset cortical area
1493    Err(ApiError::internal("Not yet implemented"))
1494}
1495
1496/// Check if visualization is enabled for the system.
1497#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1498pub async fn get_visualization(
1499    State(_state): State<ApiState>,
1500) -> ApiResult<Json<HashMap<String, bool>>> {
1501    let mut response = HashMap::new();
1502    response.insert("enabled".to_string(), true);
1503    Ok(Json(response))
1504}
1505
1506/// Execute multiple cortical area operations (create, update, delete) in a single batch.
1507#[utoipa::path(
1508    post,
1509    path = "/v1/cortical_area/batch_operations",
1510    tag = "cortical_area"
1511)]
1512pub async fn post_batch_operations(
1513    State(_state): State<ApiState>,
1514    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1515) -> ApiResult<Json<HashMap<String, i32>>> {
1516    let mut response = HashMap::new();
1517    response.insert("processed".to_string(), 0);
1518    Ok(Json(response))
1519}
1520
1521/// Alias for /v1/cortical_area/ipu - list all IPU cortical area IDs.
1522#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1523pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1524    get_ipu(State(state)).await
1525}
1526
1527/// Alias for /v1/cortical_area/opu - list all OPU cortical area IDs.
1528#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
1529pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1530    get_opu(State(state)).await
1531}
1532
1533/// Update the 3D position of a cortical area. (Not yet implemented)
1534#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
1535pub async fn put_coordinates_3d(
1536    State(_state): State<ApiState>,
1537    Json(_req): Json<HashMap<String, serde_json::Value>>,
1538) -> ApiResult<Json<HashMap<String, String>>> {
1539    Ok(Json(HashMap::from([(
1540        "message".to_string(),
1541        "Not yet implemented".to_string(),
1542    )])))
1543}
1544
1545/// Delete multiple cortical areas by their IDs in a single operation.
1546#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
1547pub async fn delete_bulk(
1548    State(_state): State<ApiState>,
1549    Json(_ids): Json<Vec<String>>,
1550) -> ApiResult<Json<HashMap<String, i32>>> {
1551    let mut response = HashMap::new();
1552    response.insert("deleted_count".to_string(), 0);
1553    Ok(Json(response))
1554}
1555
1556/// Resize a cortical area by changing its dimensions. (Not yet implemented)
1557#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
1558pub async fn post_resize(
1559    State(_state): State<ApiState>,
1560    Json(_req): Json<HashMap<String, serde_json::Value>>,
1561) -> ApiResult<Json<HashMap<String, String>>> {
1562    Ok(Json(HashMap::from([(
1563        "message".to_string(),
1564        "Not yet implemented".to_string(),
1565    )])))
1566}
1567
1568/// Move a cortical area to a new position. (Not yet implemented)
1569#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
1570pub async fn post_reposition(
1571    State(_state): State<ApiState>,
1572    Json(_req): Json<HashMap<String, serde_json::Value>>,
1573) -> ApiResult<Json<HashMap<String, String>>> {
1574    Ok(Json(HashMap::from([(
1575        "message".to_string(),
1576        "Not yet implemented".to_string(),
1577    )])))
1578}
1579
1580/// Get neurons at specific voxel coordinates within a cortical area.
1581#[utoipa::path(post, path = "/v1/cortical_area/voxel_neurons", tag = "cortical_area")]
1582pub async fn post_voxel_neurons(
1583    State(_state): State<ApiState>,
1584    Json(_req): Json<HashMap<String, serde_json::Value>>,
1585) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1586    let mut response = HashMap::new();
1587    response.insert("neurons".to_string(), serde_json::json!([]));
1588    Ok(Json(response))
1589}
1590
1591/// Get metadata for all available IPU types (vision, infrared, etc.). Includes encodings, formats, units, and topology.
1592#[utoipa::path(
1593    get,
1594    path = "/v1/cortical_area/ipu/types",
1595    tag = "cortical_area",
1596    responses(
1597        (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
1598        (status = 500, description = "Internal server error")
1599    )
1600)]
1601pub async fn get_ipu_types(
1602    State(_state): State<ApiState>,
1603) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
1604    let mut types = HashMap::new();
1605
1606    // Dynamically generate metadata from feagi_data_structures templates
1607    for unit in SensoryCorticalUnit::list_all() {
1608        let id_ref = unit.get_cortical_id_unit_reference();
1609        let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
1610
1611        // All IPU types support both absolute and incremental encodings
1612        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
1613
1614        // Determine if formats are supported based on snake_case_name
1615        // Vision and SegmentedVision use CartesianPlane (no formats)
1616        // MiscData uses Misc (no formats)
1617        // All others use Percentage-based types (have formats)
1618        let snake_name = unit.get_snake_case_name();
1619        let formats = if snake_name == "vision"
1620            || snake_name == "segmented_vision"
1621            || snake_name == "miscellaneous"
1622        {
1623            vec![]
1624        } else {
1625            vec!["linear".to_string(), "fractional".to_string()]
1626        };
1627
1628        // Default resolution based on type
1629        let resolution = if snake_name == "vision" {
1630            vec![64, 64, 1] // Vision sensors typically 64x64
1631        } else if snake_name == "segmented_vision" {
1632            vec![32, 32, 1] // Segmented vision segments are smaller
1633        } else {
1634            vec![1, 1, 1] // Most sensors are scalar (1x1x1)
1635        };
1636
1637        // Most sensors are asymmetric
1638        let structure = "asymmetric".to_string();
1639
1640        // Get unit default topology
1641        let topology_map = unit.get_unit_default_topology();
1642        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
1643            .into_iter()
1644            .map(|(idx, topo)| {
1645                (
1646                    *idx as usize,
1647                    UnitTopologyData {
1648                        relative_position: topo.relative_position,
1649                        dimensions: topo.channel_dimensions_default,
1650                    },
1651                )
1652            })
1653            .collect();
1654
1655        types.insert(
1656            key,
1657            CorticalTypeMetadata {
1658                description: unit.get_friendly_name().to_string(),
1659                encodings,
1660                formats,
1661                units: unit.get_number_cortical_areas() as u32,
1662                resolution,
1663                structure,
1664                unit_default_topology,
1665            },
1666        );
1667    }
1668
1669    Ok(Json(types))
1670}
1671
1672/// Get metadata for all available OPU types (motors, servos, etc.). Includes encodings, formats, units, and topology.
1673#[utoipa::path(
1674    get,
1675    path = "/v1/cortical_area/opu/types",
1676    tag = "cortical_area",
1677    responses(
1678        (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
1679        (status = 500, description = "Internal server error")
1680    )
1681)]
1682pub async fn get_opu_types(
1683    State(_state): State<ApiState>,
1684) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
1685    let mut types = HashMap::new();
1686
1687    // Dynamically generate metadata from feagi_data_structures templates
1688    for unit in MotorCorticalUnit::list_all() {
1689        let id_ref = unit.get_cortical_id_unit_reference();
1690        let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
1691
1692        // All OPU types support both absolute and incremental encodings
1693        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
1694
1695        // Determine if formats are supported based on snake_case_name
1696        // MiscData uses Misc (no formats)
1697        // All others use Percentage-based types (have formats)
1698        let snake_name = unit.get_snake_case_name();
1699        let formats = if snake_name == "miscellaneous" {
1700            vec![]
1701        } else {
1702            vec!["linear".to_string(), "fractional".to_string()]
1703        };
1704
1705        // Default resolution - all motors/actuators are typically scalar
1706        let resolution = vec![1, 1, 1];
1707
1708        // All actuators are asymmetric
1709        let structure = "asymmetric".to_string();
1710
1711        // Get unit default topology
1712        let topology_map = unit.get_unit_default_topology();
1713        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
1714            .into_iter()
1715            .map(|(idx, topo)| {
1716                (
1717                    *idx as usize,
1718                    UnitTopologyData {
1719                        relative_position: topo.relative_position,
1720                        dimensions: topo.channel_dimensions_default,
1721                    },
1722                )
1723            })
1724            .collect();
1725
1726        types.insert(
1727            key,
1728            CorticalTypeMetadata {
1729                description: unit.get_friendly_name().to_string(),
1730                encodings,
1731                formats,
1732                units: unit.get_number_cortical_areas() as u32,
1733                resolution,
1734                structure,
1735                unit_default_topology,
1736            },
1737        );
1738    }
1739
1740    Ok(Json(types))
1741}
1742
1743/// Get list of all cortical area indices (numerical indices used internally for indexing).
1744#[utoipa::path(
1745    get,
1746    path = "/v1/cortical_area/cortical_area_index_list",
1747    tag = "cortical_area"
1748)]
1749pub async fn get_cortical_area_index_list(
1750    State(state): State<ApiState>,
1751) -> ApiResult<Json<Vec<u32>>> {
1752    let connectome_service = state.connectome_service.as_ref();
1753    let areas = connectome_service
1754        .list_cortical_areas()
1755        .await
1756        .map_err(|e| ApiError::internal(format!("{}", e)))?;
1757    // CRITICAL FIX: Return the actual cortical_idx values, not fabricated sequential indices
1758    let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
1759    Ok(Json(indices))
1760}
1761
1762/// Get mapping from cortical area IDs to their internal indices. Returns {cortical_id: index}.
1763#[utoipa::path(
1764    get,
1765    path = "/v1/cortical_area/cortical_idx_mapping",
1766    tag = "cortical_area"
1767)]
1768pub async fn get_cortical_idx_mapping(
1769    State(state): State<ApiState>,
1770) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
1771    use std::collections::BTreeMap;
1772
1773    let connectome_service = state.connectome_service.as_ref();
1774    let areas = connectome_service
1775        .list_cortical_areas()
1776        .await
1777        .map_err(|e| ApiError::internal(format!("{}", e)))?;
1778    // CRITICAL FIX: Use the actual cortical_idx from CorticalArea, NOT enumerate() which ignores reserved indices!
1779    // Use BTreeMap for consistent alphabetical ordering
1780    let mapping: BTreeMap<String, u32> = areas
1781        .iter()
1782        .map(|a| (a.cortical_id.clone(), a.cortical_idx))
1783        .collect();
1784    Ok(Json(mapping))
1785}
1786
1787/// Get restrictions on which cortical areas can connect to which (connection validation rules).
1788#[utoipa::path(
1789    get,
1790    path = "/v1/cortical_area/mapping_restrictions",
1791    tag = "cortical_area"
1792)]
1793pub async fn get_mapping_restrictions_query(
1794    State(_state): State<ApiState>,
1795    Query(_params): Query<HashMap<String, String>>,
1796) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1797    Ok(Json(HashMap::new()))
1798}
1799
1800/// Get memory usage of a specific cortical area in bytes (calculated from neuron count).
1801#[utoipa::path(
1802    get,
1803    path = "/v1/cortical_area/{cortical_id}/memory_usage",
1804    tag = "cortical_area"
1805)]
1806pub async fn get_memory_usage(
1807    State(state): State<ApiState>,
1808    Path(cortical_id): Path<String>,
1809) -> ApiResult<Json<HashMap<String, i64>>> {
1810    let connectome_service = state.connectome_service.as_ref();
1811
1812    // CRITICAL FIX: Calculate actual memory usage based on neuron count instead of hardcoded 0
1813    let area_info = connectome_service
1814        .get_cortical_area(&cortical_id)
1815        .await
1816        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
1817
1818    // Calculate memory usage: neuron_count Γ— bytes per neuron
1819    // Each neuron in NeuronArray uses ~48 bytes (membrane_potential, threshold, refractory, etc.)
1820    const BYTES_PER_NEURON: i64 = 48;
1821    let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
1822
1823    let mut response = HashMap::new();
1824    response.insert("memory_bytes".to_string(), memory_bytes);
1825    Ok(Json(response))
1826}
1827
1828/// Get the total number of neurons in a specific cortical area.
1829#[utoipa::path(
1830    get,
1831    path = "/v1/cortical_area/{cortical_id}/neuron_count",
1832    tag = "cortical_area"
1833)]
1834pub async fn get_area_neuron_count(
1835    State(state): State<ApiState>,
1836    Path(cortical_id): Path<String>,
1837) -> ApiResult<Json<i64>> {
1838    let connectome_service = state.connectome_service.as_ref();
1839
1840    // CRITICAL FIX: Get actual neuron count from ConnectomeService instead of hardcoded 0
1841    let area_info = connectome_service
1842        .get_cortical_area(&cortical_id)
1843        .await
1844        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
1845
1846    Ok(Json(area_info.neuron_count as i64))
1847}
1848
1849/// Get available cortical type options for UI selection: Sensory, Motor, Custom, Memory.
1850#[utoipa::path(
1851    post,
1852    path = "/v1/cortical_area/cortical_type_options",
1853    tag = "cortical_area"
1854)]
1855pub async fn post_cortical_type_options(
1856    State(_state): State<ApiState>,
1857) -> ApiResult<Json<Vec<String>>> {
1858    Ok(Json(vec![
1859        "Sensory".to_string(),
1860        "Motor".to_string(),
1861        "Custom".to_string(),
1862        "Memory".to_string(),
1863    ]))
1864}
1865
1866/// Get mapping restrictions for specific cortical areas (POST version with request body).
1867#[utoipa::path(
1868    post,
1869    path = "/v1/cortical_area/mapping_restrictions",
1870    tag = "cortical_area"
1871)]
1872pub async fn post_mapping_restrictions(
1873    State(_state): State<ApiState>,
1874    Json(_req): Json<HashMap<String, String>>,
1875) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1876    Ok(Json(HashMap::new()))
1877}
1878
1879/// Get mapping restrictions between two specific cortical areas (connection validation).
1880#[utoipa::path(
1881    post,
1882    path = "/v1/cortical_area/mapping_restrictions_between_areas",
1883    tag = "cortical_area"
1884)]
1885pub async fn post_mapping_restrictions_between_areas(
1886    State(_state): State<ApiState>,
1887    Json(_req): Json<HashMap<String, String>>,
1888) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1889    Ok(Json(HashMap::new()))
1890}
1891
1892/// Update 3D coordinates of a cortical area (alternative endpoint). (Not yet implemented)
1893#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
1894pub async fn put_coord_3d(
1895    State(_state): State<ApiState>,
1896    Json(_req): Json<HashMap<String, serde_json::Value>>,
1897) -> ApiResult<Json<HashMap<String, String>>> {
1898    Ok(Json(HashMap::from([(
1899        "message".to_string(),
1900        "Not yet implemented".to_string(),
1901    )])))
1902}