feagi_api/endpoints/
genome.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Genome API Endpoints - Exact port from Python `/v1/genome/*`
5
6// Removed - using crate::common::State instead
7use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, Query, State};
9use feagi_services::types::LoadGenomeParams;
10use std::collections::HashMap;
11use tracing::info;
12
13/// Get the current genome file name.
14#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
15pub async fn get_file_name(
16    State(_state): State<ApiState>,
17) -> ApiResult<Json<HashMap<String, String>>> {
18    // TODO: Get current genome filename
19    Ok(Json(HashMap::from([(
20        "genome_file_name".to_string(),
21        "".to_string(),
22    )])))
23}
24
25/// Get list of available circuit templates from the circuit library.
26#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
27pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
28    // TODO: Get available circuit library
29    Ok(Json(vec![]))
30}
31
32/// Set the destination for genome amalgamation (merging genomes).
33#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
34pub async fn post_amalgamation_destination(
35    State(_state): State<ApiState>,
36    Json(_req): Json<HashMap<String, serde_json::Value>>,
37) -> ApiResult<Json<HashMap<String, String>>> {
38    Err(ApiError::internal("Not yet implemented"))
39}
40
41/// Cancel a pending genome amalgamation operation.
42#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
43pub async fn delete_amalgamation_cancellation(
44    State(_state): State<ApiState>,
45) -> ApiResult<Json<HashMap<String, String>>> {
46    Err(ApiError::internal("Not yet implemented"))
47}
48
49/// Append additional structures to the current genome.
50#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
51pub async fn post_genome_append(
52    State(_state): State<ApiState>,
53    Json(_req): Json<HashMap<String, serde_json::Value>>,
54) -> ApiResult<Json<HashMap<String, String>>> {
55    Err(ApiError::internal("Not yet implemented"))
56}
57
58/// Load the minimal barebones genome with only essential neural structures.
59#[utoipa::path(
60    post,
61    path = "/v1/genome/upload/barebones",
62    responses(
63        (status = 200, description = "Barebones genome loaded successfully"),
64        (status = 500, description = "Failed to load genome")
65    ),
66    tag = "genome"
67)]
68pub async fn post_upload_barebones_genome(
69    State(state): State<ApiState>,
70) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
71    tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
72    let result = load_default_genome(state, "barebones").await;
73    match &result {
74        Ok(_) => {
75            tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
76        }
77        Err(e) => {
78            tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
79        }
80    }
81    result
82}
83
84/// Load the essential genome with core sensory and motor areas.
85#[utoipa::path(
86    post,
87    path = "/v1/genome/upload/essential",
88    responses(
89        (status = 200, description = "Essential genome loaded successfully"),
90        (status = 500, description = "Failed to load genome")
91    ),
92    tag = "genome"
93)]
94pub async fn post_upload_essential_genome(
95    State(state): State<ApiState>,
96) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
97    load_default_genome(state, "essential").await
98}
99
100/// Helper function to load a default genome by name from embedded Rust genomes
101async fn load_default_genome(
102    state: ApiState,
103    genome_name: &str,
104) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
105    tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
106    tracing::debug!(target: "feagi-api", "   State components available: genome_service=true, runtime_service=true");
107
108    // Load genome from embedded Rust templates (no file I/O!)
109    let genome_json = match genome_name {
110        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
111        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
112        "test" => feagi_evolutionary::TEST_GENOME_JSON,
113        "vision" => feagi_evolutionary::VISION_GENOME_JSON,
114        _ => {
115            return Err(ApiError::invalid_input(format!(
116                "Unknown genome name '{}'. Available: barebones, essential, test, vision",
117                genome_name
118            )))
119        }
120    };
121
122    tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
123                   genome_name, genome_json.len());
124
125    // Load genome via service (which will automatically ensure core components)
126    let genome_service = state.genome_service.as_ref();
127    let params = LoadGenomeParams {
128        json_str: genome_json.to_string(),
129    };
130
131    tracing::info!(target: "feagi-api","Calling genome service load_genome...");
132    let genome_info = genome_service
133        .load_genome(params)
134        .await
135        .map_err(|e| ApiError::internal(format!("Failed to load genome: {}", e)))?;
136
137    tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
138               genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
139
140    // CRITICAL: Update burst frequency from genome's simulation_timestep
141    // Genome specifies timestep in seconds, convert to Hz: frequency = 1 / timestep
142    let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
143    tracing::info!(target: "feagi-api","Updating burst frequency from genome: {} seconds timestep → {:.0} Hz",
144                   genome_info.simulation_timestep, burst_frequency_hz);
145
146    // Update runtime service with new frequency
147    let runtime_service = state.runtime_service.as_ref();
148    runtime_service
149        .set_frequency(burst_frequency_hz)
150        .await
151        .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
152
153    tracing::info!(target: "feagi-api","✅ Burst frequency updated to {:.0} Hz from genome physiology", burst_frequency_hz);
154
155    // Return response matching Python format
156    let mut response = HashMap::new();
157    response.insert("success".to_string(), serde_json::Value::Bool(true));
158    response.insert(
159        "message".to_string(),
160        serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
161    );
162    response.insert(
163        "cortical_area_count".to_string(),
164        serde_json::Value::Number(genome_info.cortical_area_count.into()),
165    );
166    response.insert(
167        "brain_region_count".to_string(),
168        serde_json::Value::Number(genome_info.brain_region_count.into()),
169    );
170    response.insert(
171        "genome_id".to_string(),
172        serde_json::Value::String(genome_info.genome_id),
173    );
174    response.insert(
175        "genome_title".to_string(),
176        serde_json::Value::String(genome_info.genome_title),
177    );
178
179    Ok(Json(response))
180}
181
182/// Get the current genome name.
183#[utoipa::path(
184    get,
185    path = "/v1/genome/name",
186    tag = "genome",
187    responses(
188        (status = 200, description = "Genome name", body = String)
189    )
190)]
191pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
192    // Get genome metadata to extract name
193    // TODO: Implement proper genome name retrieval from genome service
194    Ok(Json("default_genome".to_string()))
195}
196
197/// Get the genome creation or modification timestamp.
198#[utoipa::path(
199    get,
200    path = "/v1/genome/timestamp",
201    tag = "genome",
202    responses(
203        (status = 200, description = "Genome timestamp", body = i64)
204    )
205)]
206pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
207    // TODO: Store and retrieve genome timestamp
208    Ok(Json(0))
209}
210
211/// Save the current genome to a file with optional ID and title parameters.
212#[utoipa::path(
213    post,
214    path = "/v1/genome/save",
215    tag = "genome",
216    responses(
217        (status = 200, description = "Genome saved", body = HashMap<String, String>)
218    )
219)]
220pub async fn post_save(
221    State(state): State<ApiState>,
222    Json(request): Json<HashMap<String, String>>,
223) -> ApiResult<Json<HashMap<String, String>>> {
224    use std::fs;
225    use std::path::Path;
226
227    info!("Saving genome to file");
228
229    // Get parameters
230    let genome_id = request.get("genome_id").cloned();
231    let genome_title = request.get("genome_title").cloned();
232    let file_path = request.get("file_path").cloned();
233
234    // Create save parameters
235    let params = feagi_services::SaveGenomeParams {
236        genome_id,
237        genome_title,
238    };
239
240    // Call genome service to generate JSON
241    let genome_service = state.genome_service.as_ref();
242    let genome_json = genome_service
243        .save_genome(params)
244        .await
245        .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
246
247    // Determine file path
248    let save_path = if let Some(path) = file_path {
249        path
250    } else {
251        // Default to genomes directory with timestamp
252        let timestamp = std::time::SystemTime::now()
253            .duration_since(std::time::UNIX_EPOCH)
254            .unwrap()
255            .as_secs();
256        format!("genomes/saved_genome_{}.json", timestamp)
257    };
258
259    // Ensure parent directory exists
260    if let Some(parent) = Path::new(&save_path).parent() {
261        fs::create_dir_all(parent)
262            .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
263    }
264
265    // Write to file
266    fs::write(&save_path, genome_json)
267        .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
268
269    info!("✅ Genome saved successfully to: {}", save_path);
270
271    Ok(Json(HashMap::from([
272        (
273            "message".to_string(),
274            "Genome saved successfully".to_string(),
275        ),
276        ("file_path".to_string(), save_path),
277    ])))
278}
279
280/// Load a genome from a file by name.
281#[utoipa::path(
282    post,
283    path = "/v1/genome/load",
284    tag = "genome",
285    responses(
286        (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
287    )
288)]
289pub async fn post_load(
290    State(state): State<ApiState>,
291    Json(request): Json<HashMap<String, String>>,
292) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
293    let genome_name = request
294        .get("genome_name")
295        .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
296
297    // Load genome from defaults
298    let genome_service = state.genome_service.as_ref();
299    let params = feagi_services::LoadGenomeParams {
300        json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
301    };
302
303    let genome_info = genome_service
304        .load_genome(params)
305        .await
306        .map_err(|e| ApiError::internal(format!("Failed to load genome: {}", e)))?;
307
308    let mut response = HashMap::new();
309    response.insert(
310        "message".to_string(),
311        serde_json::json!("Genome loaded successfully"),
312    );
313    response.insert(
314        "genome_title".to_string(),
315        serde_json::json!(genome_info.genome_title),
316    );
317
318    Ok(Json(response))
319}
320
321/// Upload and load a genome from JSON payload.
322#[utoipa::path(
323    post,
324    path = "/v1/genome/upload",
325    tag = "genome",
326    responses(
327        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
328    )
329)]
330pub async fn post_upload(
331    State(state): State<ApiState>,
332    Json(genome_json): Json<serde_json::Value>,
333) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
334    let genome_service = state.genome_service.as_ref();
335
336    // Convert to JSON string
337    let json_str = serde_json::to_string(&genome_json)
338        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
339
340    let params = LoadGenomeParams { json_str };
341    let genome_info = genome_service
342        .load_genome(params)
343        .await
344        .map_err(|e| ApiError::internal(format!("Failed to upload genome: {}", e)))?;
345
346    let mut response = HashMap::new();
347    response.insert("success".to_string(), serde_json::json!(true));
348    response.insert(
349        "message".to_string(),
350        serde_json::json!("Genome uploaded successfully"),
351    );
352    response.insert(
353        "cortical_area_count".to_string(),
354        serde_json::json!(genome_info.cortical_area_count),
355    );
356    response.insert(
357        "brain_region_count".to_string(),
358        serde_json::json!(genome_info.brain_region_count),
359    );
360
361    Ok(Json(response))
362}
363
364/// Download the current genome as a JSON document.
365#[utoipa::path(
366    get,
367    path = "/v1/genome/download",
368    tag = "genome",
369    responses(
370        (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
371    )
372)]
373pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
374    let genome_service = state.genome_service.as_ref();
375
376    // Get genome as JSON string
377    let genome_json_str = genome_service
378        .save_genome(feagi_services::types::SaveGenomeParams {
379            genome_id: None,
380            genome_title: None,
381        })
382        .await
383        .map_err(|e| ApiError::internal(format!("Failed to export genome: {}", e)))?;
384
385    // Parse to Value for JSON response
386    let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
387        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
388
389    Ok(Json(genome_value))
390}
391
392/// Get genome properties including metadata, size, and configuration details.
393#[utoipa::path(
394    get,
395    path = "/v1/genome/properties",
396    tag = "genome",
397    responses(
398        (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
399    )
400)]
401pub async fn get_properties(
402    State(_state): State<ApiState>,
403) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
404    // TODO: Implement proper metadata retrieval from genome service
405    Ok(Json(HashMap::new()))
406}
407
408/// Validate a genome structure for correctness and completeness.
409#[utoipa::path(
410    post,
411    path = "/v1/genome/validate",
412    tag = "genome",
413    responses(
414        (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
415    )
416)]
417pub async fn post_validate(
418    State(_state): State<ApiState>,
419    Json(_genome): Json<serde_json::Value>,
420) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
421    // TODO: Implement genome validation
422    let mut response = HashMap::new();
423    response.insert("valid".to_string(), serde_json::json!(true));
424    response.insert("errors".to_string(), serde_json::json!([]));
425    response.insert("warnings".to_string(), serde_json::json!([]));
426
427    Ok(Json(response))
428}
429
430/// Transform genome between different formats (flat to hierarchical or vice versa).
431#[utoipa::path(
432    post,
433    path = "/v1/genome/transform",
434    tag = "genome",
435    responses(
436        (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
437    )
438)]
439pub async fn post_transform(
440    State(_state): State<ApiState>,
441    Json(_request): Json<HashMap<String, serde_json::Value>>,
442) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
443    // TODO: Implement genome transformation
444    let mut response = HashMap::new();
445    response.insert(
446        "message".to_string(),
447        serde_json::json!("Genome transformation not yet implemented"),
448    );
449
450    Ok(Json(response))
451}
452
453/// Clone the current genome with a new name, creating an independent copy.
454#[utoipa::path(
455    post,
456    path = "/v1/genome/clone",
457    tag = "genome",
458    responses(
459        (status = 200, description = "Genome cloned", body = HashMap<String, String>)
460    )
461)]
462pub async fn post_clone(
463    State(_state): State<ApiState>,
464    Json(_request): Json<HashMap<String, String>>,
465) -> ApiResult<Json<HashMap<String, String>>> {
466    // TODO: Implement genome cloning
467    Ok(Json(HashMap::from([(
468        "message".to_string(),
469        "Genome cloning not yet implemented".to_string(),
470    )])))
471}
472
473/// Reset genome to its default state, clearing all customizations.
474#[utoipa::path(
475    post,
476    path = "/v1/genome/reset",
477    tag = "genome",
478    responses(
479        (status = 200, description = "Genome reset", body = HashMap<String, String>)
480    )
481)]
482pub async fn post_reset(
483    State(_state): State<ApiState>,
484) -> ApiResult<Json<HashMap<String, String>>> {
485    // TODO: Implement genome reset
486    Ok(Json(HashMap::from([(
487        "message".to_string(),
488        "Genome reset not yet implemented".to_string(),
489    )])))
490}
491
492/// Get genome metadata (alternative endpoint to properties).
493#[utoipa::path(
494    get,
495    path = "/v1/genome/metadata",
496    tag = "genome",
497    responses(
498        (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
499    )
500)]
501pub async fn get_metadata(
502    State(state): State<ApiState>,
503) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
504    get_properties(State(state)).await
505}
506
507/// Merge another genome into the current genome, combining their structures.
508#[utoipa::path(
509    post,
510    path = "/v1/genome/merge",
511    tag = "genome",
512    responses(
513        (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
514    )
515)]
516pub async fn post_merge(
517    State(_state): State<ApiState>,
518    Json(_request): Json<HashMap<String, serde_json::Value>>,
519) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
520    // TODO: Implement genome merging
521    let mut response = HashMap::new();
522    response.insert(
523        "message".to_string(),
524        serde_json::json!("Genome merging not yet implemented"),
525    );
526
527    Ok(Json(response))
528}
529
530/// Get a diff comparison between two genomes showing their differences.
531#[utoipa::path(
532    get,
533    path = "/v1/genome/diff",
534    tag = "genome",
535    params(
536        ("genome_a" = String, Query, description = "First genome name"),
537        ("genome_b" = String, Query, description = "Second genome name")
538    ),
539    responses(
540        (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
541    )
542)]
543pub async fn get_diff(
544    State(_state): State<ApiState>,
545    Query(_params): Query<HashMap<String, String>>,
546) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
547    // TODO: Implement genome diffing
548    let mut response = HashMap::new();
549    response.insert("differences".to_string(), serde_json::json!([]));
550
551    Ok(Json(response))
552}
553
554/// Export genome in a specific format (JSON, YAML, binary, etc.).
555#[utoipa::path(
556    post,
557    path = "/v1/genome/export_format",
558    tag = "genome",
559    responses(
560        (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
561    )
562)]
563pub async fn post_export_format(
564    State(_state): State<ApiState>,
565    Json(_request): Json<HashMap<String, String>>,
566) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
567    // TODO: Implement format-specific export
568    let mut response = HashMap::new();
569    response.insert(
570        "message".to_string(),
571        serde_json::json!("Format export not yet implemented"),
572    );
573
574    Ok(Json(response))
575}
576
577// EXACT Python paths:
578/// Get current amalgamation status and configuration.
579#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
580pub async fn get_amalgamation(
581    State(_state): State<ApiState>,
582) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
583    Ok(Json(HashMap::new()))
584}
585
586/// Get history of all genome amalgamation operations performed.
587#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
588pub async fn get_amalgamation_history_exact(
589    State(_state): State<ApiState>,
590) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
591    Ok(Json(Vec::new()))
592}
593
594/// Get metadata about all available cortical types including supported encodings and configurations.
595#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
596pub async fn get_cortical_template(
597    State(_state): State<ApiState>,
598) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
599    use feagi_structures::genomic::cortical_area::io_cortical_area_data_type::{
600        FrameChangeHandling, IOCorticalAreaDataFlag, PercentageNeuronPositioning,
601    };
602    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
603    use serde_json::json;
604
605    let mut templates = HashMap::new();
606
607    // Helper to convert data type to human-readable format
608    let data_type_to_json = |dt: IOCorticalAreaDataFlag| -> serde_json::Value {
609        let (variant, frame, positioning) = match dt {
610            IOCorticalAreaDataFlag::Boolean => ("Boolean", FrameChangeHandling::Absolute, None),
611            IOCorticalAreaDataFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
612            IOCorticalAreaDataFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
613            IOCorticalAreaDataFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
614            IOCorticalAreaDataFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
615            IOCorticalAreaDataFlag::SignedPercentage(f, p) => ("SignedPercentage", f, Some(p)),
616            IOCorticalAreaDataFlag::SignedPercentage2D(f, p) => ("SignedPercentage2D", f, Some(p)),
617            IOCorticalAreaDataFlag::SignedPercentage3D(f, p) => ("SignedPercentage3D", f, Some(p)),
618            IOCorticalAreaDataFlag::SignedPercentage4D(f, p) => ("SignedPercentage4D", f, Some(p)),
619            IOCorticalAreaDataFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
620            IOCorticalAreaDataFlag::Misc(f) => ("Misc", f, None),
621        };
622
623        let frame_str = match frame {
624            FrameChangeHandling::Absolute => "Absolute",
625            FrameChangeHandling::Incremental => "Incremental",
626        };
627
628        let positioning_str = positioning.map(|p| match p {
629            PercentageNeuronPositioning::Linear => "Linear",
630            PercentageNeuronPositioning::Fractional => "Fractional",
631        });
632
633        json!({
634            "variant": variant,
635            "frame_change_handling": frame_str,
636            "percentage_positioning": positioning_str,
637            "config_value": dt.to_data_type_configuration_flag()
638        })
639    };
640
641    // Add motor types
642    for motor_unit in MotorCorticalUnit::list_all() {
643        let friendly_name = motor_unit.get_friendly_name();
644        let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
645        let num_areas = motor_unit.get_number_cortical_areas();
646        let topology = motor_unit.get_unit_default_topology();
647
648        // Get supported data types for this motor unit
649        // Most motor units support SignedPercentage with both frame modes and both positioning modes
650        let mut data_types = vec![];
651        for frame in [
652            FrameChangeHandling::Absolute,
653            FrameChangeHandling::Incremental,
654        ] {
655            for positioning in [
656                PercentageNeuronPositioning::Linear,
657                PercentageNeuronPositioning::Fractional,
658            ] {
659                let dt = IOCorticalAreaDataFlag::SignedPercentage(frame, positioning);
660                data_types.push(data_type_to_json(dt));
661            }
662        }
663
664        templates.insert(
665            format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
666            json!({
667                "type": "motor",
668                "friendly_name": friendly_name,
669                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
670                "number_of_cortical_areas": num_areas,
671                "unit_default_topology": topology,
672                "supported_data_types": data_types,
673                "description": format!("Motor output: {}", friendly_name)
674            }),
675        );
676    }
677
678    // Add sensory types
679    for sensory_unit in SensoryCorticalUnit::list_all() {
680        let friendly_name = sensory_unit.get_friendly_name();
681        let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
682        let num_areas = sensory_unit.get_number_cortical_areas();
683        let topology = sensory_unit.get_unit_default_topology();
684
685        // Sensory units can support various data types depending on their nature
686        let mut data_types = vec![];
687        for frame in [
688            FrameChangeHandling::Absolute,
689            FrameChangeHandling::Incremental,
690        ] {
691            for positioning in [
692                PercentageNeuronPositioning::Linear,
693                PercentageNeuronPositioning::Fractional,
694            ] {
695                let dt = IOCorticalAreaDataFlag::Percentage(frame, positioning);
696                data_types.push(data_type_to_json(dt));
697            }
698        }
699
700        templates.insert(
701            format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
702            json!({
703                "type": "sensory",
704                "friendly_name": friendly_name,
705                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
706                "number_of_cortical_areas": num_areas,
707                "unit_default_topology": topology,
708                "supported_data_types": data_types,
709                "description": format!("Sensory input: {}", friendly_name)
710            }),
711        );
712    }
713
714    Ok(Json(templates))
715}
716
717/// Get list of available embedded default genome templates (barebones, essential, test, vision).
718#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
719pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
720    Ok(Json(vec![
721        "barebones".to_string(),
722        "essential".to_string(),
723        "test".to_string(),
724        "vision".to_string(),
725    ]))
726}
727
728/// Download a specific brain region from the genome.
729#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
730pub async fn get_download_region(
731    State(_state): State<ApiState>,
732    Query(_params): Query<HashMap<String, String>>,
733) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
734    Ok(Json(HashMap::new()))
735}
736
737/// Get the current genome number or generation identifier.
738#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
739pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
740    Ok(Json(0))
741}
742
743/// Perform genome amalgamation by specifying a filename.
744#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
745pub async fn post_amalgamation_by_filename(
746    State(_state): State<ApiState>,
747    Json(_req): Json<HashMap<String, String>>,
748) -> ApiResult<Json<HashMap<String, String>>> {
749    Ok(Json(HashMap::from([(
750        "message".to_string(),
751        "Not yet implemented".to_string(),
752    )])))
753}
754
755/// Perform genome amalgamation using a direct JSON payload.
756#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
757pub async fn post_amalgamation_by_payload(
758    State(_state): State<ApiState>,
759    Json(_req): Json<serde_json::Value>,
760) -> ApiResult<Json<HashMap<String, String>>> {
761    Ok(Json(HashMap::from([(
762        "message".to_string(),
763        "Not yet implemented".to_string(),
764    )])))
765}
766
767/// Perform genome amalgamation by uploading a genome file.
768#[utoipa::path(post, path = "/v1/genome/amalgamation_by_upload", tag = "genome")]
769pub async fn post_amalgamation_by_upload(
770    State(_state): State<ApiState>,
771    Json(_req): Json<serde_json::Value>,
772) -> ApiResult<Json<HashMap<String, String>>> {
773    Ok(Json(HashMap::from([(
774        "message".to_string(),
775        "Not yet implemented".to_string(),
776    )])))
777}
778
779/// Append structures to the genome from a file.
780#[utoipa::path(post, path = "/v1/genome/append-file", tag = "genome")]
781pub async fn post_append_file(
782    State(_state): State<ApiState>,
783    Json(_req): Json<HashMap<String, String>>,
784) -> ApiResult<Json<HashMap<String, String>>> {
785    Ok(Json(HashMap::from([(
786        "message".to_string(),
787        "Not yet implemented".to_string(),
788    )])))
789}
790
791/// Upload and load a genome from a file.
792#[utoipa::path(post, path = "/v1/genome/upload/file", tag = "genome")]
793pub async fn post_upload_file(
794    State(_state): State<ApiState>,
795    Json(_req): Json<serde_json::Value>,
796) -> ApiResult<Json<HashMap<String, String>>> {
797    Ok(Json(HashMap::from([(
798        "message".to_string(),
799        "Not yet implemented".to_string(),
800    )])))
801}
802
803/// Upload a genome file with edit mode enabled.
804#[utoipa::path(post, path = "/v1/genome/upload/file/edit", tag = "genome")]
805pub async fn post_upload_file_edit(
806    State(_state): State<ApiState>,
807    Json(_req): Json<HashMap<String, String>>,
808) -> ApiResult<Json<HashMap<String, String>>> {
809    Ok(Json(HashMap::from([(
810        "message".to_string(),
811        "Not yet implemented".to_string(),
812    )])))
813}
814
815/// Upload and load a genome from a JSON string.
816#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
817pub async fn post_upload_string(
818    State(_state): State<ApiState>,
819    Json(_req): Json<String>,
820) -> ApiResult<Json<HashMap<String, String>>> {
821    Ok(Json(HashMap::from([(
822        "message".to_string(),
823        "Not yet implemented".to_string(),
824    )])))
825}