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    // BREAKING CHANGE (unreleased API):
866    // `data_type_config` is now per-subunit, because some cortical units have heterogeneous
867    // subunits (e.g. Gaze: Percentage2D + Percentage).
868    //
869    // Request must provide:
870    //   data_type_configs_by_subunit: { "0": <u16>, "1": <u16>, ... }
871    let raw_configs = request
872        .get("data_type_configs_by_subunit")
873        .and_then(|v| v.as_object())
874        .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
875
876    let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
877
878    for (k, v) in raw_configs {
879        let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
880            ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
881        })?;
882        let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
883            ApiError::invalid_input("data_type_configs_by_subunit key out of range")
884        })?;
885
886        let parsed_u64 = if let Some(u) = v.as_u64() {
887            Some(u)
888        } else if let Some(i) = v.as_i64() {
889            if i >= 0 {
890                Some(i as u64)
891            } else {
892                None
893            }
894        } else if let Some(f) = v.as_f64() {
895            if f >= 0.0 {
896                Some(f.round() as u64)
897            } else {
898                None
899            }
900        } else if let Some(s) = v.as_str() {
901            s.parse::<u64>().ok()
902        } else {
903            None
904        }
905        .ok_or_else(|| {
906            ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
907        })?;
908
909        if parsed_u64 > u16::MAX as u64 {
910            return Err(ApiError::invalid_input(
911                "data_type_configs_by_subunit value exceeds u16::MAX",
912            ));
913        }
914
915        data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
916    }
917
918    tracing::info!(
919        target: "feagi-api",
920        "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
921        cortical_type_key,
922        neurons_per_voxel,
923        data_type_configs_by_subunit
924    );
925
926    // Determine number of units and get topology
927    let (num_units, unit_topology) = if cortical_type_str == "IPU" {
928        // Find the matching sensory cortical unit
929        let unit = SensoryCorticalUnit::list_all()
930            .iter()
931            .find(|u| {
932                let id_ref = u.get_cortical_id_unit_reference();
933                let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
934                key == cortical_type_key
935            })
936            .ok_or_else(|| {
937                ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
938            })?;
939
940        (
941            unit.get_number_cortical_areas(),
942            unit.get_unit_default_topology(),
943        )
944    } else if cortical_type_str == "OPU" {
945        // Find the matching motor cortical unit
946        let unit = MotorCorticalUnit::list_all()
947            .iter()
948            .find(|u| {
949                let id_ref = u.get_cortical_id_unit_reference();
950                let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
951                key == cortical_type_key
952            })
953            .ok_or_else(|| {
954                ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
955            })?;
956
957        (
958            unit.get_number_cortical_areas(),
959            unit.get_unit_default_topology(),
960        )
961    } else {
962        return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
963    };
964
965    tracing::info!(
966        "Creating {} units for cortical type: {}",
967        num_units,
968        cortical_type_key
969    );
970
971    // Build creation parameters for all units
972    let mut creation_params = Vec::new();
973    for unit_idx in 0..num_units {
974        let data_type_config = data_type_configs_by_subunit
975            .get(&(unit_idx as u8))
976            .copied()
977            .ok_or_else(|| {
978                ApiError::invalid_input(format!(
979                    "data_type_configs_by_subunit missing entry for subunit {}",
980                    unit_idx
981                ))
982            })?;
983
984        // Split per-subunit data_type_config into two bytes for cortical ID
985        let config_byte_4 = (data_type_config & 0xFF) as u8; // Lower byte
986        let config_byte_5 = ((data_type_config >> 8) & 0xFF) as u8; // Upper byte
987
988        // Get per-device dimensions from topology, then scale X by device_count:
989        // total_x = device_count * per_device_x
990        let (per_device_dimensions, dimensions) =
991            if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
992                let dims = topo.channel_dimensions_default;
993                let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
994                let total_x = per_device.0.saturating_mul(device_count);
995                (per_device, (total_x, per_device.1, per_device.2))
996            } else {
997                ((1, 1, 1), (device_count.max(1), 1, 1)) // Fallback
998            };
999
1000        // Calculate position for this unit
1001        let position =
1002            if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1003                let rel_pos = topo.relative_position;
1004                (
1005                    coordinates_3d[0] + rel_pos[0],
1006                    coordinates_3d[1] + rel_pos[1],
1007                    coordinates_3d[2] + rel_pos[2],
1008                )
1009            } else {
1010                (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
1011            };
1012
1013        // Construct proper 8-byte cortical ID
1014        // Byte structure: [type(i/o), subtype[0], subtype[1], subtype[2], encoding_type, encoding_format, unit_idx, group_id]
1015        // Extract the 3-character subtype from cortical_type_key (e.g., "isvi" -> "svi")
1016        let subtype_bytes = if cortical_type_key.len() >= 4 {
1017            let subtype_str = &cortical_type_key[1..4]; // Skip the 'i' or 'o' prefix
1018            let mut bytes = [0u8; 3];
1019            for (i, c) in subtype_str.chars().take(3).enumerate() {
1020                bytes[i] = c as u8;
1021            }
1022            bytes
1023        } else {
1024            return Err(ApiError::invalid_input("Invalid cortical_type_key"));
1025        };
1026
1027        // Construct the 8-byte cortical ID
1028        let cortical_id_bytes = [
1029            if cortical_type_str == "IPU" {
1030                b'i'
1031            } else {
1032                b'o'
1033            }, // Byte 0: type
1034            subtype_bytes[0], // Byte 1: subtype[0]
1035            subtype_bytes[1], // Byte 2: subtype[1]
1036            subtype_bytes[2], // Byte 3: subtype[2]
1037            config_byte_4,    // Byte 4: data type config (lower byte)
1038            config_byte_5,    // Byte 5: data type config (upper byte)
1039            unit_idx as u8,   // Byte 6: unit index
1040            group_id,         // Byte 7: group ID
1041        ];
1042
1043        // Encode to base64 for use as cortical_id string
1044        let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1045
1046        tracing::debug!(target: "feagi-api",
1047            "  Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
1048            unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
1049            dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
1050        );
1051
1052        // Store device_count and per-device dimensions in properties for BV compatibility
1053        let mut properties = HashMap::new();
1054        properties.insert(
1055            "dev_count".to_string(),
1056            serde_json::Value::Number(serde_json::Number::from(device_count)),
1057        );
1058        properties.insert(
1059            "cortical_dimensions_per_device".to_string(),
1060            serde_json::json!([
1061                per_device_dimensions.0,
1062                per_device_dimensions.1,
1063                per_device_dimensions.2
1064            ]),
1065        );
1066
1067        let params = CreateCorticalAreaParams {
1068            cortical_id: cortical_id.clone(),
1069            name: format!("{} Unit {}", cortical_type_key, unit_idx),
1070            dimensions,
1071            position,
1072            area_type: cortical_type_str.to_string(),
1073            visible: Some(true),
1074            sub_group: None,
1075            neurons_per_voxel: Some(neurons_per_voxel),
1076            postsynaptic_current: Some(0.0),
1077            plasticity_constant: Some(0.0),
1078            degeneration: Some(0.0),
1079            psp_uniform_distribution: Some(false),
1080            firing_threshold_increment: Some(0.0),
1081            firing_threshold_limit: Some(0.0),
1082            consecutive_fire_count: Some(0),
1083            snooze_period: Some(0),
1084            refractory_period: Some(0),
1085            leak_coefficient: Some(0.0),
1086            leak_variability: Some(0.0),
1087            burst_engine_active: Some(true),
1088            properties: Some(properties),
1089        };
1090
1091        creation_params.push(params);
1092    }
1093
1094    tracing::info!(
1095        "Calling GenomeService to create {} cortical areas",
1096        creation_params.len()
1097    );
1098
1099    // ARCHITECTURE: Call genome_service.create_cortical_areas (proper flow)
1100    // This will: 1) Update runtime genome, 2) Call neuroembryogenesis, 3) Create neurons/synapses
1101    let areas_details = genome_service
1102        .create_cortical_areas(creation_params)
1103        .await
1104        .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
1105
1106    tracing::info!(
1107        "✅ Successfully created {} cortical areas via GenomeService",
1108        areas_details.len()
1109    );
1110
1111    // Serialize as JSON
1112    let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
1113
1114    // Extract cortical IDs from created areas
1115    let created_ids: Vec<String> = areas_details
1116        .iter()
1117        .map(|a| a.cortical_id.clone())
1118        .collect();
1119
1120    // Return comprehensive response
1121    let first_id = created_ids.first().cloned().unwrap_or_default();
1122    let mut response = serde_json::Map::new();
1123    response.insert(
1124        "message".to_string(),
1125        serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
1126    );
1127    response.insert(
1128        "cortical_id".to_string(),
1129        serde_json::Value::String(first_id),
1130    ); // For backward compatibility
1131    response.insert(
1132        "cortical_ids".to_string(),
1133        serde_json::Value::String(created_ids.join(", ")),
1134    );
1135    response.insert(
1136        "unit_count".to_string(),
1137        serde_json::Value::Number(created_ids.len().into()),
1138    );
1139    response.insert("areas".to_string(), areas_json); // Full details for all areas
1140
1141    Ok(Json(serde_json::Value::Object(response)))
1142}
1143
1144/// Update properties of an existing cortical area (position, dimensions, neural parameters, etc.).
1145#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
1146pub async fn put_cortical_area(
1147    State(state): State<ApiState>,
1148    Json(mut request): Json<HashMap<String, serde_json::Value>>,
1149) -> ApiResult<Json<HashMap<String, String>>> {
1150    let genome_service = state.genome_service.as_ref();
1151
1152    // Extract cortical_id
1153    let cortical_id = request
1154        .get("cortical_id")
1155        .and_then(|v| v.as_str())
1156        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
1157        .to_string();
1158
1159    tracing::debug!(
1160        target: "feagi-api",
1161        "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
1162        cortical_id,
1163        request.keys().collect::<Vec<_>>()
1164    );
1165
1166    // Remove cortical_id from changes (it's not a property to update)
1167    request.remove("cortical_id");
1168
1169    // Call GenomeService with raw changes (it handles classification and routing)
1170    match genome_service
1171        .update_cortical_area(&cortical_id, request)
1172        .await
1173    {
1174        Ok(area_info) => {
1175            let updated_id = area_info.cortical_id.clone();
1176            tracing::debug!(
1177                target: "feagi-api",
1178                "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
1179                cortical_id,
1180                updated_id
1181            );
1182            Ok(Json(HashMap::from([
1183                ("message".to_string(), "Cortical area updated".to_string()),
1184                ("cortical_id".to_string(), updated_id),
1185                ("previous_cortical_id".to_string(), cortical_id),
1186            ])))
1187        }
1188        Err(e) => {
1189            tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
1190            Err(ApiError::internal(format!("Failed to update: {}", e)))
1191        }
1192    }
1193}
1194
1195/// Delete a cortical area by ID. Removes the area and all associated neurons and synapses.
1196#[utoipa::path(
1197    delete,
1198    path = "/v1/cortical_area/cortical_area",
1199    tag = "cortical_area"
1200)]
1201#[allow(unused_variables)] // In development - parameters will be used when implemented
1202pub async fn delete_cortical_area(
1203    State(state): State<ApiState>,
1204    Json(request): Json<HashMap<String, String>>,
1205) -> ApiResult<Json<HashMap<String, String>>> {
1206    let connectome_service = state.connectome_service.as_ref();
1207    let cortical_id = request
1208        .get("cortical_id")
1209        .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
1210
1211    match connectome_service.delete_cortical_area(cortical_id).await {
1212        Ok(_) => Ok(Json(HashMap::from([(
1213            "message".to_string(),
1214            "Cortical area deleted".to_string(),
1215        )]))),
1216        Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
1217    }
1218}
1219
1220/// Create a custom cortical area for internal processing with specified dimensions and position.
1221#[utoipa::path(
1222    post,
1223    path = "/v1/cortical_area/custom_cortical_area",
1224    tag = "cortical_area"
1225)]
1226pub async fn post_custom_cortical_area(
1227    State(state): State<ApiState>,
1228    Json(request): Json<HashMap<String, serde_json::Value>>,
1229) -> ApiResult<Json<HashMap<String, String>>> {
1230    use feagi_services::types::CreateCorticalAreaParams;
1231    use std::time::{SystemTime, UNIX_EPOCH};
1232
1233    // Helper: check whether BV is requesting a MEMORY cortical area (still routed through this endpoint).
1234    //
1235    // Brain Visualizer sends:
1236    //   sub_group_id: "MEMORY"
1237    //   cortical_group: "CUSTOM"
1238    //
1239    // In feagi-core, the authoritative cortical type is derived from the CorticalID prefix byte:
1240    // - b'c' => Custom
1241    // - b'm' => Memory
1242    //
1243    // So if sub_group_id indicates MEMORY, we must generate an 'm' prefixed CorticalID.
1244    let is_memory_area_requested = request
1245        .get("sub_group_id")
1246        .and_then(|v| v.as_str())
1247        .map(|s| s.eq_ignore_ascii_case("MEMORY"))
1248        .unwrap_or(false);
1249
1250    // Extract required fields from request
1251    let cortical_name = request
1252        .get("cortical_name")
1253        .and_then(|v| v.as_str())
1254        .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
1255
1256    let cortical_dimensions: Vec<u32> = request
1257        .get("cortical_dimensions")
1258        .and_then(|v| v.as_array())
1259        .and_then(|arr| {
1260            if arr.len() == 3 {
1261                Some(vec![
1262                    arr[0].as_u64()? as u32,
1263                    arr[1].as_u64()? as u32,
1264                    arr[2].as_u64()? as u32,
1265                ])
1266            } else {
1267                None
1268            }
1269        })
1270        .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
1271
1272    let coordinates_3d: Vec<i32> = request
1273        .get("coordinates_3d")
1274        .and_then(|v| v.as_array())
1275        .and_then(|arr| {
1276            if arr.len() == 3 {
1277                Some(vec![
1278                    arr[0].as_i64()? as i32,
1279                    arr[1].as_i64()? as i32,
1280                    arr[2].as_i64()? as i32,
1281                ])
1282            } else {
1283                None
1284            }
1285        })
1286        .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
1287
1288    let brain_region_id = request
1289        .get("brain_region_id")
1290        .and_then(|v| v.as_str())
1291        .map(|s| s.to_string());
1292
1293    let cortical_sub_group = request
1294        .get("cortical_sub_group")
1295        .and_then(|v| v.as_str())
1296        .filter(|s| !s.is_empty())
1297        .map(|s| s.to_string());
1298
1299    tracing::info!(target: "feagi-api",
1300        "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
1301        if is_memory_area_requested { "memory" } else { "custom" },
1302        cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
1303        coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
1304    );
1305
1306    // Generate unique cortical ID for custom cortical area
1307    // Format: [b'c', 6 random alphanumeric bytes, group_counter]
1308    // Use timestamp + counter to ensure uniqueness
1309    let timestamp = SystemTime::now()
1310        .duration_since(UNIX_EPOCH)
1311        .unwrap()
1312        .as_millis() as u64;
1313
1314    // Create 8-byte cortical ID for custom/memory area
1315    // Byte 0: 'c' for custom OR 'm' for memory (authoritative type discriminator)
1316    // Bytes 1-6: Derived from name (first 6 chars, padded with underscores)
1317    // Byte 7: Counter based on timestamp lower bits
1318    let mut cortical_id_bytes = [0u8; 8];
1319    cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
1320
1321    // Use the cortical name for bytes 1-6 (truncate or pad as needed)
1322    let name_bytes = cortical_name.as_bytes();
1323    for i in 1..7 {
1324        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1325            // Use alphanumeric ASCII only
1326            let c = name_bytes[i - 1];
1327            if c.is_ascii_alphanumeric() || c == b'_' {
1328                c
1329            } else {
1330                b'_'
1331            }
1332        } else {
1333            b'_' // Padding
1334        };
1335    }
1336
1337    // Byte 7: Use timestamp lower byte for uniqueness
1338    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1339
1340    // Encode to base64 for use as cortical_id string
1341    let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1342
1343    tracing::debug!(target: "feagi-api",
1344        "Generated cortical_id: {} (raw bytes: {:?})",
1345        cortical_id, cortical_id_bytes
1346    );
1347
1348    // Build properties with brain_region_id if provided
1349    let mut properties = HashMap::new();
1350    if let Some(region_id) = brain_region_id.clone() {
1351        properties.insert(
1352            "parent_region_id".to_string(),
1353            serde_json::Value::String(region_id),
1354        );
1355    }
1356
1357    // Create cortical area parameters
1358    let params = CreateCorticalAreaParams {
1359        cortical_id: cortical_id.clone(),
1360        name: cortical_name.to_string(),
1361        dimensions: (
1362            cortical_dimensions[0] as usize,
1363            cortical_dimensions[1] as usize,
1364            cortical_dimensions[2] as usize,
1365        ),
1366        position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1367        area_type: if is_memory_area_requested {
1368            "Memory".to_string()
1369        } else {
1370            "Custom".to_string()
1371        },
1372        visible: Some(true),
1373        sub_group: cortical_sub_group,
1374        neurons_per_voxel: Some(1),
1375        postsynaptic_current: None,
1376        plasticity_constant: Some(0.0),
1377        degeneration: Some(0.0),
1378        psp_uniform_distribution: Some(false),
1379        firing_threshold_increment: Some(0.0),
1380        firing_threshold_limit: Some(0.0),
1381        consecutive_fire_count: Some(0),
1382        snooze_period: Some(0),
1383        refractory_period: Some(0),
1384        leak_coefficient: Some(0.0),
1385        leak_variability: Some(0.0),
1386        burst_engine_active: Some(true),
1387        properties: Some(properties),
1388    };
1389
1390    let genome_service = state.genome_service.as_ref();
1391
1392    tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1393
1394    // Create the cortical area via GenomeService
1395    let areas_details = genome_service
1396        .create_cortical_areas(vec![params])
1397        .await
1398        .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1399
1400    let created_area = areas_details
1401        .first()
1402        .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1403
1404    tracing::info!(target: "feagi-api",
1405        "✅ Successfully created custom cortical area '{}' with ID: {}",
1406        cortical_name, created_area.cortical_id
1407    );
1408
1409    // Return response
1410    let mut response = HashMap::new();
1411    response.insert(
1412        "message".to_string(),
1413        "Custom cortical area created successfully".to_string(),
1414    );
1415    response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1416    response.insert("cortical_name".to_string(), cortical_name.to_string());
1417
1418    Ok(Json(response))
1419}
1420
1421/// Clone an existing cortical area with all its properties and structure. (Not yet implemented)
1422#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1423pub async fn post_clone(
1424    State(state): State<ApiState>,
1425    Json(request): Json<CloneCorticalAreaRequest>,
1426) -> ApiResult<Json<HashMap<String, String>>> {
1427    use base64::{engine::general_purpose, Engine as _};
1428    use feagi_services::types::CreateCorticalAreaParams;
1429    use feagi_structures::genomic::cortical_area::CorticalID;
1430    use serde_json::Value;
1431    use std::time::{SystemTime, UNIX_EPOCH};
1432
1433    let genome_service = state.genome_service.as_ref();
1434    let connectome_service = state.connectome_service.as_ref();
1435
1436    // Resolve + validate source cortical ID.
1437    let source_id = request.source_area_id.clone();
1438    let source_typed = CorticalID::try_from_base_64(&source_id)
1439        .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1440    let src_first_byte = source_typed.as_bytes()[0];
1441    if src_first_byte != b'c' && src_first_byte != b'm' {
1442        return Err(ApiError::invalid_input(format!(
1443            "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1444            src_first_byte
1445        )));
1446    }
1447
1448    // Fetch full source info (dimensions, neural params, properties, mappings).
1449    let source_area = connectome_service
1450        .get_cortical_area(&source_id)
1451        .await
1452        .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1453
1454    // FEAGI is the source of truth for brain-region membership.
1455    //
1456    // Do NOT trust the client/UI to provide parent_region_id correctly, because FEAGI already
1457    // knows the source area’s parent. We use FEAGI’s view of parent_region_id for persistence.
1458    //
1459    // If the client provides parent_region_id and it disagrees, fail fast to prevent ambiguity.
1460    let source_parent_region_id = source_area
1461        .parent_region_id
1462        .clone()
1463        .or_else(|| {
1464            source_area
1465                .properties
1466                .get("parent_region_id")
1467                .and_then(|v| v.as_str())
1468                .map(|s| s.to_string())
1469        })
1470        .ok_or_else(|| {
1471            ApiError::internal(format!(
1472                "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1473                source_id
1474            ))
1475        })?;
1476
1477    if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1478        if client_parent_region_id != &source_parent_region_id {
1479            return Err(ApiError::invalid_input(format!(
1480                "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1481                client_parent_region_id, source_id, source_parent_region_id
1482            )));
1483        }
1484    }
1485
1486    // Extract outgoing mappings (we will apply them after creation, via update_cortical_mapping).
1487    let outgoing_mapping_dst = source_area
1488        .properties
1489        .get("cortical_mapping_dst")
1490        .and_then(|v| v.as_object())
1491        .cloned();
1492
1493    // Generate unique cortical ID for the clone.
1494    //
1495    // Rules:
1496    // - Byte 0 keeps the source type discriminator (b'c' or b'm')
1497    // - Bytes 1-6 derived from new_name (alphanumeric/_ only)
1498    // - Byte 7 timestamp lower byte for uniqueness
1499    let timestamp = SystemTime::now()
1500        .duration_since(UNIX_EPOCH)
1501        .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1502        .as_millis() as u64;
1503
1504    let mut cortical_id_bytes = [0u8; 8];
1505    cortical_id_bytes[0] = src_first_byte;
1506
1507    let name_bytes = request.new_name.as_bytes();
1508    for i in 1..7 {
1509        cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1510            let c = name_bytes[i - 1];
1511            if c.is_ascii_alphanumeric() || c == b'_' {
1512                c
1513            } else {
1514                b'_'
1515            }
1516        } else {
1517            b'_'
1518        };
1519    }
1520    cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1521
1522    let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1523
1524    // Clone properties, but do NOT carry over cortical mapping properties directly.
1525    // Mappings must be created via update_cortical_mapping so synapses are regenerated.
1526    let mut cloned_properties = source_area.properties.clone();
1527    cloned_properties.remove("cortical_mapping_dst");
1528
1529    // Set parent region + 2D coordinate explicitly for the clone.
1530    cloned_properties.insert(
1531        "parent_region_id".to_string(),
1532        Value::String(source_parent_region_id),
1533    );
1534    cloned_properties.insert(
1535        "coordinate_2d".to_string(),
1536        serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1537    );
1538
1539    let params = CreateCorticalAreaParams {
1540        cortical_id: new_area_id.clone(),
1541        name: request.new_name.clone(),
1542        dimensions: source_area.dimensions,
1543        position: (
1544            request.coordinates_3d[0],
1545            request.coordinates_3d[1],
1546            request.coordinates_3d[2],
1547        ),
1548        area_type: source_area.area_type.clone(),
1549        visible: Some(source_area.visible),
1550        sub_group: source_area.sub_group.clone(),
1551        neurons_per_voxel: Some(source_area.neurons_per_voxel),
1552        postsynaptic_current: Some(source_area.postsynaptic_current),
1553        plasticity_constant: Some(source_area.plasticity_constant),
1554        degeneration: Some(source_area.degeneration),
1555        psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1556        // Note: FEAGI core currently accepts scalar firing_threshold_increment on create.
1557        // We preserve full source properties above; the service layer remains authoritative.
1558        firing_threshold_increment: None,
1559        firing_threshold_limit: Some(source_area.firing_threshold_limit),
1560        consecutive_fire_count: Some(source_area.consecutive_fire_count),
1561        snooze_period: Some(source_area.snooze_period),
1562        refractory_period: Some(source_area.refractory_period),
1563        leak_coefficient: Some(source_area.leak_coefficient),
1564        leak_variability: Some(source_area.leak_variability),
1565        burst_engine_active: Some(source_area.burst_engine_active),
1566        properties: Some(cloned_properties),
1567    };
1568
1569    // Create the cloned area via GenomeService (proper flow: genome update → neuroembryogenesis → NPU).
1570    let created_areas = genome_service
1571        .create_cortical_areas(vec![params])
1572        .await
1573        .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1574
1575    // DIAGNOSTIC: Log what coordinates were returned after creation
1576    if let Some(created_area) = created_areas.first() {
1577        tracing::info!(target: "feagi-api",
1578            "Clone created area {} with position {:?} (requested {:?})",
1579            new_area_id, created_area.position, request.coordinates_3d
1580        );
1581    }
1582
1583    // Optionally clone cortical mappings (AutoWiring).
1584    if request.clone_cortical_mapping {
1585        // 1) Outgoing mappings: source -> dst becomes new -> dst
1586        if let Some(dst_map) = outgoing_mapping_dst {
1587            for (dst_id, rules) in dst_map {
1588                let dst_effective = if dst_id == source_id {
1589                    // Self-loop on source should become self-loop on clone.
1590                    new_area_id.clone()
1591                } else {
1592                    dst_id.clone()
1593                };
1594
1595                let Some(rules_array) = rules.as_array() else {
1596                    return Err(ApiError::invalid_input(format!(
1597                        "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1598                        dst_id, rules
1599                    )));
1600                };
1601
1602                connectome_service
1603                    .update_cortical_mapping(
1604                        new_area_id.clone(),
1605                        dst_effective,
1606                        rules_array.clone(),
1607                    )
1608                    .await
1609                    .map_err(|e| {
1610                        ApiError::internal(format!(
1611                            "Failed to clone outgoing mapping from {}: {}",
1612                            source_id, e
1613                        ))
1614                    })?;
1615            }
1616        }
1617
1618        // 2) Incoming mappings: any src -> source becomes src -> new
1619        // We discover these by scanning all areas' cortical_mapping_dst maps.
1620        let all_areas = connectome_service
1621            .list_cortical_areas()
1622            .await
1623            .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1624
1625        for area in all_areas {
1626            // Skip the source area itself: source->* already handled by outgoing clone above.
1627            if area.cortical_id == source_id {
1628                continue;
1629            }
1630
1631            let Some(dst_map) = area
1632                .properties
1633                .get("cortical_mapping_dst")
1634                .and_then(|v| v.as_object())
1635            else {
1636                continue;
1637            };
1638
1639            let Some(rules) = dst_map.get(&source_id) else {
1640                continue;
1641            };
1642
1643            let Some(rules_array) = rules.as_array() else {
1644                return Err(ApiError::invalid_input(format!(
1645                    "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1646                    area.cortical_id, source_id, rules
1647                )));
1648            };
1649
1650            connectome_service
1651                .update_cortical_mapping(
1652                    area.cortical_id.clone(),
1653                    new_area_id.clone(),
1654                    rules_array.clone(),
1655                )
1656                .await
1657                .map_err(|e| {
1658                    ApiError::internal(format!(
1659                        "Failed to clone incoming mapping into {} from {}: {}",
1660                        source_id, area.cortical_id, e
1661                    ))
1662                })?;
1663        }
1664    }
1665
1666    Ok(Json(HashMap::from([
1667        ("message".to_string(), "Cortical area cloned".to_string()),
1668        ("new_area_id".to_string(), new_area_id),
1669    ])))
1670}
1671
1672/// Request payload for POST /v1/cortical_area/clone
1673#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1674pub struct CloneCorticalAreaRequest {
1675    /// Base64 cortical area ID to clone.
1676    pub source_area_id: String,
1677    /// New cortical area name (display name).
1678    pub new_name: String,
1679    /// New 3D coordinates for placement.
1680    pub coordinates_3d: [i32; 3],
1681    /// New 2D coordinates for visualization placement.
1682    pub coordinates_2d: [i32; 2],
1683    /// Target parent brain region ID to attach the clone under.
1684    ///
1685    /// NOTE: FEAGI does NOT rely on the client for this value; it derives the parent from the
1686    /// source area’s membership. If provided and mismatched, FEAGI rejects the request.
1687    #[serde(default)]
1688    pub parent_region_id: Option<String>,
1689    /// If true, clones cortical mappings (incoming + outgoing) to reproduce wiring.
1690    pub clone_cortical_mapping: bool,
1691}
1692
1693/// Update properties of multiple cortical areas in a single request. (Not yet implemented)
1694#[utoipa::path(
1695    put,
1696    path = "/v1/cortical_area/multi/cortical_area",
1697    tag = "cortical_area"
1698)]
1699pub async fn put_multi_cortical_area(
1700    State(state): State<ApiState>,
1701    Json(mut request): Json<HashMap<String, serde_json::Value>>,
1702) -> ApiResult<Json<HashMap<String, String>>> {
1703    let genome_service = state.genome_service.as_ref();
1704
1705    // Extract cortical_id_list
1706    let cortical_ids: Vec<String> = request
1707        .get("cortical_id_list")
1708        .and_then(|v| v.as_array())
1709        .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1710        .iter()
1711        .filter_map(|v| v.as_str().map(String::from))
1712        .collect();
1713
1714    if cortical_ids.is_empty() {
1715        return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1716    }
1717
1718    tracing::debug!(
1719        target: "feagi-api",
1720        "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1721        cortical_ids.len(),
1722        request.keys().collect::<Vec<_>>()
1723    );
1724
1725    // Remove cortical_id_list from changes (it's not a property to update)
1726    request.remove("cortical_id_list");
1727
1728    // Build shared properties (applies to all unless overridden per-id)
1729    let mut shared_properties = request.clone();
1730    for cortical_id in &cortical_ids {
1731        shared_properties.remove(cortical_id);
1732    }
1733
1734    // Update each cortical area, using per-id properties when provided
1735    for cortical_id in &cortical_ids {
1736        tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1737        let mut properties = shared_properties.clone();
1738        if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1739            for (key, value) in per_id_map {
1740                properties.insert(key.clone(), value.clone());
1741            }
1742        }
1743        match genome_service
1744            .update_cortical_area(cortical_id, properties)
1745            .await
1746        {
1747            Ok(_) => {
1748                tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1749            }
1750            Err(e) => {
1751                tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1752                return Err(ApiError::internal(format!(
1753                    "Failed to update cortical area {}: {}",
1754                    cortical_id, e
1755                )));
1756            }
1757        }
1758    }
1759
1760    Ok(Json(HashMap::from([
1761        (
1762            "message".to_string(),
1763            format!("Updated {} cortical areas", cortical_ids.len()),
1764        ),
1765        ("cortical_ids".to_string(), cortical_ids.join(", ")),
1766    ])))
1767}
1768
1769/// Delete multiple cortical areas by their IDs. (Not yet implemented)
1770#[utoipa::path(
1771    delete,
1772    path = "/v1/cortical_area/multi/cortical_area",
1773    tag = "cortical_area"
1774)]
1775#[allow(unused_variables)] // In development
1776pub async fn delete_multi_cortical_area(
1777    State(state): State<ApiState>,
1778    Json(request): Json<Vec<String>>,
1779) -> ApiResult<Json<HashMap<String, String>>> {
1780    // TODO: Delete multiple cortical areas
1781    Err(ApiError::internal("Not yet implemented"))
1782}
1783
1784/// Update the 2D visualization coordinates of a cortical area. (Not yet implemented)
1785#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1786#[allow(unused_variables)] // In development
1787pub async fn put_coord_2d(
1788    State(state): State<ApiState>,
1789    Json(request): Json<HashMap<String, serde_json::Value>>,
1790) -> ApiResult<Json<HashMap<String, String>>> {
1791    // TODO: Update 2D coordinates
1792    Err(ApiError::internal("Not yet implemented"))
1793}
1794
1795/// Hide/show cortical areas in visualizations. (Not yet implemented)
1796#[utoipa::path(
1797    put,
1798    path = "/v1/cortical_area/suppress_cortical_visibility",
1799    tag = "cortical_area"
1800)]
1801#[allow(unused_variables)] // In development
1802pub async fn put_suppress_cortical_visibility(
1803    State(state): State<ApiState>,
1804    Json(request): Json<HashMap<String, serde_json::Value>>,
1805) -> ApiResult<Json<HashMap<String, String>>> {
1806    // TODO: Suppress cortical visibility
1807    Err(ApiError::internal("Not yet implemented"))
1808}
1809
1810/// Reset runtime neural state for one or more cortical areas (membrane potential, refractory
1811/// counters, FCL candidates). Genome, connections, and parameters are unchanged.
1812#[utoipa::path(
1813    put,
1814    path = "/v1/cortical_area/reset",
1815    tag = "cortical_area",
1816    request_body = CorticalAreaResetRequest,
1817    responses(
1818        (status = 200, description = "Reset applied", body = CorticalAreaResetResponse),
1819    )
1820)]
1821pub async fn put_reset(
1822    State(state): State<ApiState>,
1823    Json(request): Json<CorticalAreaResetRequest>,
1824) -> ApiResult<Json<CorticalAreaResetResponse>> {
1825    use tracing::info;
1826
1827    if request.area_list.is_empty() {
1828        return Err(ApiError::invalid_input("area_list cannot be empty"));
1829    }
1830
1831    info!(
1832        target: "feagi-api",
1833        "[RESET] Received reset request for {} cortical areas: {:?}",
1834        request.area_list.len(),
1835        request.area_list
1836    );
1837
1838    let connectome_service = state.connectome_service.as_ref();
1839    let mut cortical_indices: Vec<u32> = Vec::with_capacity(request.area_list.len());
1840    for id in &request.area_list {
1841        let area = connectome_service
1842            .get_cortical_area(id)
1843            .await
1844            .map_err(ApiError::from)?;
1845        cortical_indices.push(area.cortical_idx);
1846        info!(
1847            target: "feagi-api",
1848            "[RESET] Resolved cortical ID '{}' to index {}",
1849            id,
1850            area.cortical_idx
1851        );
1852    }
1853
1854    info!(
1855        target: "feagi-api",
1856        "[RESET] Calling runtime service to reset indices: {:?}",
1857        cortical_indices
1858    );
1859
1860    let reset_pairs = state
1861        .runtime_service
1862        .reset_cortical_area_states(&cortical_indices)
1863        .await
1864        .map_err(ApiError::from)?;
1865
1866    let results: Vec<CorticalAreaResetItem> = reset_pairs
1867        .into_iter()
1868        .map(|(cortical_idx, neurons_reset)| {
1869            info!(
1870                target: "feagi-api",
1871                "[RESET] Cortical area {} reset: {} neurons cleared",
1872                cortical_idx,
1873                neurons_reset
1874            );
1875            CorticalAreaResetItem {
1876                cortical_idx,
1877                neurons_reset,
1878            }
1879        })
1880        .collect();
1881
1882    info!(
1883        target: "feagi-api",
1884        "[RESET] Reset complete for {} areas",
1885        results.len()
1886    );
1887
1888    Ok(Json(CorticalAreaResetResponse {
1889        message: "ok".to_string(),
1890        results,
1891    }))
1892}
1893
1894/// Check if visualization is enabled for the system.
1895#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1896pub async fn get_visualization(
1897    State(_state): State<ApiState>,
1898) -> ApiResult<Json<HashMap<String, bool>>> {
1899    let mut response = HashMap::new();
1900    response.insert("enabled".to_string(), true);
1901    Ok(Json(response))
1902}
1903
1904/// Execute multiple cortical area operations (create, update, delete) in a single batch.
1905#[utoipa::path(
1906    post,
1907    path = "/v1/cortical_area/batch_operations",
1908    tag = "cortical_area"
1909)]
1910pub async fn post_batch_operations(
1911    State(_state): State<ApiState>,
1912    Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1913) -> ApiResult<Json<HashMap<String, i32>>> {
1914    let mut response = HashMap::new();
1915    response.insert("processed".to_string(), 0);
1916    Ok(Json(response))
1917}
1918
1919/// Alias for /v1/cortical_area/ipu - list all IPU cortical area IDs.
1920#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1921pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1922    get_ipu(State(state)).await
1923}
1924
1925/// Alias for /v1/cortical_area/opu - list all OPU cortical area IDs.
1926#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
1927pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1928    get_opu(State(state)).await
1929}
1930
1931/// Update the 3D position of a cortical area. (Not yet implemented)
1932#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
1933pub async fn put_coordinates_3d(
1934    State(_state): State<ApiState>,
1935    Json(_req): Json<HashMap<String, serde_json::Value>>,
1936) -> ApiResult<Json<HashMap<String, String>>> {
1937    Ok(Json(HashMap::from([(
1938        "message".to_string(),
1939        "Not yet implemented".to_string(),
1940    )])))
1941}
1942
1943/// Delete multiple cortical areas by their IDs in a single operation.
1944#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
1945pub async fn delete_bulk(
1946    State(_state): State<ApiState>,
1947    Json(_ids): Json<Vec<String>>,
1948) -> ApiResult<Json<HashMap<String, i32>>> {
1949    let mut response = HashMap::new();
1950    response.insert("deleted_count".to_string(), 0);
1951    Ok(Json(response))
1952}
1953
1954/// Resize a cortical area by changing its dimensions. (Not yet implemented)
1955#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
1956pub async fn post_resize(
1957    State(_state): State<ApiState>,
1958    Json(_req): Json<HashMap<String, serde_json::Value>>,
1959) -> ApiResult<Json<HashMap<String, String>>> {
1960    Ok(Json(HashMap::from([(
1961        "message".to_string(),
1962        "Not yet implemented".to_string(),
1963    )])))
1964}
1965
1966/// Move a cortical area to a new position. (Not yet implemented)
1967#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
1968pub async fn post_reposition(
1969    State(_state): State<ApiState>,
1970    Json(_req): Json<HashMap<String, serde_json::Value>>,
1971) -> ApiResult<Json<HashMap<String, String>>> {
1972    Ok(Json(HashMap::from([(
1973        "message".to_string(),
1974        "Not yet implemented".to_string(),
1975    )])))
1976}
1977
1978/// List all neurons at a voxel `(x, y, z)` within a cortical area, with the same live property snapshot as `/v1/connectome/neuron_properties`.
1979#[utoipa::path(
1980    get,
1981    path = "/v1/cortical_area/voxel_neurons",
1982    tag = "cortical_area",
1983    params(VoxelNeuronsQuery),
1984    responses(
1985        (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
1986        (status = 404, description = "Cortical area or neuron data not found"),
1987        (status = 500, description = "Internal server error")
1988    )
1989)]
1990pub async fn get_voxel_neurons(
1991    State(state): State<ApiState>,
1992    Query(params): Query<VoxelNeuronsQuery>,
1993) -> ApiResult<Json<VoxelNeuronsResponse>> {
1994    resolve_voxel_neurons(
1995        &state,
1996        params.cortical_id,
1997        params.x,
1998        params.y,
1999        params.z,
2000        params.synapse_page,
2001    )
2002    .await
2003    .map(Json)
2004}
2005
2006/// Same as [`get_voxel_neurons`] but accepts a JSON body (for clients that cannot use query strings).
2007#[utoipa::path(
2008    post,
2009    path = "/v1/cortical_area/voxel_neurons",
2010    tag = "cortical_area",
2011    request_body = VoxelNeuronsBody,
2012    responses(
2013        (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2014        (status = 404, description = "Cortical area or neuron data not found"),
2015        (status = 500, description = "Internal server error")
2016    )
2017)]
2018pub async fn post_voxel_neurons(
2019    State(state): State<ApiState>,
2020    Json(body): Json<VoxelNeuronsBody>,
2021) -> ApiResult<Json<VoxelNeuronsResponse>> {
2022    resolve_voxel_neurons(
2023        &state,
2024        body.cortical_id,
2025        body.x,
2026        body.y,
2027        body.z,
2028        body.synapse_page,
2029    )
2030    .await
2031    .map(Json)
2032}
2033
2034/// GET /v1/cortical_area/memory — plasticity runtime stats, genome memory parameters, upstream wiring, synapse counts, and paginated memory neuron ids.
2035#[utoipa::path(
2036    get,
2037    path = "/v1/cortical_area/memory",
2038    tag = "cortical_area",
2039    params(MemoryCorticalAreaQuery),
2040    responses(
2041        (status = 200, description = "Memory cortical area details", body = MemoryCorticalAreaResponse),
2042        (status = 400, description = "Invalid cortical id or not a memory area"),
2043        (status = 500, description = "Internal server error")
2044    )
2045)]
2046pub async fn get_memory_cortical_area(
2047    State(state): State<ApiState>,
2048    Query(params): Query<MemoryCorticalAreaQuery>,
2049) -> ApiResult<Json<MemoryCorticalAreaResponse>> {
2050    let connectome_service = state.connectome_service.as_ref();
2051    let area = connectome_service
2052        .get_cortical_area(&params.cortical_id)
2053        .await
2054        .map_err(ApiError::from)?;
2055
2056    let mem_props = extract_memory_properties(&area.properties).ok_or_else(|| {
2057        ApiError::invalid_input(
2058            "cortical area is not a memory area (expected is_mem_type memory properties)",
2059        )
2060    })?;
2061
2062    let cortical_idx = area.cortical_idx;
2063    let cortical_name = area.name.clone();
2064
2065    let cid = CorticalID::try_from_base_64(&params.cortical_id)
2066        .map_err(|e| ApiError::invalid_input(format!("Invalid cortical_id: {}", e)))?;
2067
2068    let page_size_u32 = params
2069        .page_size
2070        .clamp(1, MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX);
2071    let page_size = page_size_u32 as usize;
2072    let offset = (params.page as usize).saturating_mul(page_size);
2073
2074    let manager = feagi_brain_development::ConnectomeManager::instance();
2075    let mgr = manager.read();
2076
2077    let upstream_cortical_area_indices = mgr.get_upstream_cortical_areas(&cid);
2078    let upstream_cortical_area_count = upstream_cortical_area_indices.len();
2079
2080    let exec = mgr
2081        .get_plasticity_executor()
2082        .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
2083    let ex = exec
2084        .lock()
2085        .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
2086
2087    let runtime = ex
2088        .memory_cortical_area_runtime_info(cortical_idx)
2089        .ok_or_else(|| ApiError::internal("Plasticity service not initialized"))?;
2090
2091    let (memory_neuron_ids_u32, total_memory_neuron_ids) = ex
2092        .paginated_memory_neuron_ids_in_area(cortical_idx, offset, page_size)
2093        .unwrap_or((Vec::new(), 0));
2094
2095    let has_more = offset.saturating_add(memory_neuron_ids_u32.len()) < total_memory_neuron_ids;
2096
2097    let memory_neuron_ids: Vec<u64> = memory_neuron_ids_u32
2098        .into_iter()
2099        .map(|id| id as u64)
2100        .collect();
2101
2102    Ok(Json(MemoryCorticalAreaResponse {
2103        cortical_id: params.cortical_id,
2104        cortical_idx,
2105        cortical_name,
2106        short_term_neuron_count: runtime.short_term_neuron_count,
2107        long_term_neuron_count: runtime.long_term_neuron_count,
2108        memory_parameters: MemoryCorticalAreaParamsResponse {
2109            temporal_depth: mem_props.temporal_depth,
2110            longterm_mem_threshold: mem_props.longterm_threshold,
2111            lifespan_growth_rate: mem_props.lifespan_growth_rate,
2112            init_lifespan: mem_props.init_lifespan,
2113        },
2114        upstream_cortical_area_indices,
2115        upstream_cortical_area_count,
2116        upstream_pattern_cache_size: runtime.upstream_pattern_cache_size,
2117        incoming_synapse_count: area.incoming_synapse_count,
2118        outgoing_synapse_count: area.outgoing_synapse_count,
2119        total_memory_neuron_ids,
2120        page: params.page,
2121        page_size: page_size_u32,
2122        memory_neuron_ids,
2123        has_more,
2124    }))
2125}
2126
2127/// Get metadata for all available IPU types (vision, infrared, etc.). Includes encodings, formats, units, and topology.
2128#[utoipa::path(
2129    get,
2130    path = "/v1/cortical_area/ipu/types",
2131    tag = "cortical_area",
2132    responses(
2133        (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2134        (status = 500, description = "Internal server error")
2135    )
2136)]
2137pub async fn get_ipu_types(
2138    State(_state): State<ApiState>,
2139) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2140    let mut types = HashMap::new();
2141
2142    // Dynamically generate metadata from feagi_data_structures templates
2143    for unit in SensoryCorticalUnit::list_all() {
2144        let id_ref = unit.get_cortical_id_unit_reference();
2145        let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2146
2147        // All IPU types support both absolute and incremental encodings
2148        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2149
2150        // Determine if formats are supported based on snake_case_name
2151        // Vision and SegmentedVision use CartesianPlane (no formats)
2152        // MiscData uses Misc (no formats)
2153        // All others use Percentage-based types (have formats)
2154        let snake_name = unit.get_snake_case_name();
2155        let formats = if snake_name == "vision"
2156            || snake_name == "segmented_vision"
2157            || snake_name == "miscellaneous"
2158        {
2159            vec![]
2160        } else {
2161            vec!["linear".to_string(), "fractional".to_string()]
2162        };
2163
2164        // Default resolution based on type
2165        let resolution = if snake_name == "vision" {
2166            vec![64, 64, 1] // Vision sensors typically 64x64
2167        } else if snake_name == "segmented_vision" {
2168            vec![32, 32, 1] // Segmented vision segments are smaller
2169        } else {
2170            vec![1, 1, 1] // Most sensors are scalar (1x1x1)
2171        };
2172
2173        // Most sensors are asymmetric
2174        let structure = "asymmetric".to_string();
2175
2176        // Get unit default topology
2177        let topology_map = unit.get_unit_default_topology();
2178        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2179            .into_iter()
2180            .map(|(idx, topo)| {
2181                (
2182                    *idx as usize,
2183                    UnitTopologyData {
2184                        relative_position: topo.relative_position,
2185                        dimensions: topo.channel_dimensions_default,
2186                    },
2187                )
2188            })
2189            .collect();
2190
2191        types.insert(
2192            key,
2193            CorticalTypeMetadata {
2194                description: unit.get_friendly_name().to_string(),
2195                encodings,
2196                formats,
2197                units: unit.get_number_cortical_areas() as u32,
2198                resolution,
2199                structure,
2200                unit_default_topology,
2201            },
2202        );
2203    }
2204
2205    Ok(Json(types))
2206}
2207
2208/// Get metadata for all available OPU types (motors, servos, etc.). Includes encodings, formats, units, and topology.
2209#[utoipa::path(
2210    get,
2211    path = "/v1/cortical_area/opu/types",
2212    tag = "cortical_area",
2213    responses(
2214        (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2215        (status = 500, description = "Internal server error")
2216    )
2217)]
2218pub async fn get_opu_types(
2219    State(_state): State<ApiState>,
2220) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2221    let mut types = HashMap::new();
2222
2223    // Dynamically generate metadata from feagi_data_structures templates
2224    for unit in MotorCorticalUnit::list_all() {
2225        let id_ref = unit.get_cortical_id_unit_reference();
2226        let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2227
2228        // All OPU types support both absolute and incremental encodings
2229        let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2230
2231        // Determine if formats are supported based on snake_case_name
2232        // MiscData uses Misc (no formats)
2233        // All others use Percentage-based types (have formats)
2234        let snake_name = unit.get_snake_case_name();
2235        let formats = if snake_name == "miscellaneous" {
2236            vec![]
2237        } else {
2238            vec!["linear".to_string(), "fractional".to_string()]
2239        };
2240
2241        // Default resolution - all motors/actuators are typically scalar
2242        let resolution = vec![1, 1, 1];
2243
2244        // All actuators are asymmetric
2245        let structure = "asymmetric".to_string();
2246
2247        // Get unit default topology
2248        let topology_map = unit.get_unit_default_topology();
2249        let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2250            .into_iter()
2251            .map(|(idx, topo)| {
2252                (
2253                    *idx as usize,
2254                    UnitTopologyData {
2255                        relative_position: topo.relative_position,
2256                        dimensions: topo.channel_dimensions_default,
2257                    },
2258                )
2259            })
2260            .collect();
2261
2262        types.insert(
2263            key,
2264            CorticalTypeMetadata {
2265                description: unit.get_friendly_name().to_string(),
2266                encodings,
2267                formats,
2268                units: unit.get_number_cortical_areas() as u32,
2269                resolution,
2270                structure,
2271                unit_default_topology,
2272            },
2273        );
2274    }
2275
2276    Ok(Json(types))
2277}
2278
2279/// Get list of all cortical area indices (numerical indices used internally for indexing).
2280#[utoipa::path(
2281    get,
2282    path = "/v1/cortical_area/cortical_area_index_list",
2283    tag = "cortical_area"
2284)]
2285pub async fn get_cortical_area_index_list(
2286    State(state): State<ApiState>,
2287) -> ApiResult<Json<Vec<u32>>> {
2288    let connectome_service = state.connectome_service.as_ref();
2289    let areas = connectome_service
2290        .list_cortical_areas()
2291        .await
2292        .map_err(|e| ApiError::internal(format!("{}", e)))?;
2293    // CRITICAL FIX: Return the actual cortical_idx values, not fabricated sequential indices
2294    let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
2295    Ok(Json(indices))
2296}
2297
2298/// Get mapping from cortical area IDs to their internal indices. Returns {cortical_id: index}.
2299#[utoipa::path(
2300    get,
2301    path = "/v1/cortical_area/cortical_idx_mapping",
2302    tag = "cortical_area"
2303)]
2304pub async fn get_cortical_idx_mapping(
2305    State(state): State<ApiState>,
2306) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
2307    use std::collections::BTreeMap;
2308
2309    let connectome_service = state.connectome_service.as_ref();
2310    let areas = connectome_service
2311        .list_cortical_areas()
2312        .await
2313        .map_err(|e| ApiError::internal(format!("{}", e)))?;
2314    // CRITICAL FIX: Use the actual cortical_idx from CorticalArea, NOT enumerate() which ignores reserved indices!
2315    // Use BTreeMap for consistent alphabetical ordering
2316    let mapping: BTreeMap<String, u32> = areas
2317        .iter()
2318        .map(|a| (a.cortical_id.clone(), a.cortical_idx))
2319        .collect();
2320    Ok(Json(mapping))
2321}
2322
2323/// Get restrictions on which cortical areas can connect to which (connection validation rules).
2324#[utoipa::path(
2325    get,
2326    path = "/v1/cortical_area/mapping_restrictions",
2327    tag = "cortical_area"
2328)]
2329pub async fn get_mapping_restrictions_query(
2330    State(_state): State<ApiState>,
2331    Query(_params): Query<HashMap<String, String>>,
2332) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2333    Ok(Json(HashMap::new()))
2334}
2335
2336/// Get memory usage of a specific cortical area in bytes (calculated from neuron count).
2337#[utoipa::path(
2338    get,
2339    path = "/v1/cortical_area/{cortical_id}/memory_usage",
2340    tag = "cortical_area"
2341)]
2342pub async fn get_memory_usage(
2343    State(state): State<ApiState>,
2344    Path(cortical_id): Path<String>,
2345) -> ApiResult<Json<HashMap<String, i64>>> {
2346    let connectome_service = state.connectome_service.as_ref();
2347
2348    // CRITICAL FIX: Calculate actual memory usage based on neuron count instead of hardcoded 0
2349    let area_info = connectome_service
2350        .get_cortical_area(&cortical_id)
2351        .await
2352        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2353
2354    // Calculate memory usage: neuron_count × bytes per neuron
2355    // Each neuron in NeuronArray uses ~48 bytes (membrane_potential, threshold, refractory, etc.)
2356    const BYTES_PER_NEURON: i64 = 48;
2357    let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
2358
2359    let mut response = HashMap::new();
2360    response.insert("memory_bytes".to_string(), memory_bytes);
2361    Ok(Json(response))
2362}
2363
2364/// Get the total number of neurons in a specific cortical area.
2365#[utoipa::path(
2366    get,
2367    path = "/v1/cortical_area/{cortical_id}/neuron_count",
2368    tag = "cortical_area"
2369)]
2370pub async fn get_area_neuron_count(
2371    State(state): State<ApiState>,
2372    Path(cortical_id): Path<String>,
2373) -> ApiResult<Json<i64>> {
2374    let connectome_service = state.connectome_service.as_ref();
2375
2376    // CRITICAL FIX: Get actual neuron count from ConnectomeService instead of hardcoded 0
2377    let area_info = connectome_service
2378        .get_cortical_area(&cortical_id)
2379        .await
2380        .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2381
2382    Ok(Json(area_info.neuron_count as i64))
2383}
2384
2385/// Get available cortical type options for UI selection: Sensory, Motor, Custom, Memory.
2386#[utoipa::path(
2387    post,
2388    path = "/v1/cortical_area/cortical_type_options",
2389    tag = "cortical_area"
2390)]
2391pub async fn post_cortical_type_options(
2392    State(_state): State<ApiState>,
2393) -> ApiResult<Json<Vec<String>>> {
2394    Ok(Json(vec![
2395        "Sensory".to_string(),
2396        "Motor".to_string(),
2397        "Custom".to_string(),
2398        "Memory".to_string(),
2399    ]))
2400}
2401
2402/// Get mapping restrictions for specific cortical areas (POST version with request body).
2403#[utoipa::path(
2404    post,
2405    path = "/v1/cortical_area/mapping_restrictions",
2406    tag = "cortical_area"
2407)]
2408pub async fn post_mapping_restrictions(
2409    State(_state): State<ApiState>,
2410    Json(_req): Json<HashMap<String, String>>,
2411) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2412    Ok(Json(HashMap::new()))
2413}
2414
2415/// Get mapping restrictions between two specific cortical areas (connection validation).
2416#[utoipa::path(
2417    post,
2418    path = "/v1/cortical_area/mapping_restrictions_between_areas",
2419    tag = "cortical_area"
2420)]
2421pub async fn post_mapping_restrictions_between_areas(
2422    State(_state): State<ApiState>,
2423    Json(_req): Json<HashMap<String, String>>,
2424) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2425    Ok(Json(HashMap::new()))
2426}
2427
2428/// Update 3D coordinates of a cortical area (alternative endpoint). (Not yet implemented)
2429#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
2430pub async fn put_coord_3d(
2431    State(_state): State<ApiState>,
2432    Json(_req): Json<HashMap<String, serde_json::Value>>,
2433) -> ApiResult<Json<HashMap<String, String>>> {
2434    Ok(Json(HashMap::from([(
2435        "message".to_string(),
2436        "Not yet implemented".to_string(),
2437    )])))
2438}
2439
2440#[cfg(test)]
2441mod voxel_neurons_dto_tests {
2442    use super::{
2443        synapse_details_for_neuron, synapse_page_window, VoxelNeuronsBody, VoxelNeuronsResponse,
2444    };
2445
2446    #[test]
2447    fn synapse_page_window_paginates_fifty_per_direction() {
2448        let (s, e, more) = synapse_page_window(120, 0);
2449        assert_eq!((s, e, more), (0, 50, true));
2450        let (s, e, more) = synapse_page_window(120, 1);
2451        assert_eq!((s, e, more), (50, 100, true));
2452        let (s, e, more) = synapse_page_window(120, 2);
2453        assert_eq!((s, e, more), (100, 120, false));
2454        let (s, e, more) = synapse_page_window(120, 3);
2455        assert_eq!((s, e, more), (0, 0, false));
2456    }
2457
2458    #[test]
2459    fn synapse_details_matches_connectome_shape() {
2460        let mgr = feagi_brain_development::ConnectomeManager::new_for_testing();
2461        let out_full = vec![(10, 2.0, 5.0, 1)];
2462        let inc_full = vec![(3, 4.0, 6.0, 0)];
2463        let (out, inc) = synapse_details_for_neuron(&mgr, 7, &out_full, &inc_full);
2464        let out_a = out.as_array().expect("outgoing array");
2465        assert_eq!(out_a[0]["source_neuron_id"], serde_json::json!(7));
2466        assert_eq!(out_a[0]["target_neuron_id"], serde_json::json!(10));
2467        assert_eq!(out_a[0]["weight"], serde_json::json!(2.0));
2468        assert_eq!(out_a[0]["postsynaptic_potential"], serde_json::json!(5.0));
2469        assert_eq!(out_a[0]["synapse_type"], serde_json::json!(1));
2470        assert!(out_a[0].get("target_cortical_id").is_some());
2471        assert!(out_a[0].get("target_cortical_name").is_some());
2472        assert!(out_a[0].get("target_x").is_some());
2473        let in_a = inc.as_array().expect("incoming array");
2474        assert_eq!(in_a[0]["source_neuron_id"], serde_json::json!(3));
2475        assert_eq!(in_a[0]["target_neuron_id"], serde_json::json!(7));
2476        assert!(in_a[0].get("source_cortical_id").is_some());
2477        assert!(in_a[0].get("source_cortical_name").is_some());
2478        assert!(in_a[0].get("source_x").is_some());
2479    }
2480
2481    #[test]
2482    fn voxel_neurons_body_deserializes_from_json() {
2483        let j = r#"{"cortical_id":"X19fcG93ZXI=","x":0,"y":0,"z":0}"#;
2484        let b: VoxelNeuronsBody = serde_json::from_str(j).expect("deserialize body");
2485        assert_eq!(b.cortical_id, "X19fcG93ZXI=");
2486        assert_eq!((b.x, b.y, b.z), (0, 0, 0));
2487        assert_eq!(b.synapse_page, 0);
2488    }
2489
2490    #[test]
2491    fn voxel_neurons_response_serializes() {
2492        let r = VoxelNeuronsResponse {
2493            cortical_id: "id".to_string(),
2494            cortical_name: "test_area".to_string(),
2495            cortical_idx: 2,
2496            voxel_coordinate: [1, 2, 3],
2497            x: 1,
2498            y: 2,
2499            z: 3,
2500            synapse_page: 0,
2501            neuron_count: 0,
2502            neurons: vec![],
2503        };
2504        let v = serde_json::to_value(&r).expect("serialize");
2505        assert_eq!(v["cortical_name"], serde_json::json!("test_area"));
2506        assert_eq!(v["voxel_coordinate"], serde_json::json!([1, 2, 3]));
2507        assert_eq!(v["neuron_count"], serde_json::json!(0));
2508        assert_eq!(v["synapse_page"], serde_json::json!(0));
2509        assert_eq!(v["neurons"], serde_json::json!([]));
2510    }
2511}