Skip to main content

feagi_api/endpoints/
cortical_area.rs

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