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/// Mirrors prioritized genome transition in [feagi_state_manager::GenomeState] for health_check.
102/// - [Self::enter][]: Loading
103/// - [Self::succeed][]: Loaded (call when transition fully finished, including post-load agent IO)
104/// - Drop without succeed: Error (failed or aborted transition)
105struct GenomeTransitionStateLifecycle;
106
107impl GenomeTransitionStateLifecycle {
108    fn enter() -> Self {
109        #[cfg(feature = "services")]
110        {
111            feagi_state_manager::StateManager::instance()
112                .read()
113                .set_genome_state(feagi_state_manager::GenomeState::Loading);
114        }
115        Self
116    }
117
118    fn succeed(self) {
119        #[cfg(feature = "services")]
120        {
121            feagi_state_manager::StateManager::instance()
122                .read()
123                .set_genome_state(feagi_state_manager::GenomeState::Loaded);
124        }
125        std::mem::forget(self);
126    }
127}
128
129impl Drop for GenomeTransitionStateLifecycle {
130    fn drop(&mut self) {
131        #[cfg(feature = "services")]
132        {
133            feagi_state_manager::StateManager::instance()
134                .read()
135                .set_genome_state(feagi_state_manager::GenomeState::Error);
136        }
137    }
138}
139
140/// Execute a genome load with strict priority over concurrent operations.
141///
142/// Guarantees:
143/// - Only one genome transition may run at a time.
144/// - Runtime is quiesced before load starts.
145/// - Runtime frequency is updated from genome physiology.
146/// - Runtime is restored to running state if it was running before transition.
147async fn load_genome_with_priority(
148    state: &ApiState,
149    params: LoadGenomeParams,
150    source: &str,
151) -> ApiResult<GenomeInfo> {
152    let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
153        ApiError::conflict(
154            "Another genome transition is already in progress; wait for it to finish",
155        )
156    })?;
157    state
158        .genome_transition_in_progress
159        .store(true, Ordering::SeqCst);
160    let _guard = GenomeTransitionFlagGuard {
161        in_progress: Arc::clone(&state.genome_transition_in_progress),
162    };
163    let genome_sm_lifecycle = GenomeTransitionStateLifecycle::enter();
164
165    tracing::info!(
166        target: "feagi-api",
167        "🛑 Entering prioritized genome transition from {}",
168        source
169    );
170
171    let runtime_service = state.runtime_service.as_ref();
172    #[cfg(feature = "feagi-agent")]
173    if let Some(handler) = &state.agent_handler {
174        let deregistered_ids = {
175            let mut guard = handler.lock().unwrap();
176            guard.force_deregister_all_agents("forced by genome transition")
177        };
178        for agent_id in &deregistered_ids {
179            runtime_service.unregister_motor_subscriptions(agent_id);
180            runtime_service.unregister_visualization_subscriptions(agent_id);
181        }
182        tracing::info!(
183            target: "feagi-api",
184            "🔌 Forced deregistration for {} agents before genome transition",
185            deregistered_ids.len()
186        );
187    }
188    // Strict transition barrier: guarantee no stale subscriptions survive.
189    runtime_service.clear_all_motor_subscriptions();
190    runtime_service.clear_all_visualization_subscriptions();
191
192    let runtime_status = runtime_service
193        .get_status()
194        .await
195        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
196    let runtime_was_running = runtime_status.is_running;
197
198    if runtime_was_running {
199        tracing::info!(
200            target: "feagi-api",
201            "Stopping burst engine before prioritized genome transition"
202        );
203        runtime_service.stop().await.map_err(|e| {
204            ApiError::internal(format!(
205                "Failed to stop burst engine before genome transition: {}",
206                e
207            ))
208        })?;
209    }
210
211    let genome_service = state.genome_service.as_ref();
212    let load_result = genome_service.load_genome(params).await;
213    let genome_info = match load_result {
214        Ok(info) => info,
215        Err(e) => {
216            if runtime_was_running {
217                if let Err(restart_err) = runtime_service.start().await {
218                    tracing::warn!(
219                        target: "feagi-api",
220                        "Failed to restore runtime after failed genome load (source={}): {}",
221                        source,
222                        restart_err
223                    );
224                }
225            }
226            return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
227        }
228    };
229
230    let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
231    runtime_service
232        .set_frequency(burst_frequency_hz)
233        .await
234        .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
235
236    if runtime_was_running {
237        runtime_service.start().await.map_err(|e| {
238            ApiError::internal(format!(
239                "Failed to restart burst engine after genome transition: {}",
240                e
241            ))
242        })?;
243    }
244
245    tracing::info!(
246        target: "feagi-api",
247        "✅ Prioritized genome transition completed from {}",
248        source
249    );
250
251    // Deterministic: create missing IO areas for any registered agents immediately after genome load.
252    // Fixes nondeterministic behavior where areas were missing on first run but appeared on restart.
253    #[cfg(feature = "feagi-agent")]
254    if let Some(handler) = &state.agent_handler {
255        let device_regs_list: Vec<serde_json::Value> = {
256            let guard = handler.lock().unwrap();
257            guard
258                .get_all_registered_agents()
259                .iter()
260                .filter_map(|(sid, _)| guard.get_device_registrations_by_agent(*sid).cloned())
261                .collect()
262        };
263        for device_regs in device_regs_list {
264            crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations(
265                state,
266                &device_regs,
267            )
268            .await;
269        }
270    }
271
272    genome_sm_lifecycle.succeed();
273    Ok(genome_info)
274}
275
276/// Inject the current runtime simulation timestep (seconds) into a genome JSON value.
277///
278/// Rationale: the burst engine timestep can be updated at runtime, but `GenomeService::save_genome()`
279/// serializes the stored `RuntimeGenome` (which may still have the older physiology value).
280/// This keeps exported/saved genomes consistent with the *current* FEAGI simulation state.
281fn inject_simulation_timestep_into_genome(
282    mut genome: serde_json::Value,
283    simulation_timestep_s: f64,
284) -> Result<serde_json::Value, ApiError> {
285    let physiology = genome
286        .get_mut("physiology")
287        .and_then(|v| v.as_object_mut())
288        .ok_or_else(|| {
289            ApiError::internal(
290                "Genome JSON missing required object key 'physiology' while saving".to_string(),
291            )
292        })?;
293
294    physiology.insert(
295        "simulation_timestep".to_string(),
296        serde_json::Value::from(simulation_timestep_s),
297    );
298    Ok(genome)
299}
300
301async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
302    let runtime_service = state.runtime_service.as_ref();
303    let status = runtime_service
304        .get_status()
305        .await
306        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
307
308    // Convert frequency (Hz) to timestep (seconds).
309    Ok(if status.frequency_hz > 0.0 {
310        1.0 / status.frequency_hz
311    } else {
312        0.0
313    })
314}
315
316/// Get the current genome file name.
317#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
318pub async fn get_file_name(
319    State(_state): State<ApiState>,
320) -> ApiResult<Json<HashMap<String, String>>> {
321    // TODO: Get current genome filename
322    Ok(Json(HashMap::from([(
323        "genome_file_name".to_string(),
324        "".to_string(),
325    )])))
326}
327
328/// Get list of available circuit templates from the circuit library.
329#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
330pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
331    // TODO: Get available circuit library
332    Ok(Json(vec![]))
333}
334
335/// Set the destination for genome amalgamation (merging genomes).
336#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
337pub async fn post_amalgamation_destination(
338    State(state): State<ApiState>,
339    Query(params): Query<HashMap<String, String>>,
340    Json(req): Json<HashMap<String, serde_json::Value>>,
341) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
342    // BV sends query params:
343    // - circuit_origin_x/y/z
344    // - amalgamation_id
345    // - rewire_mode
346    //
347    // Body:
348    // - brain_region_id
349    let amalgamation_id = params
350        .get("amalgamation_id")
351        .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
352        .to_string();
353
354    let origin_x: i32 = params
355        .get("circuit_origin_x")
356        .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
357        .parse()
358        .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
359    let origin_y: i32 = params
360        .get("circuit_origin_y")
361        .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
362        .parse()
363        .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
364    let origin_z: i32 = params
365        .get("circuit_origin_z")
366        .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
367        .parse()
368        .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
369
370    let rewire_mode = params
371        .get("rewire_mode")
372        .cloned()
373        .unwrap_or_else(|| "rewire_all".to_string());
374
375    let parent_region_id = req
376        .get("brain_region_id")
377        .and_then(|v| v.as_str())
378        .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
379        .to_string();
380
381    // Resolve and consume the pending request.
382    let pending = {
383        let lock = state.amalgamation_state.write();
384        let Some(p) = lock.pending.as_ref() else {
385            return Err(ApiError::invalid_input("No amalgamation is pending"));
386        };
387        if p.summary.amalgamation_id != amalgamation_id {
388            return Err(ApiError::invalid_input(format!(
389                "Pending amalgamation_id mismatch: expected {}, got {}",
390                p.summary.amalgamation_id, amalgamation_id
391            )));
392        }
393        p.clone()
394    };
395
396    // 1) Create a new brain region to host the imported circuit.
397    // Note: ConnectomeServiceImpl shares the same RuntimeGenome Arc with GenomeServiceImpl, so
398    // persisting the region into the RuntimeGenome is required for subsequent cortical-area creation.
399    let connectome_service = state.connectome_service.as_ref();
400
401    let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
402    region_properties.insert(
403        "coordinate_3d".to_string(),
404        serde_json::json!([origin_x, origin_y, origin_z]),
405    );
406    region_properties.insert(
407        "amalgamation_id".to_string(),
408        serde_json::json!(pending.summary.amalgamation_id),
409    );
410    region_properties.insert(
411        "circuit_size".to_string(),
412        serde_json::json!(pending.summary.circuit_size),
413    );
414    region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
415
416    connectome_service
417        .create_brain_region(feagi_services::types::CreateBrainRegionParams {
418            region_id: amalgamation_id.clone(),
419            name: pending.summary.genome_title.clone(),
420            region_type: "Custom".to_string(),
421            parent_id: Some(parent_region_id.clone()),
422            properties: Some(region_properties),
423        })
424        .await
425        .map_err(|e| {
426            ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
427        })?;
428
429    // 2) Import cortical areas into that region.
430    //
431    // - Guest **Custom** and **Memory** cortical IDs are remapped to fresh IDs that do not
432    //   collide with the host (or with each other), and `cortical_mapping_dst` keys / brain
433    //   region membership are updated accordingly. This preserves the full guest circuit instead
434    //   of skipping shared template custom areas.
435    // - **Core** (`___death`, `___power`, `___fatig`) and **IPU/OPU** IDs are left canonical;
436    //   they are not duplicated if they already exist on the host (`skipped_existing_areas`).
437    // - We place areas at an offset relative to the chosen origin and set `parent_region_id`.
438    let mut imported_genome = feagi_evolutionary::load_genome_from_json(&pending.genome_json)
439        .map_err(|e| {
440            ApiError::invalid_input(format!(
441                "Pending genome payload can no longer be parsed as a genome: {}",
442                e
443            ))
444        })?;
445
446    let host_cortical_ids: std::collections::HashSet<String> = connectome_service
447        .get_cortical_area_ids()
448        .await
449        .map_err(|e| ApiError::internal(format!("Failed to list cortical area IDs: {}", e)))?
450        .into_iter()
451        .collect();
452
453    let remapped_guest_custom_memory_ids =
454        feagi_evolutionary::remap_guest_custom_memory_cortical_ids_for_amalgamation(
455            &mut imported_genome,
456            &host_cortical_ids,
457        )
458        .map_err(|e| {
459            ApiError::internal(format!(
460                "Amalgamation guest cortical ID remapping failed: {}",
461                e
462            ))
463        })?;
464    let guest_custom_memory_id_remap_count = remapped_guest_custom_memory_ids.len();
465
466    if guest_custom_memory_id_remap_count > 0 {
467        tracing::info!(
468            target: "feagi-api",
469            "🧬 [AMALGAMATION] Remapped {} guest Custom/Memory cortical IDs before import",
470            guest_custom_memory_id_remap_count
471        );
472    }
473
474    let genome_service = state.genome_service.as_ref();
475    let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
476    let mut skipped_existing: Vec<String> = Vec::new();
477
478    // Get root region ID for IPU/OPU areas
479    let root_region_id = connectome_service
480        .get_root_region_id()
481        .await
482        .map_err(|e| ApiError::internal(format!("Failed to get root region ID: {}", e)))?;
483
484    for area in imported_genome.cortical_areas.values() {
485        let cortical_id = area.cortical_id.as_base_64();
486        let exists = connectome_service
487            .cortical_area_exists(&cortical_id)
488            .await
489            .map_err(|e| {
490                ApiError::internal(format!(
491                    "Failed to check existing cortical area {}: {}",
492                    cortical_id, e
493                ))
494            })?;
495        if exists {
496            skipped_existing.push(cortical_id);
497            continue;
498        }
499
500        let mut props = area.properties.clone();
501
502        // Remove cortical_mapping_dst from properties - connections will be imported separately
503        props.remove("cortical_mapping_dst");
504
505        // Determine correct parent region based on area type
506        // IPU/OPU areas MUST go to root region, all others go to the amalgamation region
507        let area_type = area.cortical_id.as_cortical_type().map_err(|e| {
508            ApiError::internal(format!(
509                "Failed to get cortical area type for {}: {}",
510                cortical_id, e
511            ))
512        })?;
513
514        let target_parent_region_id = match area_type {
515            feagi_structures::genomic::cortical_area::CorticalAreaType::BrainInput(_)
516            | feagi_structures::genomic::cortical_area::CorticalAreaType::BrainOutput(_) => {
517                // IPU/OPU areas go to root region
518                match root_region_id.as_ref() {
519                    Some(root_id) => {
520                        tracing::info!(
521                            target: "feagi-api",
522                            "🧬 [AMALGAMATION] IPU/OPU area {} will be placed in root region {}",
523                            cortical_id,
524                            root_id
525                        );
526                        root_id.clone()
527                    }
528                    None => {
529                        tracing::warn!(
530                            target: "feagi-api",
531                            "🧬 [AMALGAMATION] No root region found for IPU/OPU area {}, using amalgamation region",
532                            cortical_id
533                        );
534                        amalgamation_id.clone()
535                    }
536                }
537            }
538            _ => {
539                // Custom, Memory, Core areas go to amalgamation region
540                amalgamation_id.clone()
541            }
542        };
543
544        props.insert(
545            "parent_region_id".to_string(),
546            serde_json::json!(target_parent_region_id),
547        );
548        props.insert(
549            "amalgamation_source".to_string(),
550            serde_json::json!("amalgamation_by_payload"),
551        );
552
553        to_create.push(feagi_services::types::CreateCorticalAreaParams {
554            cortical_id,
555            name: area.name.clone(),
556            dimensions: (
557                area.dimensions.width as usize,
558                area.dimensions.height as usize,
559                area.dimensions.depth as usize,
560            ),
561            position: (
562                origin_x.saturating_add(area.position.x),
563                origin_y.saturating_add(area.position.y),
564                origin_z.saturating_add(area.position.z),
565            ),
566            area_type: "Custom".to_string(),
567            visible: Some(true),
568            sub_group: None,
569            neurons_per_voxel: area
570                .properties
571                .get("neurons_per_voxel")
572                .and_then(|v| v.as_u64())
573                .map(|v| v as u32),
574            postsynaptic_current: area
575                .properties
576                .get("postsynaptic_current")
577                .and_then(|v| v.as_f64()),
578            plasticity_constant: area
579                .properties
580                .get("plasticity_constant")
581                .and_then(|v| v.as_f64()),
582            degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
583            psp_uniform_distribution: area
584                .properties
585                .get("psp_uniform_distribution")
586                .and_then(|v| v.as_bool()),
587            firing_threshold_increment: None,
588            firing_threshold_limit: area
589                .properties
590                .get("firing_threshold_limit")
591                .and_then(|v| v.as_f64()),
592            consecutive_fire_count: area
593                .properties
594                .get("consecutive_fire_limit")
595                .and_then(|v| v.as_u64())
596                .map(|v| v as u32),
597            snooze_period: area
598                .properties
599                .get("snooze_period")
600                .and_then(|v| v.as_u64())
601                .map(|v| v as u32),
602            refractory_period: area
603                .properties
604                .get("refractory_period")
605                .and_then(|v| v.as_u64())
606                .map(|v| v as u32),
607            leak_coefficient: area
608                .properties
609                .get("leak_coefficient")
610                .and_then(|v| v.as_f64()),
611            leak_variability: area
612                .properties
613                .get("leak_variability")
614                .and_then(|v| v.as_f64()),
615            burst_engine_active: area
616                .properties
617                .get("burst_engine_active")
618                .and_then(|v| v.as_bool()),
619            properties: Some(props),
620        });
621    }
622
623    let imported_new_area_count = to_create.len();
624    if !to_create.is_empty() {
625        genome_service
626            .create_cortical_areas(to_create)
627            .await
628            .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
629    }
630
631    // 3) Import morphologies used by the imported areas' cortical mappings.
632    //
633    // Collect all morphology IDs referenced in the cortical_mapping_dst of imported areas,
634    // then import those morphologies from the imported genome into the current genome.
635    let imported_area_ids: std::collections::HashSet<String> = imported_genome
636        .cortical_areas
637        .keys()
638        .map(|id| id.as_base_64())
639        .filter(|id| !skipped_existing.contains(id))
640        .collect();
641
642    let mut required_morphologies: std::collections::HashSet<String> =
643        std::collections::HashSet::new();
644
645    // Scan imported areas' mappings to collect required morphology IDs
646    for area in imported_genome.cortical_areas.values() {
647        if !imported_area_ids.contains(&area.cortical_id.as_base_64()) {
648            continue;
649        }
650
651        let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
652            continue;
653        };
654        let Some(dst_map) = cortical_mapping_dst.as_object() else {
655            continue;
656        };
657
658        for mapping_data in dst_map.values() {
659            let Some(mapping_array) = mapping_data.as_array() else {
660                continue;
661            };
662
663            for rule in mapping_array {
664                // Extract morphology_id from rule (can be object or array format)
665                let morphology_id = if let Some(obj) = rule.as_object() {
666                    obj.get("morphology_id").and_then(|v| v.as_str())
667                } else if let Some(arr) = rule.as_array() {
668                    arr.first().and_then(|v| v.as_str())
669                } else {
670                    None
671                };
672
673                if let Some(morph_id) = morphology_id {
674                    required_morphologies.insert(morph_id.to_string());
675                }
676            }
677        }
678    }
679
680    // Import each required morphology if it doesn't already exist
681    let mut imported_morphology_count = 0;
682    let mut skipped_morphology_count = 0;
683
684    for morphology_id in &required_morphologies {
685        // Check if morphology already exists
686        let morphologies = connectome_service.get_morphologies().await.map_err(|e| {
687            ApiError::internal(format!("Failed to get existing morphologies: {}", e))
688        })?;
689
690        if morphologies.contains_key(morphology_id) {
691            skipped_morphology_count += 1;
692            continue;
693        }
694
695        // Get morphology from imported genome
696        let Some(morphology) = imported_genome.morphologies.get(morphology_id) else {
697            tracing::warn!(
698                target: "feagi-api",
699                "🧬 [AMALGAMATION] Morphology '{}' referenced in mappings but not found in imported genome",
700                morphology_id
701            );
702            continue;
703        };
704
705        // Import the morphology
706        match connectome_service
707            .create_morphology(morphology_id.clone(), morphology.clone())
708            .await
709        {
710            Ok(_) => {
711                tracing::debug!(
712                    target: "feagi-api",
713                    "🧬 [AMALGAMATION] Imported morphology '{}'",
714                    morphology_id
715                );
716                imported_morphology_count += 1;
717            }
718            Err(e) => {
719                tracing::warn!(
720                    target: "feagi-api",
721                    "🧬 [AMALGAMATION] Failed to import morphology '{}': {}",
722                    morphology_id,
723                    e
724                );
725            }
726        }
727    }
728
729    if imported_morphology_count > 0 {
730        tracing::info!(
731            target: "feagi-api",
732            "🧬 [AMALGAMATION] Imported {} morphologies (skipped {} existing)",
733            imported_morphology_count,
734            skipped_morphology_count
735        );
736    }
737
738    // 4) Import cortical mappings from the guest genome.
739    //
740    // **Source** must be a newly created area (in `imported_area_ids`). Sources that were skipped
741    // due to ID collision never have their outgoing mappings applied here.
742    // **Destination** must already exist in the connectome (newly created or pre-existing host area).
743    // So: new→new and new→host edges can be imported; skipped→* edges are not.
744
745    let mut imported_mapping_count = 0;
746    let mut skipped_mapping_count = 0;
747
748    for area in imported_genome.cortical_areas.values() {
749        let src_area_id = area.cortical_id.as_base_64();
750
751        // Skip if this area was not imported (already existed)
752        if !imported_area_ids.contains(&src_area_id) {
753            continue;
754        }
755
756        // Check if area has cortical_mapping_dst property
757        let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
758            continue;
759        };
760        let Some(dst_map) = cortical_mapping_dst.as_object() else {
761            continue;
762        };
763
764        // Import each mapping where destination exists in connectome
765        for (dst_area_id, mapping_data) in dst_map {
766            // Check if destination area exists in connectome (either newly imported or already existing)
767            let dst_exists = connectome_service
768                .cortical_area_exists(dst_area_id)
769                .await
770                .unwrap_or(false);
771
772            if !dst_exists {
773                // Skip external references to areas not in this brain
774                skipped_mapping_count += 1;
775                continue;
776            }
777
778            let Some(mapping_array) = mapping_data.as_array() else {
779                tracing::warn!(
780                    target: "feagi-api",
781                    "🧬 [AMALGAMATION] Invalid mapping data from {} to {}: not an array",
782                    src_area_id,
783                    dst_area_id
784                );
785                continue;
786            };
787
788            // Import the cortical mapping
789            match connectome_service
790                .update_cortical_mapping(
791                    src_area_id.clone(),
792                    dst_area_id.clone(),
793                    mapping_array.clone(),
794                )
795                .await
796            {
797                Ok(synapse_count) => {
798                    tracing::debug!(
799                        target: "feagi-api",
800                        "🧬 [AMALGAMATION] Imported mapping {} -> {} ({} synapses)",
801                        src_area_id,
802                        dst_area_id,
803                        synapse_count
804                    );
805                    imported_mapping_count += 1;
806                }
807                Err(e) => {
808                    tracing::warn!(
809                        target: "feagi-api",
810                        "🧬 [AMALGAMATION] Failed to import mapping {} -> {}: {}",
811                        src_area_id,
812                        dst_area_id,
813                        e
814                    );
815                    skipped_mapping_count += 1;
816                }
817            }
818        }
819    }
820
821    if imported_mapping_count > 0 {
822        tracing::info!(
823            target: "feagi-api",
824            "🧬 [AMALGAMATION] Successfully imported {} cortical mappings (skipped {} external/missing mappings)",
825            imported_mapping_count,
826            skipped_mapping_count
827        );
828    } else if skipped_mapping_count > 0 {
829        tracing::warn!(
830            target: "feagi-api",
831            "🧬 [AMALGAMATION] No internal mappings imported! Skipped {} mappings (all external or missing)",
832            skipped_mapping_count
833        );
834    } else {
835        // Neither imported nor skipped counts: usually empty/missing `cortical_mapping_dst` on
836        // **newly created** guest areas after parse (e.g. flat blueprint `dstmap-d` is `{}`).
837        let mut nonempty_dst_on_new = 0_usize;
838        let mut empty_dst_on_new = 0_usize;
839        let mut missing_dst_on_new = 0_usize;
840        for area in imported_genome.cortical_areas.values() {
841            let id = area.cortical_id.as_base_64();
842            if !imported_area_ids.contains(&id) {
843                continue;
844            }
845            match area.properties.get("cortical_mapping_dst") {
846                None => missing_dst_on_new += 1,
847                Some(v) => {
848                    if v.as_object().map(|o| o.is_empty()).unwrap_or(true) {
849                        empty_dst_on_new += 1;
850                    } else {
851                        nonempty_dst_on_new += 1;
852                    }
853                }
854            }
855        }
856        tracing::info!(
857            target: "feagi-api",
858            "🧬 [AMALGAMATION] No cortical mappings to import from guest (new areas: nonempty_dst={} empty_dst={} missing_dst={}; imported_new_areas={}; skipped_existing_areas={}). \
859             Areas skipped due to host ID collision do not replay guest wiring. \
860             For synapses on newly created areas, guest blueprint needs non-empty dstmap (cortical_mapping_dst) on those sources.",
861            nonempty_dst_on_new,
862            empty_dst_on_new,
863            missing_dst_on_new,
864            imported_new_area_count,
865            skipped_existing.len()
866        );
867    }
868
869    // 5) Invalidate all relevant health_check hashes to force BV cache refresh.
870    //
871    // Amalgamation modifies:
872    // - Brain regions (new region created)
873    // - Cortical areas (new areas added)
874    // - Brain geometry (positions of new areas)
875    // - Morphologies (new morphologies imported)
876    // - Cortical mappings (new connections created)
877    //
878    // Incrementing these hashes signals BV to refresh its cached data without requiring a restart.
879    {
880        let state_manager = feagi_state_manager::StateManager::instance();
881        let state_manager = state_manager.read();
882
883        // Increment each relevant hash (adding 1 invalidates client cache)
884        state_manager
885            .set_brain_regions_hash(state_manager.get_brain_regions_hash().wrapping_add(1));
886        state_manager
887            .set_cortical_areas_hash(state_manager.get_cortical_areas_hash().wrapping_add(1));
888        state_manager
889            .set_brain_geometry_hash(state_manager.get_brain_geometry_hash().wrapping_add(1));
890        if imported_morphology_count > 0 {
891            state_manager
892                .set_morphologies_hash(state_manager.get_morphologies_hash().wrapping_add(1));
893        }
894        if imported_mapping_count > 0 {
895            state_manager.set_cortical_mappings_hash(
896                state_manager.get_cortical_mappings_hash().wrapping_add(1),
897            );
898        }
899
900        tracing::info!(
901            target: "feagi-api",
902            "🧬 [AMALGAMATION] Invalidated health_check hashes for BV cache refresh"
903        );
904    }
905
906    // Clear pending + write history entry
907    {
908        let mut lock = state.amalgamation_state.write();
909        let now_ms = std::time::SystemTime::now()
910            .duration_since(std::time::UNIX_EPOCH)
911            .map(|d| d.as_millis() as i64)
912            .unwrap_or(0);
913        lock.history.push(amalgamation::AmalgamationHistoryEntry {
914            amalgamation_id: pending.summary.amalgamation_id.clone(),
915            genome_title: pending.summary.genome_title.clone(),
916            circuit_size: pending.summary.circuit_size,
917            status: "confirmed".to_string(),
918            timestamp_ms: now_ms,
919        });
920        lock.pending = None;
921    }
922
923    let guest_cortical_area_count = imported_genome.cortical_areas.len();
924
925    // Build a BV-compatible list response for brain regions (regions_members-like data, but as list).
926    let regions = state
927        .connectome_service
928        .list_brain_regions()
929        .await
930        .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
931
932    let post_merge_brain_region_count = regions.len();
933    let post_merge_cortical_area_total = state
934        .connectome_service
935        .get_cortical_area_ids()
936        .await
937        .map(|ids| ids.len())
938        .map_err(|e| {
939            ApiError::internal(format!(
940                "Failed to count cortical areas after amalgamation: {}",
941                e
942            ))
943        })?;
944
945    let mut brain_regions: Vec<serde_json::Value> = Vec::new();
946    for region in regions {
947        // Shape matches BV expectations in FEAGIRequests.gd
948        let coordinate_3d = region
949            .properties
950            .get("coordinate_3d")
951            .cloned()
952            .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
953        let coordinate_2d = region
954            .properties
955            .get("coordinate_2d")
956            .cloned()
957            .unwrap_or_else(|| serde_json::json!([0, 0]));
958
959        brain_regions.push(serde_json::json!({
960            "region_id": region.region_id,
961            "title": region.name,
962            "description": "",
963            "parent_region_id": region.parent_id,
964            "coordinate_2d": coordinate_2d,
965            "coordinate_3d": coordinate_3d,
966            "areas": region.cortical_areas,
967            "regions": region.child_regions,
968            "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
969            "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
970            "designated_inputs": region.properties.get("designated_inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
971            "designated_outputs": region.properties.get("designated_outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
972        }));
973    }
974
975    tracing::info!(
976        target: "feagi-api",
977        "🧬 [AMALGAMATION] Complete genome_title='{}' amalgamation_id={} \
978         guest_custom_memory_ids_remapped={} guest_cortical_areas={} new_cortical_areas_created={} cortical_areas_skipped_host_collision={} \
979         morphologies_imported={} morphologies_skipped_already_present={} \
980         cortical_mapping_rules_imported={} cortical_mapping_rules_skipped_unresolved_dst={} \
981         post_merge_brain_regions={} post_merge_cortical_areas_total={}",
982        pending.summary.genome_title,
983        pending.summary.amalgamation_id,
984        guest_custom_memory_id_remap_count,
985        guest_cortical_area_count,
986        imported_new_area_count,
987        skipped_existing.len(),
988        imported_morphology_count,
989        skipped_morphology_count,
990        imported_mapping_count,
991        skipped_mapping_count,
992        post_merge_brain_region_count,
993        post_merge_cortical_area_total,
994    );
995
996    Ok(Json(HashMap::from([
997        (
998            "message".to_string(),
999            serde_json::Value::String("Amalgamation confirmed".to_string()),
1000        ),
1001        (
1002            "brain_regions".to_string(),
1003            serde_json::Value::Array(brain_regions),
1004        ),
1005        (
1006            "skipped_existing_areas".to_string(),
1007            serde_json::json!(skipped_existing),
1008        ),
1009        (
1010            "imported_new_area_count".to_string(),
1011            serde_json::json!(imported_new_area_count),
1012        ),
1013        (
1014            "guest_cortical_area_count".to_string(),
1015            serde_json::json!(guest_cortical_area_count),
1016        ),
1017        (
1018            "guest_custom_memory_id_remap_count".to_string(),
1019            serde_json::json!(guest_custom_memory_id_remap_count),
1020        ),
1021        (
1022            "imported_cortical_mappings".to_string(),
1023            serde_json::json!(imported_mapping_count),
1024        ),
1025        (
1026            "skipped_cortical_mappings".to_string(),
1027            serde_json::json!(skipped_mapping_count),
1028        ),
1029        (
1030            "imported_morphology_count".to_string(),
1031            serde_json::json!(imported_morphology_count),
1032        ),
1033        (
1034            "skipped_morphology_existing_count".to_string(),
1035            serde_json::json!(skipped_morphology_count),
1036        ),
1037        (
1038            "post_merge_brain_region_count".to_string(),
1039            serde_json::json!(post_merge_brain_region_count),
1040        ),
1041        (
1042            "post_merge_cortical_area_total".to_string(),
1043            serde_json::json!(post_merge_cortical_area_total),
1044        ),
1045    ])))
1046}
1047
1048/// Cancel a pending genome amalgamation operation.
1049#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
1050pub async fn delete_amalgamation_cancellation(
1051    State(state): State<ApiState>,
1052) -> ApiResult<Json<HashMap<String, String>>> {
1053    let mut lock = state.amalgamation_state.write();
1054    if let Some(pending) = lock.pending.take() {
1055        let now_ms = std::time::SystemTime::now()
1056            .duration_since(std::time::UNIX_EPOCH)
1057            .map(|d| d.as_millis() as i64)
1058            .unwrap_or(0);
1059        lock.history.push(amalgamation::AmalgamationHistoryEntry {
1060            amalgamation_id: pending.summary.amalgamation_id,
1061            genome_title: pending.summary.genome_title,
1062            circuit_size: pending.summary.circuit_size,
1063            status: "cancelled".to_string(),
1064            timestamp_ms: now_ms,
1065        });
1066
1067        tracing::info!(
1068            target: "feagi-api",
1069            "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
1070            lock.history
1071                .last()
1072                .map(|e| e.amalgamation_id.clone())
1073                .unwrap_or_else(|| "<unknown>".to_string())
1074        );
1075    }
1076    Ok(Json(HashMap::from([(
1077        "message".to_string(),
1078        "Amalgamation cancelled".to_string(),
1079    )])))
1080}
1081
1082/// Append additional structures to the current genome.
1083#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
1084pub async fn post_genome_append(
1085    State(_state): State<ApiState>,
1086    Json(_req): Json<HashMap<String, serde_json::Value>>,
1087) -> ApiResult<Json<HashMap<String, String>>> {
1088    Err(ApiError::internal("Not yet implemented"))
1089}
1090
1091/// Load the minimal barebones genome with only essential neural structures.
1092#[utoipa::path(
1093    post,
1094    path = "/v1/genome/upload/barebones",
1095    responses(
1096        (status = 200, description = "Barebones genome loaded successfully"),
1097        (status = 500, description = "Failed to load genome")
1098    ),
1099    tag = "genome"
1100)]
1101pub async fn post_upload_barebones_genome(
1102    State(state): State<ApiState>,
1103) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1104    tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
1105    let result = load_default_genome(state, "barebones").await;
1106    match &result {
1107        Ok(_) => {
1108            tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
1109        }
1110        Err(e) => {
1111            tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
1112        }
1113    }
1114    result
1115}
1116
1117/// Load the essential genome with core sensory and motor areas.
1118#[utoipa::path(
1119    post,
1120    path = "/v1/genome/upload/essential",
1121    responses(
1122        (status = 200, description = "Essential genome loaded successfully"),
1123        (status = 500, description = "Failed to load genome")
1124    ),
1125    tag = "genome"
1126)]
1127pub async fn post_upload_essential_genome(
1128    State(state): State<ApiState>,
1129) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1130    load_default_genome(state, "essential").await
1131}
1132
1133/// Helper function to load a default genome by name from embedded Rust genomes
1134async fn load_default_genome(
1135    state: ApiState,
1136    genome_name: &str,
1137) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1138    tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
1139    tracing::debug!(target: "feagi-api", "   State components available: genome_service=true, runtime_service=true");
1140    // Load genome from embedded Rust templates (no file I/O!)
1141    let genome_json = match genome_name {
1142        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
1143        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
1144        "test" => feagi_evolutionary::TEST_GENOME_JSON,
1145        "vision" => feagi_evolutionary::VISION_GENOME_JSON,
1146        _ => {
1147            return Err(ApiError::invalid_input(format!(
1148                "Unknown genome name '{}'. Available: barebones, essential, test, vision",
1149                genome_name
1150            )))
1151        }
1152    };
1153
1154    tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
1155                   genome_name, genome_json.len());
1156
1157    let params = LoadGenomeParams {
1158        json_str: genome_json.to_string(),
1159    };
1160
1161    tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
1162    let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
1163
1164    tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
1165               genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
1166
1167    // Return response matching Python format
1168    let mut response = HashMap::new();
1169    response.insert("success".to_string(), serde_json::Value::Bool(true));
1170    response.insert(
1171        "message".to_string(),
1172        serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
1173    );
1174    response.insert(
1175        "cortical_area_count".to_string(),
1176        serde_json::Value::Number(genome_info.cortical_area_count.into()),
1177    );
1178    response.insert(
1179        "brain_region_count".to_string(),
1180        serde_json::Value::Number(genome_info.brain_region_count.into()),
1181    );
1182    response.insert(
1183        "genome_id".to_string(),
1184        serde_json::Value::String(genome_info.genome_id),
1185    );
1186    response.insert(
1187        "genome_title".to_string(),
1188        serde_json::Value::String(genome_info.genome_title),
1189    );
1190
1191    Ok(Json(response))
1192}
1193
1194/// Get the current genome name.
1195#[utoipa::path(
1196    get,
1197    path = "/v1/genome/name",
1198    tag = "genome",
1199    responses(
1200        (status = 200, description = "Genome name", body = String)
1201    )
1202)]
1203pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
1204    // Get genome metadata to extract name
1205    // TODO: Implement proper genome name retrieval from genome service
1206    Ok(Json("default_genome".to_string()))
1207}
1208
1209/// Get the genome creation or modification timestamp.
1210#[utoipa::path(
1211    get,
1212    path = "/v1/genome/timestamp",
1213    tag = "genome",
1214    responses(
1215        (status = 200, description = "Genome timestamp", body = i64)
1216    )
1217)]
1218pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
1219    // TODO: Store and retrieve genome timestamp
1220    Ok(Json(0))
1221}
1222
1223/// Save the current genome to a file with optional ID and title parameters.
1224#[utoipa::path(
1225    post,
1226    path = "/v1/genome/save",
1227    tag = "genome",
1228    responses(
1229        (status = 200, description = "Genome saved", body = HashMap<String, String>)
1230    )
1231)]
1232pub async fn post_save(
1233    State(state): State<ApiState>,
1234    Json(request): Json<HashMap<String, String>>,
1235) -> ApiResult<Json<HashMap<String, String>>> {
1236    use std::fs;
1237    use std::path::Path;
1238
1239    info!("Saving genome to file");
1240
1241    // Get parameters
1242    let genome_id = request.get("genome_id").cloned();
1243    let genome_title = request.get("genome_title").cloned();
1244    let file_path = request.get("file_path").cloned();
1245
1246    // Create save parameters
1247    let params = feagi_services::SaveGenomeParams {
1248        genome_id,
1249        genome_title,
1250    };
1251
1252    // Call genome service to generate JSON
1253    let genome_service = state.genome_service.as_ref();
1254    let genome_json = genome_service
1255        .save_genome(params)
1256        .await
1257        .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
1258
1259    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at save time.
1260    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1261    let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
1262        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1263    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1264    let genome_json = serde_json::to_string_pretty(&genome_value)
1265        .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
1266
1267    // Determine file path
1268    let save_path = if let Some(path) = file_path {
1269        std::path::PathBuf::from(path)
1270    } else {
1271        // Default to hidden genome directory with timestamp.
1272        let timestamp = std::time::SystemTime::now()
1273            .duration_since(std::time::UNIX_EPOCH)
1274            .unwrap()
1275            .as_secs();
1276        std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
1277    };
1278
1279    // Ensure parent directory exists
1280    if let Some(parent) = Path::new(&save_path).parent() {
1281        fs::create_dir_all(parent)
1282            .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
1283    }
1284
1285    // Write to file
1286    fs::write(&save_path, genome_json)
1287        .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
1288
1289    info!("✅ Genome saved successfully to: {}", save_path.display());
1290
1291    Ok(Json(HashMap::from([
1292        (
1293            "message".to_string(),
1294            "Genome saved successfully".to_string(),
1295        ),
1296        ("file_path".to_string(), save_path.display().to_string()),
1297    ])))
1298}
1299
1300/// Load a genome from a file by name.
1301#[utoipa::path(
1302    post,
1303    path = "/v1/genome/load",
1304    tag = "genome",
1305    responses(
1306        (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
1307    )
1308)]
1309pub async fn post_load(
1310    State(state): State<ApiState>,
1311    Json(request): Json<HashMap<String, String>>,
1312) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1313    let genome_name = request
1314        .get("genome_name")
1315        .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
1316
1317    // Load genome from defaults
1318    let params = feagi_services::LoadGenomeParams {
1319        json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
1320    };
1321
1322    let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
1323
1324    let mut response = HashMap::new();
1325    response.insert(
1326        "message".to_string(),
1327        serde_json::json!("Genome loaded successfully"),
1328    );
1329    response.insert(
1330        "genome_title".to_string(),
1331        serde_json::json!(genome_info.genome_title),
1332    );
1333
1334    Ok(Json(response))
1335}
1336
1337/// Upload and load a genome from JSON payload.
1338#[utoipa::path(
1339    post,
1340    path = "/v1/genome/upload",
1341    tag = "genome",
1342    responses(
1343        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
1344    )
1345)]
1346pub async fn post_upload(
1347    State(state): State<ApiState>,
1348    Json(genome_json): Json<serde_json::Value>,
1349) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1350    // Convert to JSON string
1351    let json_str = serde_json::to_string(&genome_json)
1352        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1353
1354    let params = LoadGenomeParams { json_str };
1355    let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
1356
1357    let mut response = HashMap::new();
1358    response.insert("success".to_string(), serde_json::json!(true));
1359    response.insert(
1360        "message".to_string(),
1361        serde_json::json!("Genome uploaded successfully"),
1362    );
1363    response.insert(
1364        "cortical_area_count".to_string(),
1365        serde_json::json!(genome_info.cortical_area_count),
1366    );
1367    response.insert(
1368        "brain_region_count".to_string(),
1369        serde_json::json!(genome_info.brain_region_count),
1370    );
1371
1372    Ok(Json(response))
1373}
1374
1375/// Download the current genome as a JSON document.
1376#[utoipa::path(
1377    get,
1378    path = "/v1/genome/download",
1379    tag = "genome",
1380    responses(
1381        (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
1382    )
1383)]
1384pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
1385    info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
1386    let genome_service = state.genome_service.as_ref();
1387
1388    // Get genome as JSON string
1389    let genome_json_str = genome_service
1390        .save_genome(feagi_services::types::SaveGenomeParams {
1391            genome_id: None,
1392            genome_title: None,
1393        })
1394        .await
1395        .map_err(|e| {
1396            tracing::error!("Failed to export genome: {}", e);
1397            ApiError::internal(format!("Failed to export genome: {}", e))
1398        })?;
1399
1400    // Parse to Value for JSON response
1401    let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
1402        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1403
1404    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at download time.
1405    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1406    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1407
1408    info!(
1409        "✅ Genome download complete, {} bytes",
1410        genome_json_str.len()
1411    );
1412    Ok(Json(genome_value))
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use super::*;
1418    use serde_json::json;
1419
1420    #[test]
1421    fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
1422        let genome = json!({
1423            "version": "3.0",
1424            "physiology": {
1425                "simulation_timestep": 0.025,
1426                "max_age": 10000000
1427            }
1428        });
1429
1430        let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
1431        assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
1432        assert_eq!(updated["physiology"]["max_age"], json!(10000000));
1433    }
1434
1435    #[test]
1436    fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
1437        let genome = json!({ "version": "3.0" });
1438        let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
1439        assert!(format!("{err:?}").contains("physiology"));
1440    }
1441}
1442
1443/// Get genome properties including metadata, size, and configuration details.
1444#[utoipa::path(
1445    get,
1446    path = "/v1/genome/properties",
1447    tag = "genome",
1448    responses(
1449        (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
1450    )
1451)]
1452pub async fn get_properties(
1453    State(_state): State<ApiState>,
1454) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1455    // TODO: Implement proper metadata retrieval from genome service
1456    Ok(Json(HashMap::new()))
1457}
1458
1459/// Validate a genome structure for correctness and completeness.
1460#[utoipa::path(
1461    post,
1462    path = "/v1/genome/validate",
1463    tag = "genome",
1464    responses(
1465        (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1466    )
1467)]
1468pub async fn post_validate(
1469    State(_state): State<ApiState>,
1470    Json(_genome): Json<serde_json::Value>,
1471) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1472    // TODO: Implement genome validation
1473    let mut response = HashMap::new();
1474    response.insert("valid".to_string(), serde_json::json!(true));
1475    response.insert("errors".to_string(), serde_json::json!([]));
1476    response.insert("warnings".to_string(), serde_json::json!([]));
1477
1478    Ok(Json(response))
1479}
1480
1481/// Transform genome between different formats (flat to hierarchical or vice versa).
1482#[utoipa::path(
1483    post,
1484    path = "/v1/genome/transform",
1485    tag = "genome",
1486    responses(
1487        (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1488    )
1489)]
1490pub async fn post_transform(
1491    State(_state): State<ApiState>,
1492    Json(_request): Json<HashMap<String, serde_json::Value>>,
1493) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1494    // TODO: Implement genome transformation
1495    let mut response = HashMap::new();
1496    response.insert(
1497        "message".to_string(),
1498        serde_json::json!("Genome transformation not yet implemented"),
1499    );
1500
1501    Ok(Json(response))
1502}
1503
1504/// Clone the current genome with a new name, creating an independent copy.
1505#[utoipa::path(
1506    post,
1507    path = "/v1/genome/clone",
1508    tag = "genome",
1509    responses(
1510        (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1511    )
1512)]
1513pub async fn post_clone(
1514    State(_state): State<ApiState>,
1515    Json(_request): Json<HashMap<String, String>>,
1516) -> ApiResult<Json<HashMap<String, String>>> {
1517    // TODO: Implement genome cloning
1518    Ok(Json(HashMap::from([(
1519        "message".to_string(),
1520        "Genome cloning not yet implemented".to_string(),
1521    )])))
1522}
1523
1524/// Reset genome to its default state, clearing all cortical areas and brain regions.
1525/// Use before loading a new genome when "cortical area already exists" errors occur.
1526#[utoipa::path(
1527    post,
1528    path = "/v1/genome/reset",
1529    tag = "genome",
1530    responses(
1531        (status = 200, description = "Genome reset", body = HashMap<String, String>),
1532        (status = 409, description = "Genome transition in progress"),
1533        (status = 500, description = "Reset failed")
1534    )
1535)]
1536pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1537    let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1538        ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1539    })?;
1540
1541    let genome_service = state.genome_service.as_ref();
1542    genome_service.reset_connectome().await.map_err(|e| {
1543        tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1544        ApiError::internal(format!("Genome reset failed: {}", e))
1545    })?;
1546
1547    info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1548    Ok(Json(HashMap::from([(
1549        "message".to_string(),
1550        "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1551    )])))
1552}
1553
1554/// Get genome metadata (alternative endpoint to properties).
1555#[utoipa::path(
1556    get,
1557    path = "/v1/genome/metadata",
1558    tag = "genome",
1559    responses(
1560        (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1561    )
1562)]
1563pub async fn get_metadata(
1564    State(state): State<ApiState>,
1565) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1566    get_properties(State(state)).await
1567}
1568
1569/// Merge another genome into the current genome, combining their structures.
1570#[utoipa::path(
1571    post,
1572    path = "/v1/genome/merge",
1573    tag = "genome",
1574    responses(
1575        (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1576    )
1577)]
1578pub async fn post_merge(
1579    State(_state): State<ApiState>,
1580    Json(_request): Json<HashMap<String, serde_json::Value>>,
1581) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1582    // TODO: Implement genome merging
1583    let mut response = HashMap::new();
1584    response.insert(
1585        "message".to_string(),
1586        serde_json::json!("Genome merging not yet implemented"),
1587    );
1588
1589    Ok(Json(response))
1590}
1591
1592/// Get a diff comparison between two genomes showing their differences.
1593#[utoipa::path(
1594    get,
1595    path = "/v1/genome/diff",
1596    tag = "genome",
1597    params(
1598        ("genome_a" = String, Query, description = "First genome name"),
1599        ("genome_b" = String, Query, description = "Second genome name")
1600    ),
1601    responses(
1602        (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1603    )
1604)]
1605pub async fn get_diff(
1606    State(_state): State<ApiState>,
1607    Query(_params): Query<HashMap<String, String>>,
1608) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1609    // TODO: Implement genome diffing
1610    let mut response = HashMap::new();
1611    response.insert("differences".to_string(), serde_json::json!([]));
1612
1613    Ok(Json(response))
1614}
1615
1616/// Export genome in a specific format (JSON, YAML, binary, etc.).
1617#[utoipa::path(
1618    post,
1619    path = "/v1/genome/export_format",
1620    tag = "genome",
1621    responses(
1622        (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1623    )
1624)]
1625pub async fn post_export_format(
1626    State(_state): State<ApiState>,
1627    Json(_request): Json<HashMap<String, String>>,
1628) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1629    // TODO: Implement format-specific export
1630    let mut response = HashMap::new();
1631    response.insert(
1632        "message".to_string(),
1633        serde_json::json!("Format export not yet implemented"),
1634    );
1635
1636    Ok(Json(response))
1637}
1638
1639// EXACT Python paths:
1640/// Get current amalgamation status and configuration.
1641#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1642pub async fn get_amalgamation(
1643    State(state): State<ApiState>,
1644) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1645    let lock = state.amalgamation_state.read();
1646    let mut response = HashMap::new();
1647    if let Some(p) = lock.pending.as_ref() {
1648        response.insert(
1649            "pending".to_string(),
1650            amalgamation::pending_summary_to_health_json(&p.summary),
1651        );
1652    } else {
1653        response.insert("pending".to_string(), serde_json::Value::Null);
1654    }
1655    Ok(Json(response))
1656}
1657
1658/// Get history of all genome amalgamation operations performed.
1659#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1660pub async fn get_amalgamation_history_exact(
1661    State(state): State<ApiState>,
1662) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1663    let lock = state.amalgamation_state.read();
1664    let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1665    for entry in &lock.history {
1666        out.push(HashMap::from([
1667            (
1668                "amalgamation_id".to_string(),
1669                serde_json::json!(entry.amalgamation_id),
1670            ),
1671            (
1672                "genome_title".to_string(),
1673                serde_json::json!(entry.genome_title),
1674            ),
1675            (
1676                "circuit_size".to_string(),
1677                serde_json::json!(entry.circuit_size),
1678            ),
1679            ("status".to_string(), serde_json::json!(entry.status)),
1680            (
1681                "timestamp_ms".to_string(),
1682                serde_json::json!(entry.timestamp_ms),
1683            ),
1684        ]));
1685    }
1686    Ok(Json(out))
1687}
1688
1689/// Get metadata about all available cortical types including supported encodings and configurations.
1690#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1691pub async fn get_cortical_template(
1692    State(_state): State<ApiState>,
1693) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1694    use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1695        FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1696    };
1697    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1698    use serde_json::json;
1699
1700    let mut templates = HashMap::new();
1701
1702    // Helper to convert data type to human-readable format.
1703    //
1704    // NOTE: This endpoint is designed for tool/UIs (e.g. BV) and must be
1705    // deterministic across platforms and runs. No fallbacks.
1706    let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1707        let (variant, frame, positioning) = match dt {
1708            IOCorticalAreaConfigurationFlag::Boolean => {
1709                ("Boolean", FrameChangeHandling::Absolute, None)
1710            }
1711            IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1712            IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1713            IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1714            IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1715            IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1716                ("SignedPercentage", f, Some(p))
1717            }
1718            IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1719                ("SignedPercentage2D", f, Some(p))
1720            }
1721            IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1722                ("SignedPercentage3D", f, Some(p))
1723            }
1724            IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1725                ("SignedPercentage4D", f, Some(p))
1726            }
1727            IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1728            IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1729        };
1730
1731        let frame_str = match frame {
1732            FrameChangeHandling::Absolute => "Absolute",
1733            FrameChangeHandling::Incremental => "Incremental",
1734        };
1735
1736        let positioning_str = positioning.map(|p| match p {
1737            PercentageNeuronPositioning::Linear => "Linear",
1738            PercentageNeuronPositioning::Fractional => "Fractional",
1739        });
1740
1741        json!({
1742            "variant": variant,
1743            "frame_change_handling": frame_str,
1744            "percentage_positioning": positioning_str,
1745            "config_value": dt.to_data_type_configuration_flag()
1746        })
1747    };
1748
1749    // Add motor types
1750    for motor_unit in MotorCorticalUnit::list_all() {
1751        let friendly_name = motor_unit.get_friendly_name();
1752        let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1753        let num_areas = motor_unit.get_number_cortical_areas();
1754        let topology = motor_unit.get_unit_default_topology();
1755
1756        // BREAKING CHANGE (unreleased API):
1757        // - Remove unit-level `supported_data_types`.
1758        // - Expose per-subunit metadata, because some units (e.g. Gaze) have heterogeneous subunits
1759        //   with different IOCorticalAreaConfigurationFlag variants (Percentage2D vs Percentage).
1760        //
1761        // We derive supported types by:
1762        // - generating canonical cortical IDs from the MotorCorticalUnit template for each
1763        //   (frame_change_handling, percentage_neuron_positioning) combination
1764        // - extracting the IO configuration flag from each cortical ID
1765        // - grouping supported_data_types per subunit index
1766        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1767        use serde_json::{Map, Value};
1768        use std::collections::HashMap as StdHashMap;
1769
1770        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1771
1772        // Initialize subunits with topology-derived properties.
1773        for (sub_idx, topo) in topology {
1774            subunits.insert(
1775                sub_idx.get().to_string(),
1776                json!({
1777                    "relative_position": topo.relative_position,
1778                    "channel_dimensions_default": topo.channel_dimensions_default,
1779                    "channel_dimensions_min": topo.channel_dimensions_min,
1780                    "channel_dimensions_max": topo.channel_dimensions_max,
1781                    "supported_data_types": Vec::<serde_json::Value>::new(),
1782                }),
1783            );
1784        }
1785
1786        // Build per-subunit supported_data_types (deduped).
1787        let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1788        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1789            Some(allowed) => allowed.to_vec(),
1790            None => vec![
1791                FrameChangeHandling::Absolute,
1792                FrameChangeHandling::Incremental,
1793            ],
1794        };
1795
1796        let positionings = [
1797            PercentageNeuronPositioning::Linear,
1798            PercentageNeuronPositioning::Fractional,
1799        ];
1800
1801        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1802            StdHashMap::new();
1803
1804        for frame in frames {
1805            for positioning in positionings {
1806                let mut map: Map<String, Value> = Map::new();
1807                map.insert(
1808                    "frame_change_handling".to_string(),
1809                    serde_json::to_value(frame).unwrap_or(Value::Null),
1810                );
1811                map.insert(
1812                    "percentage_neuron_positioning".to_string(),
1813                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1814                );
1815
1816                // Use unit index 0 for template enumeration (index does not affect IO flags).
1817                let cortical_ids = motor_unit
1818                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1819                        CorticalUnitIndex::from(0u8),
1820                        map,
1821                    );
1822
1823                if let Ok(ids) = cortical_ids {
1824                    for (i, id) in ids.into_iter().enumerate() {
1825                        if let Ok(flag) = id.extract_io_data_flag() {
1826                            let dt_json = data_type_to_json(flag);
1827                            let subunit_key = i.to_string();
1828
1829                            let dedup_key = format!(
1830                                "{}|{}|{}",
1831                                dt_json
1832                                    .get("variant")
1833                                    .and_then(|v| v.as_str())
1834                                    .unwrap_or(""),
1835                                dt_json
1836                                    .get("frame_change_handling")
1837                                    .and_then(|v| v.as_str())
1838                                    .unwrap_or(""),
1839                                dt_json
1840                                    .get("percentage_positioning")
1841                                    .and_then(|v| v.as_str())
1842                                    .unwrap_or("")
1843                            );
1844
1845                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1846                            if !seen.insert(dedup_key) {
1847                                continue;
1848                            }
1849
1850                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1851                                if let Some(arr) = subunit_obj
1852                                    .get_mut("supported_data_types")
1853                                    .and_then(|v| v.as_array_mut())
1854                                {
1855                                    arr.push(dt_json);
1856                                }
1857                            }
1858                        }
1859                    }
1860                }
1861            }
1862        }
1863
1864        templates.insert(
1865            format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1866            json!({
1867                "type": "motor",
1868                "friendly_name": friendly_name,
1869                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1870                "number_of_cortical_areas": num_areas,
1871                "subunits": subunits,
1872                "description": format!("Motor output: {}", friendly_name)
1873            }),
1874        );
1875    }
1876
1877    // Add sensory types
1878    for sensory_unit in SensoryCorticalUnit::list_all() {
1879        let friendly_name = sensory_unit.get_friendly_name();
1880        let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1881        let num_areas = sensory_unit.get_number_cortical_areas();
1882        let topology = sensory_unit.get_unit_default_topology();
1883
1884        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1885        use serde_json::{Map, Value};
1886        use std::collections::HashMap as StdHashMap;
1887
1888        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1889
1890        for (sub_idx, topo) in topology {
1891            subunits.insert(
1892                sub_idx.get().to_string(),
1893                json!({
1894                    "relative_position": topo.relative_position,
1895                    "channel_dimensions_default": topo.channel_dimensions_default,
1896                    "channel_dimensions_min": topo.channel_dimensions_min,
1897                    "channel_dimensions_max": topo.channel_dimensions_max,
1898                    "supported_data_types": Vec::<serde_json::Value>::new(),
1899                }),
1900            );
1901        }
1902
1903        let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1904        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1905            Some(allowed) => allowed.to_vec(),
1906            None => vec![
1907                FrameChangeHandling::Absolute,
1908                FrameChangeHandling::Incremental,
1909            ],
1910        };
1911
1912        let positionings = [
1913            PercentageNeuronPositioning::Linear,
1914            PercentageNeuronPositioning::Fractional,
1915        ];
1916
1917        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1918            StdHashMap::new();
1919
1920        for frame in frames {
1921            for positioning in positionings {
1922                let mut map: Map<String, Value> = Map::new();
1923                map.insert(
1924                    "frame_change_handling".to_string(),
1925                    serde_json::to_value(frame).unwrap_or(Value::Null),
1926                );
1927                map.insert(
1928                    "percentage_neuron_positioning".to_string(),
1929                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1930                );
1931
1932                let cortical_ids = sensory_unit
1933                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1934                        CorticalUnitIndex::from(0u8),
1935                        map,
1936                    );
1937
1938                if let Ok(ids) = cortical_ids {
1939                    for (i, id) in ids.into_iter().enumerate() {
1940                        if let Ok(flag) = id.extract_io_data_flag() {
1941                            let dt_json = data_type_to_json(flag);
1942                            let subunit_key = i.to_string();
1943
1944                            let dedup_key = format!(
1945                                "{}|{}|{}",
1946                                dt_json
1947                                    .get("variant")
1948                                    .and_then(|v| v.as_str())
1949                                    .unwrap_or(""),
1950                                dt_json
1951                                    .get("frame_change_handling")
1952                                    .and_then(|v| v.as_str())
1953                                    .unwrap_or(""),
1954                                dt_json
1955                                    .get("percentage_positioning")
1956                                    .and_then(|v| v.as_str())
1957                                    .unwrap_or("")
1958                            );
1959
1960                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1961                            if !seen.insert(dedup_key) {
1962                                continue;
1963                            }
1964
1965                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1966                                if let Some(arr) = subunit_obj
1967                                    .get_mut("supported_data_types")
1968                                    .and_then(|v| v.as_array_mut())
1969                                {
1970                                    arr.push(dt_json);
1971                                }
1972                            }
1973                        }
1974                    }
1975                }
1976            }
1977        }
1978
1979        templates.insert(
1980            format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1981            json!({
1982                "type": "sensory",
1983                "friendly_name": friendly_name,
1984                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1985                "number_of_cortical_areas": num_areas,
1986                "subunits": subunits,
1987                "description": format!("Sensory input: {}", friendly_name)
1988            }),
1989        );
1990    }
1991
1992    Ok(Json(templates))
1993}
1994
1995/// Get list of available embedded default genome templates (barebones, essential, test, vision).
1996#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1997pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1998    Ok(Json(vec![
1999        "barebones".to_string(),
2000        "essential".to_string(),
2001        "test".to_string(),
2002        "vision".to_string(),
2003    ]))
2004}
2005
2006/// Download a specific brain region from the genome.
2007#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
2008pub async fn get_download_region(
2009    State(state): State<ApiState>,
2010    Query(params): Query<HashMap<String, String>>,
2011) -> ApiResult<Json<serde_json::Value>> {
2012    let region_id = params
2013        .get("region_id")
2014        .cloned()
2015        .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
2016    let json_str = state
2017        .genome_service
2018        .export_region_genome(region_id)
2019        .await
2020        .map_err(ApiError::from)?;
2021    let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
2022        ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
2023    })?;
2024    Ok(Json(value))
2025}
2026
2027/// Get the current genome number or generation identifier.
2028#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
2029pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
2030    Ok(Json(0))
2031}
2032
2033/// Perform genome amalgamation by specifying a filename.
2034#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
2035pub async fn post_amalgamation_by_filename(
2036    State(state): State<ApiState>,
2037    Json(req): Json<HashMap<String, String>>,
2038) -> ApiResult<Json<HashMap<String, String>>> {
2039    // Deterministic implementation:
2040    // - Supports embedded Rust template genomes by name (no filesystem I/O).
2041    // - For all other filenames, require /amalgamation_by_payload.
2042    let file_name = req
2043        .get("file_name")
2044        .or_else(|| req.get("filename"))
2045        .or_else(|| req.get("genome_file_name"))
2046        .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
2047
2048    let genome_json = match file_name.as_str() {
2049        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
2050        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
2051        "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
2052        "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
2053        other => {
2054            return Err(ApiError::invalid_input(format!(
2055                "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
2056                other
2057            )))
2058        }
2059    };
2060
2061    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
2062
2063    Ok(Json(HashMap::from([
2064        ("message".to_string(), "Amalgamation queued".to_string()),
2065        ("amalgamation_id".to_string(), amalgamation_id),
2066    ])))
2067}
2068
2069/// Perform genome amalgamation using a direct JSON payload.
2070#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
2071pub async fn post_amalgamation_by_payload(
2072    State(state): State<ApiState>,
2073    Json(req): Json<serde_json::Value>,
2074) -> ApiResult<Json<HashMap<String, String>>> {
2075    let json_str = serde_json::to_string(&req)
2076        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
2077    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2078
2079    Ok(Json(HashMap::from([
2080        ("message".to_string(), "Amalgamation queued".to_string()),
2081        ("amalgamation_id".to_string(), amalgamation_id),
2082    ])))
2083}
2084
2085/// Perform genome amalgamation by uploading a genome file.
2086#[cfg(feature = "http")]
2087#[utoipa::path(
2088    post,
2089    path = "/v1/genome/amalgamation_by_upload",
2090    tag = "genome",
2091    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2092    responses(
2093        (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
2094        (status = 400, description = "Invalid request"),
2095        (status = 500, description = "Internal server error")
2096    )
2097)]
2098pub async fn post_amalgamation_by_upload(
2099    State(state): State<ApiState>,
2100    mut multipart: Multipart,
2101) -> ApiResult<Json<HashMap<String, String>>> {
2102    let mut genome_json: Option<String> = None;
2103
2104    while let Some(field) = multipart
2105        .next_field()
2106        .await
2107        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2108    {
2109        if field.name() == Some("file") {
2110            let bytes = field.bytes().await.map_err(|e| {
2111                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2112            })?;
2113
2114            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2115                ApiError::invalid_input(format!(
2116                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2117                    e
2118                ))
2119            })?;
2120            genome_json = Some(json_str.to_string());
2121            break;
2122        }
2123    }
2124
2125    let json_str =
2126        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2127    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2128
2129    Ok(Json(HashMap::from([
2130        ("message".to_string(), "Amalgamation queued".to_string()),
2131        ("amalgamation_id".to_string(), amalgamation_id),
2132    ])))
2133}
2134
2135/// Append structures to the genome from a file.
2136#[cfg(feature = "http")]
2137#[utoipa::path(
2138    post,
2139    path = "/v1/genome/append-file",
2140    tag = "genome",
2141    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2142    responses(
2143        (status = 200, description = "Append processed", body = HashMap<String, String>)
2144    )
2145)]
2146pub async fn post_append_file(
2147    State(_state): State<ApiState>,
2148    mut _multipart: Multipart,
2149) -> ApiResult<Json<HashMap<String, String>>> {
2150    Ok(Json(HashMap::from([(
2151        "message".to_string(),
2152        "Not yet implemented".to_string(),
2153    )])))
2154}
2155
2156/// Upload and load a genome from a file.
2157#[cfg(feature = "http")]
2158#[utoipa::path(
2159    post,
2160    path = "/v1/genome/upload/file",
2161    tag = "genome",
2162    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2163    responses(
2164        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
2165        (status = 400, description = "Invalid request"),
2166        (status = 500, description = "Internal server error")
2167    )
2168)]
2169pub async fn post_upload_file(
2170    State(state): State<ApiState>,
2171    mut multipart: Multipart,
2172) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
2173    let mut genome_json: Option<String> = None;
2174
2175    while let Some(field) = multipart
2176        .next_field()
2177        .await
2178        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2179    {
2180        if field.name() == Some("file") {
2181            let bytes = field.bytes().await.map_err(|e| {
2182                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2183            })?;
2184
2185            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2186                ApiError::invalid_input(format!(
2187                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2188                    e
2189                ))
2190            })?;
2191            genome_json = Some(json_str.to_string());
2192            break;
2193        }
2194    }
2195
2196    let json_str =
2197        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2198
2199    let genome_info =
2200        load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
2201            .await?;
2202
2203    let mut response = HashMap::new();
2204    response.insert("success".to_string(), serde_json::json!(true));
2205    response.insert(
2206        "message".to_string(),
2207        serde_json::json!("Genome uploaded successfully"),
2208    );
2209    response.insert(
2210        "cortical_area_count".to_string(),
2211        serde_json::json!(genome_info.cortical_area_count),
2212    );
2213    response.insert(
2214        "brain_region_count".to_string(),
2215        serde_json::json!(genome_info.brain_region_count),
2216    );
2217
2218    Ok(Json(response))
2219}
2220
2221/// Upload a genome file with edit mode enabled.
2222#[cfg(feature = "http")]
2223#[utoipa::path(
2224    post,
2225    path = "/v1/genome/upload/file/edit",
2226    tag = "genome",
2227    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2228    responses(
2229        (status = 200, description = "Upload processed", body = HashMap<String, String>)
2230    )
2231)]
2232pub async fn post_upload_file_edit(
2233    State(_state): State<ApiState>,
2234    mut _multipart: Multipart,
2235) -> ApiResult<Json<HashMap<String, String>>> {
2236    Ok(Json(HashMap::from([(
2237        "message".to_string(),
2238        "Not yet implemented".to_string(),
2239    )])))
2240}
2241
2242/// Upload and load a genome from a JSON string.
2243#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
2244pub async fn post_upload_string(
2245    State(_state): State<ApiState>,
2246    Json(_req): Json<String>,
2247) -> ApiResult<Json<HashMap<String, String>>> {
2248    Ok(Json(HashMap::from([(
2249        "message".to_string(),
2250        "Not yet implemented".to_string(),
2251    )])))
2252}