1use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, Path, State};
9use feagi_services::types::CreateBrainRegionParams;
10use feagi_structures::genomic::brain_regions::RegionID;
11use std::collections::HashMap;
12
13#[utoipa::path(
34 get,
35 path = "/v1/region/regions_members",
36 tag = "region",
37 responses(
38 (status = 200, description = "Brain regions with member areas", body = HashMap<String, serde_json::Value>),
39 (status = 500, description = "Internal server error")
40 )
41)]
42pub async fn get_regions_members(
43 State(state): State<ApiState>,
44) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
45 use tracing::trace;
46 let connectome_service = state.connectome_service.as_ref();
47 match connectome_service.list_brain_regions().await {
48 Ok(regions) => {
49 trace!(target: "feagi-api", "Found {} brain regions to return", regions.len());
50 let mut result = HashMap::new();
51 for region in regions {
52 trace!(
53 target: "feagi-api",
54 "Region: {} ({}) with {} areas",
55 region.region_id,
56 region.name,
57 region.cortical_areas.len()
58 );
59
60 let inputs = region
62 .properties
63 .get("inputs")
64 .and_then(|v| v.as_array())
65 .map(|arr| {
66 arr.iter()
67 .filter_map(|v| v.as_str().map(String::from))
68 .collect::<Vec<String>>()
69 })
70 .unwrap_or_default();
71
72 let outputs = region
73 .properties
74 .get("outputs")
75 .and_then(|v| v.as_array())
76 .map(|arr| {
77 arr.iter()
78 .filter_map(|v| v.as_str().map(String::from))
79 .collect::<Vec<String>>()
80 })
81 .unwrap_or_default();
82
83 trace!(
84 target: "feagi-api",
85 "Inputs: {} areas, Outputs: {} areas",
86 inputs.len(),
87 outputs.len()
88 );
89
90 let coordinate_3d = region
92 .properties
93 .get("coordinate_3d")
94 .and_then(|v| v.as_array())
95 .and_then(|arr| {
96 if arr.len() >= 3 {
97 Some(serde_json::json!([arr[0], arr[1], arr[2]]))
98 } else {
99 None
100 }
101 })
102 .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
103
104 let coordinate_2d = region
106 .properties
107 .get("coordinate_2d")
108 .and_then(|v| v.as_array())
109 .and_then(|arr| {
110 if arr.len() >= 2 {
111 Some(serde_json::json!([arr[0], arr[1]]))
112 } else {
113 None
114 }
115 })
116 .unwrap_or_else(|| serde_json::json!([0, 0]));
117
118 result.insert(
119 region.region_id.clone(),
120 serde_json::json!({
121 "title": region.name,
122 "description": "", "parent_region_id": region.parent_id,
124 "coordinate_2d": coordinate_2d,
125 "coordinate_3d": coordinate_3d,
126 "areas": region.cortical_areas,
127 "regions": region.child_regions,
128 "inputs": inputs,
129 "outputs": outputs
130 }),
131 );
132 }
133 trace!(target: "feagi-api", "Returning {} regions in response", result.len());
134 Ok(Json(result))
135 }
136 Err(e) => Err(ApiError::internal(format!("Failed to get regions: {}", e))),
137 }
138}
139
140#[utoipa::path(post, path = "/v1/region/region", tag = "region")]
142pub async fn post_region(
143 State(state): State<ApiState>,
144 Json(mut req): Json<HashMap<String, serde_json::Value>>,
145) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
146 let connectome_service = state.connectome_service.as_ref();
147
148 let title = req
149 .get("title")
150 .or_else(|| req.get("name"))
151 .and_then(|v| v.as_str())
152 .map(str::trim)
153 .filter(|value| !value.is_empty())
154 .ok_or_else(|| ApiError::invalid_input("title required"))?
155 .to_string();
156 req.remove("title");
157 req.remove("name");
158
159 let region_id = match req.get("region_id").and_then(|v| v.as_str()) {
160 Some(value) if !value.trim().is_empty() => RegionID::from_string(value)
161 .map_err(|e| ApiError::invalid_input(format!("Invalid region_id: {}", e)))?
162 .to_string(),
163 _ => RegionID::new().to_string(),
164 };
165 req.remove("region_id");
166
167 let parent_region_id = req
168 .get("parent_region_id")
169 .and_then(|v| v.as_str())
170 .map(str::trim)
171 .filter(|value| !value.is_empty())
172 .map(str::to_string);
173 req.remove("parent_region_id");
174
175 let region_type = req
176 .get("region_type")
177 .and_then(|v| v.as_str())
178 .map(str::trim)
179 .filter(|value| !value.is_empty())
180 .map(str::to_string)
181 .unwrap_or_else(|| "Undefined".to_string());
182 req.remove("region_type");
183
184 let coordinate_2d_value = req
185 .get("coordinate_2d")
186 .or_else(|| req.get("coordinates_2d"))
187 .and_then(|v| v.as_array().cloned())
188 .ok_or_else(|| ApiError::invalid_input("coordinates_2d required"))?;
189 if coordinate_2d_value.len() != 2 {
190 return Err(ApiError::invalid_input(
191 "coordinates_2d must contain exactly 2 values",
192 ));
193 }
194 req.remove("coordinate_2d");
195 req.remove("coordinates_2d");
196
197 let coordinate_3d_value = req
198 .get("coordinate_3d")
199 .or_else(|| req.get("coordinates_3d"))
200 .and_then(|v| v.as_array().cloned())
201 .ok_or_else(|| ApiError::invalid_input("coordinates_3d required"))?;
202 if coordinate_3d_value.len() != 3 {
203 return Err(ApiError::invalid_input(
204 "coordinates_3d must contain exactly 3 values",
205 ));
206 }
207 req.remove("coordinate_3d");
208 req.remove("coordinates_3d");
209
210 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
211 properties.insert(
212 "coordinate_2d".to_string(),
213 serde_json::Value::Array(coordinate_2d_value),
214 );
215 properties.insert(
216 "coordinate_3d".to_string(),
217 serde_json::Value::Array(coordinate_3d_value),
218 );
219 if let Some(parent_region_id) = &parent_region_id {
220 properties.insert(
221 "parent_region_id".to_string(),
222 serde_json::json!(parent_region_id),
223 );
224 }
225 if let Some(areas) = req.remove("areas") {
226 properties.insert("areas".to_string(), areas);
227 }
228 if let Some(regions) = req.remove("regions") {
229 properties.insert("regions".to_string(), regions);
230 }
231 for (key, value) in req {
232 properties.insert(key, value);
233 }
234
235 let params = CreateBrainRegionParams {
236 region_id: region_id.clone(),
237 name: title.clone(),
238 region_type,
239 parent_id: parent_region_id.clone(),
240 properties: Some(properties),
241 };
242
243 let info = connectome_service
244 .create_brain_region(params)
245 .await
246 .map_err(ApiError::from)?;
247
248 let coordinate_2d = info
249 .properties
250 .get("coordinate_2d")
251 .cloned()
252 .ok_or_else(|| ApiError::internal("Missing coordinate_2d on created region"))?;
253 let coordinate_3d = info
254 .properties
255 .get("coordinate_3d")
256 .cloned()
257 .ok_or_else(|| ApiError::internal("Missing coordinate_3d on created region"))?;
258
259 let mut response = HashMap::from([
260 ("region_id".to_string(), serde_json::json!(info.region_id)),
261 ("title".to_string(), serde_json::json!(info.name)),
262 (
263 "parent_region_id".to_string(),
264 serde_json::json!(info.parent_id),
265 ),
266 ("coordinate_2d".to_string(), coordinate_2d),
267 ("coordinate_3d".to_string(), coordinate_3d),
268 ("areas".to_string(), serde_json::json!(info.cortical_areas)),
269 ("regions".to_string(), serde_json::json!(info.child_regions)),
270 ]);
271
272 if let Some(inputs) = info.properties.get("inputs") {
273 response.insert("inputs".to_string(), inputs.clone());
274 }
275 if let Some(outputs) = info.properties.get("outputs") {
276 response.insert("outputs".to_string(), outputs.clone());
277 }
278
279 Ok(Json(response))
280}
281
282#[utoipa::path(put, path = "/v1/region/region", tag = "region")]
284pub async fn put_region(
285 State(state): State<ApiState>,
286 Json(mut request): Json<HashMap<String, serde_json::Value>>,
287) -> ApiResult<Json<HashMap<String, String>>> {
288 let connectome_service = state.connectome_service.as_ref();
289
290 let region_id = request
292 .get("region_id")
293 .and_then(|v| v.as_str())
294 .ok_or_else(|| ApiError::invalid_input("region_id required"))?
295 .to_string();
296
297 request.remove("region_id");
299
300 match connectome_service
302 .update_brain_region(®ion_id, request)
303 .await
304 {
305 Ok(_) => Ok(Json(HashMap::from([
306 ("message".to_string(), "Brain region updated".to_string()),
307 ("region_id".to_string(), region_id),
308 ]))),
309 Err(e) => Err(ApiError::internal(format!(
310 "Failed to update brain region: {}",
311 e
312 ))),
313 }
314}
315
316#[utoipa::path(delete, path = "/v1/region/region", tag = "region")]
318pub async fn delete_region(
319 State(state): State<ApiState>,
320 Json(req): Json<HashMap<String, String>>,
321) -> ApiResult<Json<HashMap<String, String>>> {
322 let connectome_service = state.connectome_service.as_ref();
323 let region_id = req
324 .get("region_id")
325 .or_else(|| req.get("id"))
326 .map(String::as_str)
327 .map(str::trim)
328 .filter(|value| !value.is_empty())
329 .ok_or_else(|| ApiError::invalid_input("region_id required"))?
330 .to_string();
331
332 connectome_service
333 .delete_brain_region(®ion_id)
334 .await
335 .map_err(ApiError::from)?;
336
337 Ok(Json(HashMap::from([
338 ("message".to_string(), "Brain region deleted".to_string()),
339 ("region_id".to_string(), region_id),
340 ])))
341}
342
343#[utoipa::path(post, path = "/v1/region/clone", tag = "region")]
345pub async fn post_clone(
346 State(_state): State<ApiState>,
347 Json(_req): Json<HashMap<String, serde_json::Value>>,
348) -> ApiResult<Json<HashMap<String, String>>> {
349 Err(ApiError::internal("Not yet implemented"))
350}
351
352#[utoipa::path(put, path = "/v1/region/relocate_members", tag = "region")]
354pub async fn put_relocate_members(
355 State(state): State<ApiState>,
356 Json(request): Json<HashMap<String, serde_json::Value>>,
357) -> ApiResult<Json<HashMap<String, String>>> {
358 let connectome_service = state.connectome_service.as_ref();
359
360 if request.is_empty() {
361 return Err(ApiError::invalid_input("Request cannot be empty"));
362 }
363
364 let mut updated_regions: Vec<String> = Vec::new();
365
366 for (region_id, payload) in request {
367 let payload_obj = payload.as_object().ok_or_else(|| {
368 ApiError::invalid_input(format!("Region '{}' entry must be an object", region_id))
369 })?;
370
371 if payload_obj.contains_key("parent_region_id") {
372 return Err(ApiError::invalid_input(
373 "parent_region_id relocation is not implemented via relocate_members",
374 ));
375 }
376
377 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
378 if let Some(value) = payload_obj
379 .get("coordinate_2d")
380 .or_else(|| payload_obj.get("coordinates_2d"))
381 {
382 properties.insert("coordinate_2d".to_string(), value.clone());
383 }
384 if let Some(value) = payload_obj
385 .get("coordinate_3d")
386 .or_else(|| payload_obj.get("coordinates_3d"))
387 {
388 properties.insert("coordinate_3d".to_string(), value.clone());
389 }
390
391 if properties.is_empty() {
392 return Err(ApiError::invalid_input(format!(
393 "Region '{}' has no supported properties to update",
394 region_id
395 )));
396 }
397
398 connectome_service
399 .update_brain_region(®ion_id, properties)
400 .await
401 .map_err(|e| {
402 ApiError::internal(format!("Failed to update region {}: {}", region_id, e))
403 })?;
404
405 updated_regions.push(region_id);
406 }
407
408 Ok(Json(HashMap::from([
409 (
410 "message".to_string(),
411 format!("Updated {} brain regions", updated_regions.len()),
412 ),
413 ("region_ids".to_string(), updated_regions.join(", ")),
414 ])))
415}
416
417#[utoipa::path(delete, path = "/v1/region/region_and_members", tag = "region")]
419pub async fn delete_region_and_members(
420 State(_state): State<ApiState>,
421 Json(_req): Json<HashMap<String, String>>,
422) -> ApiResult<Json<HashMap<String, String>>> {
423 Err(ApiError::internal("Not yet implemented"))
424}
425
426#[utoipa::path(
429 get,
430 path = "/v1/region/regions",
431 tag = "region",
432 responses(
433 (status = 200, description = "List of region IDs", body = Vec<String>)
434 )
435)]
436pub async fn get_regions(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
437 let connectome_service = state.connectome_service.as_ref();
438
439 let regions = connectome_service
440 .list_brain_regions()
441 .await
442 .map_err(|e| ApiError::internal(format!("Failed to list regions: {}", e)))?;
443
444 let region_ids: Vec<String> = regions.iter().map(|r| r.region_id.clone()).collect();
445 Ok(Json(region_ids))
446}
447
448#[utoipa::path(
451 get,
452 path = "/v1/region/region_titles",
453 tag = "region",
454 responses(
455 (status = 200, description = "Region ID to title mapping", body = HashMap<String, String>)
456 )
457)]
458pub async fn get_region_titles(
459 State(state): State<ApiState>,
460) -> ApiResult<Json<HashMap<String, String>>> {
461 let connectome_service = state.connectome_service.as_ref();
462
463 let regions = connectome_service
464 .list_brain_regions()
465 .await
466 .map_err(|e| ApiError::internal(format!("Failed to list regions: {}", e)))?;
467
468 let mut titles = HashMap::new();
469 for region in regions {
470 titles.insert(region.region_id.clone(), region.name.clone());
471 }
472
473 Ok(Json(titles))
474}
475
476#[utoipa::path(
479 get,
480 path = "/v1/region/region/{region_id}",
481 tag = "region",
482 params(
483 ("region_id" = String, Path, description = "Brain region ID")
484 ),
485 responses(
486 (status = 200, description = "Region properties", body = HashMap<String, serde_json::Value>),
487 (status = 404, description = "Region not found")
488 )
489)]
490pub async fn get_region_detail(
491 State(state): State<ApiState>,
492 Path(region_id): Path<String>,
493) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
494 let connectome_service = state.connectome_service.as_ref();
495
496 let region = connectome_service
497 .get_brain_region(®ion_id)
498 .await
499 .map_err(|e| ApiError::not_found("region", &e.to_string()))?;
500
501 let coordinate_3d = region
503 .properties
504 .get("coordinate_3d")
505 .and_then(|v| v.as_array())
506 .and_then(|arr| {
507 if arr.len() >= 3 {
508 Some(serde_json::json!([arr[0], arr[1], arr[2]]))
509 } else {
510 None
511 }
512 })
513 .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
514
515 let coordinate_2d = region
517 .properties
518 .get("coordinate_2d")
519 .and_then(|v| v.as_array())
520 .and_then(|arr| {
521 if arr.len() >= 2 {
522 Some(serde_json::json!([arr[0], arr[1]]))
523 } else {
524 None
525 }
526 })
527 .unwrap_or_else(|| serde_json::json!([0, 0]));
528
529 let mut response = HashMap::new();
530 response.insert("region_id".to_string(), serde_json::json!(region.region_id));
531 response.insert("title".to_string(), serde_json::json!(region.name));
532 response.insert("description".to_string(), serde_json::json!(""));
533 response.insert("coordinate_2d".to_string(), coordinate_2d);
534 response.insert("coordinate_3d".to_string(), coordinate_3d);
535 response.insert(
536 "areas".to_string(),
537 serde_json::json!(region.cortical_areas),
538 );
539 response.insert(
540 "regions".to_string(),
541 serde_json::json!(region.child_regions),
542 );
543 response.insert(
544 "parent_region_id".to_string(),
545 serde_json::json!(region.parent_id),
546 );
547
548 Ok(Json(response))
549}
550
551#[utoipa::path(
554 put,
555 path = "/v1/region/change_region_parent",
556 tag = "region",
557 responses(
558 (status = 200, description = "Parent changed", body = HashMap<String, String>)
559 )
560)]
561pub async fn put_change_region_parent(
562 State(_state): State<ApiState>,
563 Json(_request): Json<HashMap<String, String>>,
564) -> ApiResult<Json<HashMap<String, String>>> {
565 Ok(Json(HashMap::from([(
566 "message".to_string(),
567 "Region parent change not yet implemented".to_string(),
568 )])))
569}
570
571#[utoipa::path(
574 put,
575 path = "/v1/region/change_cortical_area_region",
576 tag = "region",
577 responses(
578 (status = 200, description = "Association changed", body = HashMap<String, String>)
579 )
580)]
581pub async fn put_change_cortical_area_region(
582 State(_state): State<ApiState>,
583 Json(_request): Json<HashMap<String, String>>,
584) -> ApiResult<Json<HashMap<String, String>>> {
585 Ok(Json(HashMap::from([(
586 "message".to_string(),
587 "Cortical area region association change not yet implemented".to_string(),
588 )])))
589}