Skip to main content

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::amalgamation;
8use crate::common::ApiState;
9use crate::common::{ApiError, ApiResult, Json, Query, State};
10use feagi_services::types::{GenomeInfo, LoadGenomeParams};
11use std::collections::HashMap;
12use std::sync::atomic::Ordering;
13use std::sync::Arc;
14use tracing::info;
15use uuid::Uuid;
16
17#[cfg(feature = "http")]
18use axum::extract::Multipart;
19
20/// Multipart file upload schema for Swagger UI.
21///
22/// This enables Swagger to show a file picker for endpoints that accept genome JSON files.
23#[derive(Debug, Clone, utoipa::ToSchema)]
24pub struct GenomeFileUploadForm {
25    /// Genome JSON file contents.
26    #[schema(value_type = String, format = Binary)]
27    pub file: String,
28}
29
30fn queue_amalgamation_from_genome_json_str(
31    state: &ApiState,
32    genome_json: String,
33) -> Result<String, ApiError> {
34    // Only one pending amalgamation is supported per FEAGI session (matches BV workflow).
35    {
36        let lock = state.amalgamation_state.read();
37        if lock.pending.is_some() {
38            return Err(ApiError::invalid_input(
39                "Amalgamation already pending; cancel it first via /v1/genome/amalgamation_cancellation",
40            ));
41        }
42    }
43
44    let genome = feagi_evolutionary::load_genome_from_json(&genome_json)
45        .map_err(|e| ApiError::invalid_input(format!("Invalid genome payload: {}", e)))?;
46
47    let circuit_size = amalgamation::compute_circuit_size_from_runtime_genome(&genome);
48
49    let amalgamation_id = Uuid::new_v4().to_string();
50    let genome_title = genome.metadata.genome_title.clone();
51
52    let summary = amalgamation::AmalgamationPendingSummary {
53        amalgamation_id: amalgamation_id.clone(),
54        genome_title,
55        circuit_size,
56    };
57
58    let pending = amalgamation::AmalgamationPending {
59        summary: summary.clone(),
60        genome_json,
61    };
62
63    {
64        let mut lock = state.amalgamation_state.write();
65        let now_ms = std::time::SystemTime::now()
66            .duration_since(std::time::UNIX_EPOCH)
67            .map(|d| d.as_millis() as i64)
68            .unwrap_or(0);
69
70        lock.history.push(amalgamation::AmalgamationHistoryEntry {
71            amalgamation_id: summary.amalgamation_id.clone(),
72            genome_title: summary.genome_title.clone(),
73            circuit_size: summary.circuit_size,
74            status: "pending".to_string(),
75            timestamp_ms: now_ms,
76        });
77        lock.pending = Some(pending);
78    }
79
80    tracing::info!(
81        target: "feagi-api",
82        "🧬 [AMALGAMATION] Queued pending amalgamation id={} title='{}' circuit_size={:?}",
83        summary.amalgamation_id,
84        summary.genome_title,
85        summary.circuit_size
86    );
87
88    Ok(amalgamation_id)
89}
90
91struct GenomeTransitionFlagGuard {
92    in_progress: Arc<std::sync::atomic::AtomicBool>,
93}
94
95impl Drop for GenomeTransitionFlagGuard {
96    fn drop(&mut self) {
97        self.in_progress.store(false, Ordering::SeqCst);
98    }
99}
100
101/// Execute a genome load with strict priority over concurrent operations.
102///
103/// Guarantees:
104/// - Only one genome transition may run at a time.
105/// - Runtime is quiesced before load starts.
106/// - Runtime frequency is updated from genome physiology.
107/// - Runtime is restored to running state if it was running before transition.
108async fn load_genome_with_priority(
109    state: &ApiState,
110    params: LoadGenomeParams,
111    source: &str,
112) -> ApiResult<GenomeInfo> {
113    let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
114        ApiError::conflict(
115            "Another genome transition is already in progress; wait for it to finish",
116        )
117    })?;
118    state
119        .genome_transition_in_progress
120        .store(true, Ordering::SeqCst);
121    let _guard = GenomeTransitionFlagGuard {
122        in_progress: Arc::clone(&state.genome_transition_in_progress),
123    };
124
125    tracing::info!(
126        target: "feagi-api",
127        "🛑 Entering prioritized genome transition from {}",
128        source
129    );
130
131    let runtime_service = state.runtime_service.as_ref();
132    let runtime_status = runtime_service
133        .get_status()
134        .await
135        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
136    let runtime_was_running = runtime_status.is_running;
137
138    if runtime_was_running {
139        tracing::info!(
140            target: "feagi-api",
141            "Stopping burst engine before prioritized genome transition"
142        );
143        runtime_service.stop().await.map_err(|e| {
144            ApiError::internal(format!(
145                "Failed to stop burst engine before genome transition: {}",
146                e
147            ))
148        })?;
149    }
150
151    let genome_service = state.genome_service.as_ref();
152    let load_result = genome_service.load_genome(params).await;
153    let genome_info = match load_result {
154        Ok(info) => info,
155        Err(e) => {
156            if runtime_was_running {
157                if let Err(restart_err) = runtime_service.start().await {
158                    tracing::warn!(
159                        target: "feagi-api",
160                        "Failed to restore runtime after failed genome load (source={}): {}",
161                        source,
162                        restart_err
163                    );
164                }
165            }
166            return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
167        }
168    };
169
170    let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
171    runtime_service
172        .set_frequency(burst_frequency_hz)
173        .await
174        .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
175
176    if runtime_was_running {
177        runtime_service.start().await.map_err(|e| {
178            ApiError::internal(format!(
179                "Failed to restart burst engine after genome transition: {}",
180                e
181            ))
182        })?;
183    }
184
185    tracing::info!(
186        target: "feagi-api",
187        "✅ Prioritized genome transition completed from {}",
188        source
189    );
190    Ok(genome_info)
191}
192
193/// Inject the current runtime simulation timestep (seconds) into a genome JSON value.
194///
195/// Rationale: the burst engine timestep can be updated at runtime, but `GenomeService::save_genome()`
196/// serializes the stored `RuntimeGenome` (which may still have the older physiology value).
197/// This keeps exported/saved genomes consistent with the *current* FEAGI simulation state.
198fn inject_simulation_timestep_into_genome(
199    mut genome: serde_json::Value,
200    simulation_timestep_s: f64,
201) -> Result<serde_json::Value, ApiError> {
202    let physiology = genome
203        .get_mut("physiology")
204        .and_then(|v| v.as_object_mut())
205        .ok_or_else(|| {
206            ApiError::internal(
207                "Genome JSON missing required object key 'physiology' while saving".to_string(),
208            )
209        })?;
210
211    physiology.insert(
212        "simulation_timestep".to_string(),
213        serde_json::Value::from(simulation_timestep_s),
214    );
215    Ok(genome)
216}
217
218async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
219    let runtime_service = state.runtime_service.as_ref();
220    let status = runtime_service
221        .get_status()
222        .await
223        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
224
225    // Convert frequency (Hz) to timestep (seconds).
226    Ok(if status.frequency_hz > 0.0 {
227        1.0 / status.frequency_hz
228    } else {
229        0.0
230    })
231}
232
233/// Get the current genome file name.
234#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
235pub async fn get_file_name(
236    State(_state): State<ApiState>,
237) -> ApiResult<Json<HashMap<String, String>>> {
238    // TODO: Get current genome filename
239    Ok(Json(HashMap::from([(
240        "genome_file_name".to_string(),
241        "".to_string(),
242    )])))
243}
244
245/// Get list of available circuit templates from the circuit library.
246#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
247pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
248    // TODO: Get available circuit library
249    Ok(Json(vec![]))
250}
251
252/// Set the destination for genome amalgamation (merging genomes).
253#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
254pub async fn post_amalgamation_destination(
255    State(state): State<ApiState>,
256    Query(params): Query<HashMap<String, String>>,
257    Json(req): Json<HashMap<String, serde_json::Value>>,
258) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
259    // BV sends query params:
260    // - circuit_origin_x/y/z
261    // - amalgamation_id
262    // - rewire_mode
263    //
264    // Body:
265    // - brain_region_id
266    let amalgamation_id = params
267        .get("amalgamation_id")
268        .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
269        .to_string();
270
271    let origin_x: i32 = params
272        .get("circuit_origin_x")
273        .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
274        .parse()
275        .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
276    let origin_y: i32 = params
277        .get("circuit_origin_y")
278        .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
279        .parse()
280        .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
281    let origin_z: i32 = params
282        .get("circuit_origin_z")
283        .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
284        .parse()
285        .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
286
287    let rewire_mode = params
288        .get("rewire_mode")
289        .cloned()
290        .unwrap_or_else(|| "rewire_all".to_string());
291
292    let parent_region_id = req
293        .get("brain_region_id")
294        .and_then(|v| v.as_str())
295        .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
296        .to_string();
297
298    // Resolve and consume the pending request.
299    let pending = {
300        let lock = state.amalgamation_state.write();
301        let Some(p) = lock.pending.as_ref() else {
302            return Err(ApiError::invalid_input("No amalgamation is pending"));
303        };
304        if p.summary.amalgamation_id != amalgamation_id {
305            return Err(ApiError::invalid_input(format!(
306                "Pending amalgamation_id mismatch: expected {}, got {}",
307                p.summary.amalgamation_id, amalgamation_id
308            )));
309        }
310        p.clone()
311    };
312
313    // 1) Create a new brain region to host the imported circuit.
314    // Note: ConnectomeServiceImpl shares the same RuntimeGenome Arc with GenomeServiceImpl, so
315    // persisting the region into the RuntimeGenome is required for subsequent cortical-area creation.
316    let connectome_service = state.connectome_service.as_ref();
317
318    let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
319    region_properties.insert(
320        "coordinate_3d".to_string(),
321        serde_json::json!([origin_x, origin_y, origin_z]),
322    );
323    region_properties.insert(
324        "amalgamation_id".to_string(),
325        serde_json::json!(pending.summary.amalgamation_id),
326    );
327    region_properties.insert(
328        "circuit_size".to_string(),
329        serde_json::json!(pending.summary.circuit_size),
330    );
331    region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
332
333    connectome_service
334        .create_brain_region(feagi_services::types::CreateBrainRegionParams {
335            region_id: amalgamation_id.clone(),
336            name: pending.summary.genome_title.clone(),
337            region_type: "Custom".to_string(),
338            parent_id: Some(parent_region_id.clone()),
339            properties: Some(region_properties),
340        })
341        .await
342        .map_err(|e| {
343            ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
344        })?;
345
346    // 2) Import cortical areas into that region.
347    //
348    // Current deterministic behavior:
349    // - We import *only* cortical areas whose IDs do not exist in the current connectome.
350    // - We place them at an offset relative to the chosen origin.
351    // - We assign parent_region_id to the new region so the genome stays consistent.
352    //
353    // If a genome contains shared/global IDs (e.g., core areas), those will be skipped.
354    let imported_genome =
355        feagi_evolutionary::load_genome_from_json(&pending.genome_json).map_err(|e| {
356            ApiError::invalid_input(format!(
357                "Pending genome payload can no longer be parsed as a genome: {}",
358                e
359            ))
360        })?;
361
362    let genome_service = state.genome_service.as_ref();
363    let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
364    let mut skipped_existing: Vec<String> = Vec::new();
365
366    for area in imported_genome.cortical_areas.values() {
367        let cortical_id = area.cortical_id.as_base_64();
368        let exists = connectome_service
369            .cortical_area_exists(&cortical_id)
370            .await
371            .map_err(|e| {
372                ApiError::internal(format!(
373                    "Failed to check existing cortical area {}: {}",
374                    cortical_id, e
375                ))
376            })?;
377        if exists {
378            skipped_existing.push(cortical_id);
379            continue;
380        }
381
382        let mut props = area.properties.clone();
383        props.insert(
384            "parent_region_id".to_string(),
385            serde_json::json!(amalgamation_id.clone()),
386        );
387        props.insert(
388            "amalgamation_source".to_string(),
389            serde_json::json!("amalgamation_by_payload"),
390        );
391
392        to_create.push(feagi_services::types::CreateCorticalAreaParams {
393            cortical_id,
394            name: area.name.clone(),
395            dimensions: (
396                area.dimensions.width as usize,
397                area.dimensions.height as usize,
398                area.dimensions.depth as usize,
399            ),
400            position: (
401                origin_x.saturating_add(area.position.x),
402                origin_y.saturating_add(area.position.y),
403                origin_z.saturating_add(area.position.z),
404            ),
405            area_type: "Custom".to_string(),
406            visible: Some(true),
407            sub_group: None,
408            neurons_per_voxel: area
409                .properties
410                .get("neurons_per_voxel")
411                .and_then(|v| v.as_u64())
412                .map(|v| v as u32),
413            postsynaptic_current: area
414                .properties
415                .get("postsynaptic_current")
416                .and_then(|v| v.as_f64()),
417            plasticity_constant: area
418                .properties
419                .get("plasticity_constant")
420                .and_then(|v| v.as_f64()),
421            degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
422            psp_uniform_distribution: area
423                .properties
424                .get("psp_uniform_distribution")
425                .and_then(|v| v.as_bool()),
426            firing_threshold_increment: None,
427            firing_threshold_limit: area
428                .properties
429                .get("firing_threshold_limit")
430                .and_then(|v| v.as_f64()),
431            consecutive_fire_count: area
432                .properties
433                .get("consecutive_fire_limit")
434                .and_then(|v| v.as_u64())
435                .map(|v| v as u32),
436            snooze_period: area
437                .properties
438                .get("snooze_period")
439                .and_then(|v| v.as_u64())
440                .map(|v| v as u32),
441            refractory_period: area
442                .properties
443                .get("refractory_period")
444                .and_then(|v| v.as_u64())
445                .map(|v| v as u32),
446            leak_coefficient: area
447                .properties
448                .get("leak_coefficient")
449                .and_then(|v| v.as_f64()),
450            leak_variability: area
451                .properties
452                .get("leak_variability")
453                .and_then(|v| v.as_f64()),
454            burst_engine_active: area
455                .properties
456                .get("burst_engine_active")
457                .and_then(|v| v.as_bool()),
458            properties: Some(props),
459        });
460    }
461
462    if !to_create.is_empty() {
463        genome_service
464            .create_cortical_areas(to_create)
465            .await
466            .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
467    }
468
469    // Clear pending + write history entry
470    {
471        let mut lock = state.amalgamation_state.write();
472        let now_ms = std::time::SystemTime::now()
473            .duration_since(std::time::UNIX_EPOCH)
474            .map(|d| d.as_millis() as i64)
475            .unwrap_or(0);
476        lock.history.push(amalgamation::AmalgamationHistoryEntry {
477            amalgamation_id: pending.summary.amalgamation_id.clone(),
478            genome_title: pending.summary.genome_title.clone(),
479            circuit_size: pending.summary.circuit_size,
480            status: "confirmed".to_string(),
481            timestamp_ms: now_ms,
482        });
483        lock.pending = None;
484    }
485
486    tracing::info!(
487        target: "feagi-api",
488        "🧬 [AMALGAMATION] Confirmed and cleared pending amalgamation id={} imported_areas={} skipped_existing_areas={}",
489        pending.summary.amalgamation_id,
490        if skipped_existing.is_empty() { "unknown".to_string() } else { "partial".to_string() },
491        skipped_existing.len()
492    );
493
494    // Build a BV-compatible list response for brain regions (regions_members-like data, but as list).
495    let regions = state
496        .connectome_service
497        .list_brain_regions()
498        .await
499        .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
500
501    let mut brain_regions: Vec<serde_json::Value> = Vec::new();
502    for region in regions {
503        // Shape matches BV expectations in FEAGIRequests.gd
504        let coordinate_3d = region
505            .properties
506            .get("coordinate_3d")
507            .cloned()
508            .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
509        let coordinate_2d = region
510            .properties
511            .get("coordinate_2d")
512            .cloned()
513            .unwrap_or_else(|| serde_json::json!([0, 0]));
514
515        brain_regions.push(serde_json::json!({
516            "region_id": region.region_id,
517            "title": region.name,
518            "description": "",
519            "parent_region_id": region.parent_id,
520            "coordinate_2d": coordinate_2d,
521            "coordinate_3d": coordinate_3d,
522            "areas": region.cortical_areas,
523            "regions": region.child_regions,
524            "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
525            "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
526        }));
527    }
528
529    Ok(Json(HashMap::from([
530        (
531            "message".to_string(),
532            serde_json::Value::String("Amalgamation confirmed".to_string()),
533        ),
534        (
535            "brain_regions".to_string(),
536            serde_json::Value::Array(brain_regions),
537        ),
538        (
539            "skipped_existing_areas".to_string(),
540            serde_json::json!(skipped_existing),
541        ),
542    ])))
543}
544
545/// Cancel a pending genome amalgamation operation.
546#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
547pub async fn delete_amalgamation_cancellation(
548    State(state): State<ApiState>,
549) -> ApiResult<Json<HashMap<String, String>>> {
550    let mut lock = state.amalgamation_state.write();
551    if let Some(pending) = lock.pending.take() {
552        let now_ms = std::time::SystemTime::now()
553            .duration_since(std::time::UNIX_EPOCH)
554            .map(|d| d.as_millis() as i64)
555            .unwrap_or(0);
556        lock.history.push(amalgamation::AmalgamationHistoryEntry {
557            amalgamation_id: pending.summary.amalgamation_id,
558            genome_title: pending.summary.genome_title,
559            circuit_size: pending.summary.circuit_size,
560            status: "cancelled".to_string(),
561            timestamp_ms: now_ms,
562        });
563
564        tracing::info!(
565            target: "feagi-api",
566            "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
567            lock.history
568                .last()
569                .map(|e| e.amalgamation_id.clone())
570                .unwrap_or_else(|| "<unknown>".to_string())
571        );
572    }
573    Ok(Json(HashMap::from([(
574        "message".to_string(),
575        "Amalgamation cancelled".to_string(),
576    )])))
577}
578
579/// Append additional structures to the current genome.
580#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
581pub async fn post_genome_append(
582    State(_state): State<ApiState>,
583    Json(_req): Json<HashMap<String, serde_json::Value>>,
584) -> ApiResult<Json<HashMap<String, String>>> {
585    Err(ApiError::internal("Not yet implemented"))
586}
587
588/// Load the minimal barebones genome with only essential neural structures.
589#[utoipa::path(
590    post,
591    path = "/v1/genome/upload/barebones",
592    responses(
593        (status = 200, description = "Barebones genome loaded successfully"),
594        (status = 500, description = "Failed to load genome")
595    ),
596    tag = "genome"
597)]
598pub async fn post_upload_barebones_genome(
599    State(state): State<ApiState>,
600) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
601    tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
602    let result = load_default_genome(state, "barebones").await;
603    match &result {
604        Ok(_) => {
605            tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
606        }
607        Err(e) => {
608            tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
609        }
610    }
611    result
612}
613
614/// Load the essential genome with core sensory and motor areas.
615#[utoipa::path(
616    post,
617    path = "/v1/genome/upload/essential",
618    responses(
619        (status = 200, description = "Essential genome loaded successfully"),
620        (status = 500, description = "Failed to load genome")
621    ),
622    tag = "genome"
623)]
624pub async fn post_upload_essential_genome(
625    State(state): State<ApiState>,
626) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
627    load_default_genome(state, "essential").await
628}
629
630/// Helper function to load a default genome by name from embedded Rust genomes
631async fn load_default_genome(
632    state: ApiState,
633    genome_name: &str,
634) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
635    tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
636    tracing::debug!(target: "feagi-api", "   State components available: genome_service=true, runtime_service=true");
637    // Load genome from embedded Rust templates (no file I/O!)
638    let genome_json = match genome_name {
639        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
640        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
641        "test" => feagi_evolutionary::TEST_GENOME_JSON,
642        "vision" => feagi_evolutionary::VISION_GENOME_JSON,
643        _ => {
644            return Err(ApiError::invalid_input(format!(
645                "Unknown genome name '{}'. Available: barebones, essential, test, vision",
646                genome_name
647            )))
648        }
649    };
650
651    tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
652                   genome_name, genome_json.len());
653
654    let params = LoadGenomeParams {
655        json_str: genome_json.to_string(),
656    };
657
658    tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
659    let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
660
661    tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
662               genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
663
664    // Return response matching Python format
665    let mut response = HashMap::new();
666    response.insert("success".to_string(), serde_json::Value::Bool(true));
667    response.insert(
668        "message".to_string(),
669        serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
670    );
671    response.insert(
672        "cortical_area_count".to_string(),
673        serde_json::Value::Number(genome_info.cortical_area_count.into()),
674    );
675    response.insert(
676        "brain_region_count".to_string(),
677        serde_json::Value::Number(genome_info.brain_region_count.into()),
678    );
679    response.insert(
680        "genome_id".to_string(),
681        serde_json::Value::String(genome_info.genome_id),
682    );
683    response.insert(
684        "genome_title".to_string(),
685        serde_json::Value::String(genome_info.genome_title),
686    );
687
688    Ok(Json(response))
689}
690
691/// Get the current genome name.
692#[utoipa::path(
693    get,
694    path = "/v1/genome/name",
695    tag = "genome",
696    responses(
697        (status = 200, description = "Genome name", body = String)
698    )
699)]
700pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
701    // Get genome metadata to extract name
702    // TODO: Implement proper genome name retrieval from genome service
703    Ok(Json("default_genome".to_string()))
704}
705
706/// Get the genome creation or modification timestamp.
707#[utoipa::path(
708    get,
709    path = "/v1/genome/timestamp",
710    tag = "genome",
711    responses(
712        (status = 200, description = "Genome timestamp", body = i64)
713    )
714)]
715pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
716    // TODO: Store and retrieve genome timestamp
717    Ok(Json(0))
718}
719
720/// Save the current genome to a file with optional ID and title parameters.
721#[utoipa::path(
722    post,
723    path = "/v1/genome/save",
724    tag = "genome",
725    responses(
726        (status = 200, description = "Genome saved", body = HashMap<String, String>)
727    )
728)]
729pub async fn post_save(
730    State(state): State<ApiState>,
731    Json(request): Json<HashMap<String, String>>,
732) -> ApiResult<Json<HashMap<String, String>>> {
733    use std::fs;
734    use std::path::Path;
735
736    info!("Saving genome to file");
737
738    // Get parameters
739    let genome_id = request.get("genome_id").cloned();
740    let genome_title = request.get("genome_title").cloned();
741    let file_path = request.get("file_path").cloned();
742
743    // Create save parameters
744    let params = feagi_services::SaveGenomeParams {
745        genome_id,
746        genome_title,
747    };
748
749    // Call genome service to generate JSON
750    let genome_service = state.genome_service.as_ref();
751    let genome_json = genome_service
752        .save_genome(params)
753        .await
754        .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
755
756    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at save time.
757    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
758    let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
759        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
760    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
761    let genome_json = serde_json::to_string_pretty(&genome_value)
762        .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
763
764    // Determine file path
765    let save_path = if let Some(path) = file_path {
766        std::path::PathBuf::from(path)
767    } else {
768        // Default to hidden genome directory with timestamp.
769        let timestamp = std::time::SystemTime::now()
770            .duration_since(std::time::UNIX_EPOCH)
771            .unwrap()
772            .as_secs();
773        std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
774    };
775
776    // Ensure parent directory exists
777    if let Some(parent) = Path::new(&save_path).parent() {
778        fs::create_dir_all(parent)
779            .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
780    }
781
782    // Write to file
783    fs::write(&save_path, genome_json)
784        .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
785
786    info!("✅ Genome saved successfully to: {}", save_path.display());
787
788    Ok(Json(HashMap::from([
789        (
790            "message".to_string(),
791            "Genome saved successfully".to_string(),
792        ),
793        ("file_path".to_string(), save_path.display().to_string()),
794    ])))
795}
796
797/// Load a genome from a file by name.
798#[utoipa::path(
799    post,
800    path = "/v1/genome/load",
801    tag = "genome",
802    responses(
803        (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
804    )
805)]
806pub async fn post_load(
807    State(state): State<ApiState>,
808    Json(request): Json<HashMap<String, String>>,
809) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
810    let genome_name = request
811        .get("genome_name")
812        .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
813
814    // Load genome from defaults
815    let params = feagi_services::LoadGenomeParams {
816        json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
817    };
818
819    let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
820
821    let mut response = HashMap::new();
822    response.insert(
823        "message".to_string(),
824        serde_json::json!("Genome loaded successfully"),
825    );
826    response.insert(
827        "genome_title".to_string(),
828        serde_json::json!(genome_info.genome_title),
829    );
830
831    Ok(Json(response))
832}
833
834/// Upload and load a genome from JSON payload.
835#[utoipa::path(
836    post,
837    path = "/v1/genome/upload",
838    tag = "genome",
839    responses(
840        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
841    )
842)]
843pub async fn post_upload(
844    State(state): State<ApiState>,
845    Json(genome_json): Json<serde_json::Value>,
846) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
847    // Convert to JSON string
848    let json_str = serde_json::to_string(&genome_json)
849        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
850
851    let params = LoadGenomeParams { json_str };
852    let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
853
854    let mut response = HashMap::new();
855    response.insert("success".to_string(), serde_json::json!(true));
856    response.insert(
857        "message".to_string(),
858        serde_json::json!("Genome uploaded successfully"),
859    );
860    response.insert(
861        "cortical_area_count".to_string(),
862        serde_json::json!(genome_info.cortical_area_count),
863    );
864    response.insert(
865        "brain_region_count".to_string(),
866        serde_json::json!(genome_info.brain_region_count),
867    );
868
869    Ok(Json(response))
870}
871
872/// Download the current genome as a JSON document.
873#[utoipa::path(
874    get,
875    path = "/v1/genome/download",
876    tag = "genome",
877    responses(
878        (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
879    )
880)]
881pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
882    info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
883    let genome_service = state.genome_service.as_ref();
884
885    // Get genome as JSON string
886    let genome_json_str = genome_service
887        .save_genome(feagi_services::types::SaveGenomeParams {
888            genome_id: None,
889            genome_title: None,
890        })
891        .await
892        .map_err(|e| {
893            tracing::error!("Failed to export genome: {}", e);
894            ApiError::internal(format!("Failed to export genome: {}", e))
895        })?;
896
897    // Parse to Value for JSON response
898    let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
899        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
900
901    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at download time.
902    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
903    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
904
905    info!(
906        "✅ Genome download complete, {} bytes",
907        genome_json_str.len()
908    );
909    Ok(Json(genome_value))
910}
911
912#[cfg(test)]
913mod tests {
914    use super::*;
915    use serde_json::json;
916
917    #[test]
918    fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
919        let genome = json!({
920            "version": "3.0",
921            "physiology": {
922                "simulation_timestep": 0.025,
923                "max_age": 10000000
924            }
925        });
926
927        let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
928        assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
929        assert_eq!(updated["physiology"]["max_age"], json!(10000000));
930    }
931
932    #[test]
933    fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
934        let genome = json!({ "version": "3.0" });
935        let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
936        assert!(format!("{err:?}").contains("physiology"));
937    }
938}
939
940/// Get genome properties including metadata, size, and configuration details.
941#[utoipa::path(
942    get,
943    path = "/v1/genome/properties",
944    tag = "genome",
945    responses(
946        (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
947    )
948)]
949pub async fn get_properties(
950    State(_state): State<ApiState>,
951) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
952    // TODO: Implement proper metadata retrieval from genome service
953    Ok(Json(HashMap::new()))
954}
955
956/// Validate a genome structure for correctness and completeness.
957#[utoipa::path(
958    post,
959    path = "/v1/genome/validate",
960    tag = "genome",
961    responses(
962        (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
963    )
964)]
965pub async fn post_validate(
966    State(_state): State<ApiState>,
967    Json(_genome): Json<serde_json::Value>,
968) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
969    // TODO: Implement genome validation
970    let mut response = HashMap::new();
971    response.insert("valid".to_string(), serde_json::json!(true));
972    response.insert("errors".to_string(), serde_json::json!([]));
973    response.insert("warnings".to_string(), serde_json::json!([]));
974
975    Ok(Json(response))
976}
977
978/// Transform genome between different formats (flat to hierarchical or vice versa).
979#[utoipa::path(
980    post,
981    path = "/v1/genome/transform",
982    tag = "genome",
983    responses(
984        (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
985    )
986)]
987pub async fn post_transform(
988    State(_state): State<ApiState>,
989    Json(_request): Json<HashMap<String, serde_json::Value>>,
990) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
991    // TODO: Implement genome transformation
992    let mut response = HashMap::new();
993    response.insert(
994        "message".to_string(),
995        serde_json::json!("Genome transformation not yet implemented"),
996    );
997
998    Ok(Json(response))
999}
1000
1001/// Clone the current genome with a new name, creating an independent copy.
1002#[utoipa::path(
1003    post,
1004    path = "/v1/genome/clone",
1005    tag = "genome",
1006    responses(
1007        (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1008    )
1009)]
1010pub async fn post_clone(
1011    State(_state): State<ApiState>,
1012    Json(_request): Json<HashMap<String, String>>,
1013) -> ApiResult<Json<HashMap<String, String>>> {
1014    // TODO: Implement genome cloning
1015    Ok(Json(HashMap::from([(
1016        "message".to_string(),
1017        "Genome cloning not yet implemented".to_string(),
1018    )])))
1019}
1020
1021/// Reset genome to its default state, clearing all customizations.
1022#[utoipa::path(
1023    post,
1024    path = "/v1/genome/reset",
1025    tag = "genome",
1026    responses(
1027        (status = 200, description = "Genome reset", body = HashMap<String, String>)
1028    )
1029)]
1030pub async fn post_reset(
1031    State(_state): State<ApiState>,
1032) -> ApiResult<Json<HashMap<String, String>>> {
1033    // TODO: Implement genome reset
1034    Ok(Json(HashMap::from([(
1035        "message".to_string(),
1036        "Genome reset not yet implemented".to_string(),
1037    )])))
1038}
1039
1040/// Get genome metadata (alternative endpoint to properties).
1041#[utoipa::path(
1042    get,
1043    path = "/v1/genome/metadata",
1044    tag = "genome",
1045    responses(
1046        (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1047    )
1048)]
1049pub async fn get_metadata(
1050    State(state): State<ApiState>,
1051) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1052    get_properties(State(state)).await
1053}
1054
1055/// Merge another genome into the current genome, combining their structures.
1056#[utoipa::path(
1057    post,
1058    path = "/v1/genome/merge",
1059    tag = "genome",
1060    responses(
1061        (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1062    )
1063)]
1064pub async fn post_merge(
1065    State(_state): State<ApiState>,
1066    Json(_request): Json<HashMap<String, serde_json::Value>>,
1067) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1068    // TODO: Implement genome merging
1069    let mut response = HashMap::new();
1070    response.insert(
1071        "message".to_string(),
1072        serde_json::json!("Genome merging not yet implemented"),
1073    );
1074
1075    Ok(Json(response))
1076}
1077
1078/// Get a diff comparison between two genomes showing their differences.
1079#[utoipa::path(
1080    get,
1081    path = "/v1/genome/diff",
1082    tag = "genome",
1083    params(
1084        ("genome_a" = String, Query, description = "First genome name"),
1085        ("genome_b" = String, Query, description = "Second genome name")
1086    ),
1087    responses(
1088        (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1089    )
1090)]
1091pub async fn get_diff(
1092    State(_state): State<ApiState>,
1093    Query(_params): Query<HashMap<String, String>>,
1094) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1095    // TODO: Implement genome diffing
1096    let mut response = HashMap::new();
1097    response.insert("differences".to_string(), serde_json::json!([]));
1098
1099    Ok(Json(response))
1100}
1101
1102/// Export genome in a specific format (JSON, YAML, binary, etc.).
1103#[utoipa::path(
1104    post,
1105    path = "/v1/genome/export_format",
1106    tag = "genome",
1107    responses(
1108        (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1109    )
1110)]
1111pub async fn post_export_format(
1112    State(_state): State<ApiState>,
1113    Json(_request): Json<HashMap<String, String>>,
1114) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1115    // TODO: Implement format-specific export
1116    let mut response = HashMap::new();
1117    response.insert(
1118        "message".to_string(),
1119        serde_json::json!("Format export not yet implemented"),
1120    );
1121
1122    Ok(Json(response))
1123}
1124
1125// EXACT Python paths:
1126/// Get current amalgamation status and configuration.
1127#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1128pub async fn get_amalgamation(
1129    State(state): State<ApiState>,
1130) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1131    let lock = state.amalgamation_state.read();
1132    let mut response = HashMap::new();
1133    if let Some(p) = lock.pending.as_ref() {
1134        response.insert(
1135            "pending".to_string(),
1136            amalgamation::pending_summary_to_health_json(&p.summary),
1137        );
1138    } else {
1139        response.insert("pending".to_string(), serde_json::Value::Null);
1140    }
1141    Ok(Json(response))
1142}
1143
1144/// Get history of all genome amalgamation operations performed.
1145#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1146pub async fn get_amalgamation_history_exact(
1147    State(state): State<ApiState>,
1148) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1149    let lock = state.amalgamation_state.read();
1150    let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1151    for entry in &lock.history {
1152        out.push(HashMap::from([
1153            (
1154                "amalgamation_id".to_string(),
1155                serde_json::json!(entry.amalgamation_id),
1156            ),
1157            (
1158                "genome_title".to_string(),
1159                serde_json::json!(entry.genome_title),
1160            ),
1161            (
1162                "circuit_size".to_string(),
1163                serde_json::json!(entry.circuit_size),
1164            ),
1165            ("status".to_string(), serde_json::json!(entry.status)),
1166            (
1167                "timestamp_ms".to_string(),
1168                serde_json::json!(entry.timestamp_ms),
1169            ),
1170        ]));
1171    }
1172    Ok(Json(out))
1173}
1174
1175/// Get metadata about all available cortical types including supported encodings and configurations.
1176#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1177pub async fn get_cortical_template(
1178    State(_state): State<ApiState>,
1179) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1180    use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1181        FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1182    };
1183    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1184    use serde_json::json;
1185
1186    let mut templates = HashMap::new();
1187
1188    // Helper to convert data type to human-readable format.
1189    //
1190    // NOTE: This endpoint is designed for tool/UIs (e.g. BV) and must be
1191    // deterministic across platforms and runs. No fallbacks.
1192    let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1193        let (variant, frame, positioning) = match dt {
1194            IOCorticalAreaConfigurationFlag::Boolean => {
1195                ("Boolean", FrameChangeHandling::Absolute, None)
1196            }
1197            IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1198            IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1199            IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1200            IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1201            IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1202                ("SignedPercentage", f, Some(p))
1203            }
1204            IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1205                ("SignedPercentage2D", f, Some(p))
1206            }
1207            IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1208                ("SignedPercentage3D", f, Some(p))
1209            }
1210            IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1211                ("SignedPercentage4D", f, Some(p))
1212            }
1213            IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1214            IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1215        };
1216
1217        let frame_str = match frame {
1218            FrameChangeHandling::Absolute => "Absolute",
1219            FrameChangeHandling::Incremental => "Incremental",
1220        };
1221
1222        let positioning_str = positioning.map(|p| match p {
1223            PercentageNeuronPositioning::Linear => "Linear",
1224            PercentageNeuronPositioning::Fractional => "Fractional",
1225        });
1226
1227        json!({
1228            "variant": variant,
1229            "frame_change_handling": frame_str,
1230            "percentage_positioning": positioning_str,
1231            "config_value": dt.to_data_type_configuration_flag()
1232        })
1233    };
1234
1235    // Add motor types
1236    for motor_unit in MotorCorticalUnit::list_all() {
1237        let friendly_name = motor_unit.get_friendly_name();
1238        let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1239        let num_areas = motor_unit.get_number_cortical_areas();
1240        let topology = motor_unit.get_unit_default_topology();
1241
1242        // BREAKING CHANGE (unreleased API):
1243        // - Remove unit-level `supported_data_types`.
1244        // - Expose per-subunit metadata, because some units (e.g. Gaze) have heterogeneous subunits
1245        //   with different IOCorticalAreaConfigurationFlag variants (Percentage2D vs Percentage).
1246        //
1247        // We derive supported types by:
1248        // - generating canonical cortical IDs from the MotorCorticalUnit template for each
1249        //   (frame_change_handling, percentage_neuron_positioning) combination
1250        // - extracting the IO configuration flag from each cortical ID
1251        // - grouping supported_data_types per subunit index
1252        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1253        use serde_json::{Map, Value};
1254        use std::collections::HashMap as StdHashMap;
1255
1256        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1257
1258        // Initialize subunits with topology-derived properties.
1259        for (sub_idx, topo) in topology {
1260            subunits.insert(
1261                sub_idx.get().to_string(),
1262                json!({
1263                    "relative_position": topo.relative_position,
1264                    "channel_dimensions_default": topo.channel_dimensions_default,
1265                    "channel_dimensions_min": topo.channel_dimensions_min,
1266                    "channel_dimensions_max": topo.channel_dimensions_max,
1267                    "supported_data_types": Vec::<serde_json::Value>::new(),
1268                }),
1269            );
1270        }
1271
1272        // Build per-subunit supported_data_types (deduped).
1273        let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1274        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1275            Some(allowed) => allowed.to_vec(),
1276            None => vec![
1277                FrameChangeHandling::Absolute,
1278                FrameChangeHandling::Incremental,
1279            ],
1280        };
1281
1282        let positionings = [
1283            PercentageNeuronPositioning::Linear,
1284            PercentageNeuronPositioning::Fractional,
1285        ];
1286
1287        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1288            StdHashMap::new();
1289
1290        for frame in frames {
1291            for positioning in positionings {
1292                let mut map: Map<String, Value> = Map::new();
1293                map.insert(
1294                    "frame_change_handling".to_string(),
1295                    serde_json::to_value(frame).unwrap_or(Value::Null),
1296                );
1297                map.insert(
1298                    "percentage_neuron_positioning".to_string(),
1299                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1300                );
1301
1302                // Use unit index 0 for template enumeration (index does not affect IO flags).
1303                let cortical_ids = motor_unit
1304                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1305                        CorticalUnitIndex::from(0u8),
1306                        map,
1307                    );
1308
1309                if let Ok(ids) = cortical_ids {
1310                    for (i, id) in ids.into_iter().enumerate() {
1311                        if let Ok(flag) = id.extract_io_data_flag() {
1312                            let dt_json = data_type_to_json(flag);
1313                            let subunit_key = i.to_string();
1314
1315                            let dedup_key = format!(
1316                                "{}|{}|{}",
1317                                dt_json
1318                                    .get("variant")
1319                                    .and_then(|v| v.as_str())
1320                                    .unwrap_or(""),
1321                                dt_json
1322                                    .get("frame_change_handling")
1323                                    .and_then(|v| v.as_str())
1324                                    .unwrap_or(""),
1325                                dt_json
1326                                    .get("percentage_positioning")
1327                                    .and_then(|v| v.as_str())
1328                                    .unwrap_or("")
1329                            );
1330
1331                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1332                            if !seen.insert(dedup_key) {
1333                                continue;
1334                            }
1335
1336                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1337                                if let Some(arr) = subunit_obj
1338                                    .get_mut("supported_data_types")
1339                                    .and_then(|v| v.as_array_mut())
1340                                {
1341                                    arr.push(dt_json);
1342                                }
1343                            }
1344                        }
1345                    }
1346                }
1347            }
1348        }
1349
1350        templates.insert(
1351            format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1352            json!({
1353                "type": "motor",
1354                "friendly_name": friendly_name,
1355                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1356                "number_of_cortical_areas": num_areas,
1357                "subunits": subunits,
1358                "description": format!("Motor output: {}", friendly_name)
1359            }),
1360        );
1361    }
1362
1363    // Add sensory types
1364    for sensory_unit in SensoryCorticalUnit::list_all() {
1365        let friendly_name = sensory_unit.get_friendly_name();
1366        let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1367        let num_areas = sensory_unit.get_number_cortical_areas();
1368        let topology = sensory_unit.get_unit_default_topology();
1369
1370        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1371        use serde_json::{Map, Value};
1372        use std::collections::HashMap as StdHashMap;
1373
1374        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1375
1376        for (sub_idx, topo) in topology {
1377            subunits.insert(
1378                sub_idx.get().to_string(),
1379                json!({
1380                    "relative_position": topo.relative_position,
1381                    "channel_dimensions_default": topo.channel_dimensions_default,
1382                    "channel_dimensions_min": topo.channel_dimensions_min,
1383                    "channel_dimensions_max": topo.channel_dimensions_max,
1384                    "supported_data_types": Vec::<serde_json::Value>::new(),
1385                }),
1386            );
1387        }
1388
1389        let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1390        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1391            Some(allowed) => allowed.to_vec(),
1392            None => vec![
1393                FrameChangeHandling::Absolute,
1394                FrameChangeHandling::Incremental,
1395            ],
1396        };
1397
1398        let positionings = [
1399            PercentageNeuronPositioning::Linear,
1400            PercentageNeuronPositioning::Fractional,
1401        ];
1402
1403        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1404            StdHashMap::new();
1405
1406        for frame in frames {
1407            for positioning in positionings {
1408                let mut map: Map<String, Value> = Map::new();
1409                map.insert(
1410                    "frame_change_handling".to_string(),
1411                    serde_json::to_value(frame).unwrap_or(Value::Null),
1412                );
1413                map.insert(
1414                    "percentage_neuron_positioning".to_string(),
1415                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1416                );
1417
1418                let cortical_ids = sensory_unit
1419                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1420                        CorticalUnitIndex::from(0u8),
1421                        map,
1422                    );
1423
1424                if let Ok(ids) = cortical_ids {
1425                    for (i, id) in ids.into_iter().enumerate() {
1426                        if let Ok(flag) = id.extract_io_data_flag() {
1427                            let dt_json = data_type_to_json(flag);
1428                            let subunit_key = i.to_string();
1429
1430                            let dedup_key = format!(
1431                                "{}|{}|{}",
1432                                dt_json
1433                                    .get("variant")
1434                                    .and_then(|v| v.as_str())
1435                                    .unwrap_or(""),
1436                                dt_json
1437                                    .get("frame_change_handling")
1438                                    .and_then(|v| v.as_str())
1439                                    .unwrap_or(""),
1440                                dt_json
1441                                    .get("percentage_positioning")
1442                                    .and_then(|v| v.as_str())
1443                                    .unwrap_or("")
1444                            );
1445
1446                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1447                            if !seen.insert(dedup_key) {
1448                                continue;
1449                            }
1450
1451                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1452                                if let Some(arr) = subunit_obj
1453                                    .get_mut("supported_data_types")
1454                                    .and_then(|v| v.as_array_mut())
1455                                {
1456                                    arr.push(dt_json);
1457                                }
1458                            }
1459                        }
1460                    }
1461                }
1462            }
1463        }
1464
1465        templates.insert(
1466            format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1467            json!({
1468                "type": "sensory",
1469                "friendly_name": friendly_name,
1470                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1471                "number_of_cortical_areas": num_areas,
1472                "subunits": subunits,
1473                "description": format!("Sensory input: {}", friendly_name)
1474            }),
1475        );
1476    }
1477
1478    Ok(Json(templates))
1479}
1480
1481/// Get list of available embedded default genome templates (barebones, essential, test, vision).
1482#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1483pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1484    Ok(Json(vec![
1485        "barebones".to_string(),
1486        "essential".to_string(),
1487        "test".to_string(),
1488        "vision".to_string(),
1489    ]))
1490}
1491
1492/// Download a specific brain region from the genome.
1493#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
1494pub async fn get_download_region(
1495    State(_state): State<ApiState>,
1496    Query(_params): Query<HashMap<String, String>>,
1497) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1498    Ok(Json(HashMap::new()))
1499}
1500
1501/// Get the current genome number or generation identifier.
1502#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
1503pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
1504    Ok(Json(0))
1505}
1506
1507/// Perform genome amalgamation by specifying a filename.
1508#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
1509pub async fn post_amalgamation_by_filename(
1510    State(state): State<ApiState>,
1511    Json(req): Json<HashMap<String, String>>,
1512) -> ApiResult<Json<HashMap<String, String>>> {
1513    // Deterministic implementation:
1514    // - Supports embedded Rust template genomes by name (no filesystem I/O).
1515    // - For all other filenames, require /amalgamation_by_payload.
1516    let file_name = req
1517        .get("file_name")
1518        .or_else(|| req.get("filename"))
1519        .or_else(|| req.get("genome_file_name"))
1520        .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
1521
1522    let genome_json = match file_name.as_str() {
1523        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
1524        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
1525        "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
1526        "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
1527        other => {
1528            return Err(ApiError::invalid_input(format!(
1529                "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
1530                other
1531            )))
1532        }
1533    };
1534
1535    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
1536
1537    Ok(Json(HashMap::from([
1538        ("message".to_string(), "Amalgamation queued".to_string()),
1539        ("amalgamation_id".to_string(), amalgamation_id),
1540    ])))
1541}
1542
1543/// Perform genome amalgamation using a direct JSON payload.
1544#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
1545pub async fn post_amalgamation_by_payload(
1546    State(state): State<ApiState>,
1547    Json(req): Json<serde_json::Value>,
1548) -> ApiResult<Json<HashMap<String, String>>> {
1549    let json_str = serde_json::to_string(&req)
1550        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1551    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1552
1553    Ok(Json(HashMap::from([
1554        ("message".to_string(), "Amalgamation queued".to_string()),
1555        ("amalgamation_id".to_string(), amalgamation_id),
1556    ])))
1557}
1558
1559/// Perform genome amalgamation by uploading a genome file.
1560#[cfg(feature = "http")]
1561#[utoipa::path(
1562    post,
1563    path = "/v1/genome/amalgamation_by_upload",
1564    tag = "genome",
1565    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1566    responses(
1567        (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
1568        (status = 400, description = "Invalid request"),
1569        (status = 500, description = "Internal server error")
1570    )
1571)]
1572pub async fn post_amalgamation_by_upload(
1573    State(state): State<ApiState>,
1574    mut multipart: Multipart,
1575) -> ApiResult<Json<HashMap<String, String>>> {
1576    let mut genome_json: Option<String> = None;
1577
1578    while let Some(field) = multipart
1579        .next_field()
1580        .await
1581        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1582    {
1583        if field.name() == Some("file") {
1584            let bytes = field.bytes().await.map_err(|e| {
1585                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1586            })?;
1587
1588            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1589                ApiError::invalid_input(format!(
1590                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1591                    e
1592                ))
1593            })?;
1594            genome_json = Some(json_str.to_string());
1595            break;
1596        }
1597    }
1598
1599    let json_str =
1600        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1601    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1602
1603    Ok(Json(HashMap::from([
1604        ("message".to_string(), "Amalgamation queued".to_string()),
1605        ("amalgamation_id".to_string(), amalgamation_id),
1606    ])))
1607}
1608
1609/// Append structures to the genome from a file.
1610#[cfg(feature = "http")]
1611#[utoipa::path(
1612    post,
1613    path = "/v1/genome/append-file",
1614    tag = "genome",
1615    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1616    responses(
1617        (status = 200, description = "Append processed", body = HashMap<String, String>)
1618    )
1619)]
1620pub async fn post_append_file(
1621    State(_state): State<ApiState>,
1622    mut _multipart: Multipart,
1623) -> ApiResult<Json<HashMap<String, String>>> {
1624    Ok(Json(HashMap::from([(
1625        "message".to_string(),
1626        "Not yet implemented".to_string(),
1627    )])))
1628}
1629
1630/// Upload and load a genome from a file.
1631#[cfg(feature = "http")]
1632#[utoipa::path(
1633    post,
1634    path = "/v1/genome/upload/file",
1635    tag = "genome",
1636    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1637    responses(
1638        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
1639        (status = 400, description = "Invalid request"),
1640        (status = 500, description = "Internal server error")
1641    )
1642)]
1643pub async fn post_upload_file(
1644    State(state): State<ApiState>,
1645    mut multipart: Multipart,
1646) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1647    let mut genome_json: Option<String> = None;
1648
1649    while let Some(field) = multipart
1650        .next_field()
1651        .await
1652        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1653    {
1654        if field.name() == Some("file") {
1655            let bytes = field.bytes().await.map_err(|e| {
1656                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1657            })?;
1658
1659            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1660                ApiError::invalid_input(format!(
1661                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1662                    e
1663                ))
1664            })?;
1665            genome_json = Some(json_str.to_string());
1666            break;
1667        }
1668    }
1669
1670    let json_str =
1671        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1672
1673    let genome_info =
1674        load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
1675            .await?;
1676
1677    let mut response = HashMap::new();
1678    response.insert("success".to_string(), serde_json::json!(true));
1679    response.insert(
1680        "message".to_string(),
1681        serde_json::json!("Genome uploaded successfully"),
1682    );
1683    response.insert(
1684        "cortical_area_count".to_string(),
1685        serde_json::json!(genome_info.cortical_area_count),
1686    );
1687    response.insert(
1688        "brain_region_count".to_string(),
1689        serde_json::json!(genome_info.brain_region_count),
1690    );
1691
1692    Ok(Json(response))
1693}
1694
1695/// Upload a genome file with edit mode enabled.
1696#[cfg(feature = "http")]
1697#[utoipa::path(
1698    post,
1699    path = "/v1/genome/upload/file/edit",
1700    tag = "genome",
1701    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1702    responses(
1703        (status = 200, description = "Upload processed", body = HashMap<String, String>)
1704    )
1705)]
1706pub async fn post_upload_file_edit(
1707    State(_state): State<ApiState>,
1708    mut _multipart: Multipart,
1709) -> ApiResult<Json<HashMap<String, String>>> {
1710    Ok(Json(HashMap::from([(
1711        "message".to_string(),
1712        "Not yet implemented".to_string(),
1713    )])))
1714}
1715
1716/// Upload and load a genome from a JSON string.
1717#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
1718pub async fn post_upload_string(
1719    State(_state): State<ApiState>,
1720    Json(_req): Json<String>,
1721) -> ApiResult<Json<HashMap<String, String>>> {
1722    Ok(Json(HashMap::from([(
1723        "message".to_string(),
1724        "Not yet implemented".to_string(),
1725    )])))
1726}