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