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