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                .keys()
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: under configured data root (not cwd) so containers/read-only roots work.
1272        let timestamp = std::time::SystemTime::now()
1273            .duration_since(std::time::UNIX_EPOCH)
1274            .unwrap()
1275            .as_secs();
1276        state
1277            .filesystem_data_root
1278            .join("cache")
1279            .join(".genome")
1280            .join(format!("saved_genome_{}.json", timestamp))
1281    };
1282
1283    // Ensure parent directory exists
1284    if let Some(parent) = Path::new(&save_path).parent() {
1285        fs::create_dir_all(parent)
1286            .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
1287    }
1288
1289    // Write to file
1290    fs::write(&save_path, genome_json)
1291        .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
1292
1293    info!("✅ Genome saved successfully to: {}", save_path.display());
1294
1295    Ok(Json(HashMap::from([
1296        (
1297            "message".to_string(),
1298            "Genome saved successfully".to_string(),
1299        ),
1300        ("file_path".to_string(), save_path.display().to_string()),
1301    ])))
1302}
1303
1304/// Load a genome from a file by name.
1305#[utoipa::path(
1306    post,
1307    path = "/v1/genome/load",
1308    tag = "genome",
1309    responses(
1310        (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
1311    )
1312)]
1313pub async fn post_load(
1314    State(state): State<ApiState>,
1315    Json(request): Json<HashMap<String, String>>,
1316) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1317    let genome_name = request
1318        .get("genome_name")
1319        .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
1320
1321    // Load genome from defaults
1322    let params = feagi_services::LoadGenomeParams {
1323        json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
1324    };
1325
1326    let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
1327
1328    let mut response = HashMap::new();
1329    response.insert(
1330        "message".to_string(),
1331        serde_json::json!("Genome loaded successfully"),
1332    );
1333    response.insert(
1334        "genome_title".to_string(),
1335        serde_json::json!(genome_info.genome_title),
1336    );
1337
1338    Ok(Json(response))
1339}
1340
1341/// Upload and load a genome from JSON payload.
1342#[utoipa::path(
1343    post,
1344    path = "/v1/genome/upload",
1345    tag = "genome",
1346    responses(
1347        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
1348    )
1349)]
1350pub async fn post_upload(
1351    State(state): State<ApiState>,
1352    Json(genome_json): Json<serde_json::Value>,
1353) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1354    // Convert to JSON string
1355    let json_str = serde_json::to_string(&genome_json)
1356        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1357
1358    let params = LoadGenomeParams { json_str };
1359    let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
1360
1361    let mut response = HashMap::new();
1362    response.insert("success".to_string(), serde_json::json!(true));
1363    response.insert(
1364        "message".to_string(),
1365        serde_json::json!("Genome uploaded successfully"),
1366    );
1367    response.insert(
1368        "cortical_area_count".to_string(),
1369        serde_json::json!(genome_info.cortical_area_count),
1370    );
1371    response.insert(
1372        "brain_region_count".to_string(),
1373        serde_json::json!(genome_info.brain_region_count),
1374    );
1375
1376    Ok(Json(response))
1377}
1378
1379/// Download the current genome as a JSON document.
1380#[utoipa::path(
1381    get,
1382    path = "/v1/genome/download",
1383    tag = "genome",
1384    responses(
1385        (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
1386    )
1387)]
1388pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
1389    info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
1390    let genome_service = state.genome_service.as_ref();
1391
1392    // Get genome as JSON string
1393    let genome_json_str = genome_service
1394        .save_genome(feagi_services::types::SaveGenomeParams {
1395            genome_id: None,
1396            genome_title: None,
1397        })
1398        .await
1399        .map_err(|e| {
1400            tracing::error!("Failed to export genome: {}", e);
1401            ApiError::internal(format!("Failed to export genome: {}", e))
1402        })?;
1403
1404    // Parse to Value for JSON response
1405    let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
1406        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1407
1408    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at download time.
1409    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1410    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1411
1412    info!(
1413        "✅ Genome download complete, {} bytes",
1414        genome_json_str.len()
1415    );
1416    Ok(Json(genome_value))
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421    use super::*;
1422    use serde_json::json;
1423
1424    #[test]
1425    fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
1426        let genome = json!({
1427            "version": "3.0",
1428            "physiology": {
1429                "simulation_timestep": 0.025,
1430                "max_age": 10000000
1431            }
1432        });
1433
1434        let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
1435        assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
1436        assert_eq!(updated["physiology"]["max_age"], json!(10000000));
1437    }
1438
1439    #[test]
1440    fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
1441        let genome = json!({ "version": "3.0" });
1442        let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
1443        assert!(format!("{err:?}").contains("physiology"));
1444    }
1445}
1446
1447/// Get genome properties including metadata, size, and configuration details.
1448#[utoipa::path(
1449    get,
1450    path = "/v1/genome/properties",
1451    tag = "genome",
1452    responses(
1453        (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
1454    )
1455)]
1456pub async fn get_properties(
1457    State(_state): State<ApiState>,
1458) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1459    // TODO: Implement proper metadata retrieval from genome service
1460    Ok(Json(HashMap::new()))
1461}
1462
1463/// Validate a genome structure for correctness and completeness.
1464#[utoipa::path(
1465    post,
1466    path = "/v1/genome/validate",
1467    tag = "genome",
1468    responses(
1469        (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1470    )
1471)]
1472pub async fn post_validate(
1473    State(_state): State<ApiState>,
1474    Json(_genome): Json<serde_json::Value>,
1475) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1476    // TODO: Implement genome validation
1477    let mut response = HashMap::new();
1478    response.insert("valid".to_string(), serde_json::json!(true));
1479    response.insert("errors".to_string(), serde_json::json!([]));
1480    response.insert("warnings".to_string(), serde_json::json!([]));
1481
1482    Ok(Json(response))
1483}
1484
1485/// Transform genome between different formats (flat to hierarchical or vice versa).
1486#[utoipa::path(
1487    post,
1488    path = "/v1/genome/transform",
1489    tag = "genome",
1490    responses(
1491        (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1492    )
1493)]
1494pub async fn post_transform(
1495    State(_state): State<ApiState>,
1496    Json(_request): Json<HashMap<String, serde_json::Value>>,
1497) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1498    // TODO: Implement genome transformation
1499    let mut response = HashMap::new();
1500    response.insert(
1501        "message".to_string(),
1502        serde_json::json!("Genome transformation not yet implemented"),
1503    );
1504
1505    Ok(Json(response))
1506}
1507
1508/// Clone the current genome with a new name, creating an independent copy.
1509#[utoipa::path(
1510    post,
1511    path = "/v1/genome/clone",
1512    tag = "genome",
1513    responses(
1514        (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1515    )
1516)]
1517pub async fn post_clone(
1518    State(_state): State<ApiState>,
1519    Json(_request): Json<HashMap<String, String>>,
1520) -> ApiResult<Json<HashMap<String, String>>> {
1521    // TODO: Implement genome cloning
1522    Ok(Json(HashMap::from([(
1523        "message".to_string(),
1524        "Genome cloning not yet implemented".to_string(),
1525    )])))
1526}
1527
1528/// Reset genome to its default state, clearing all cortical areas and brain regions.
1529/// Use before loading a new genome when "cortical area already exists" errors occur.
1530#[utoipa::path(
1531    post,
1532    path = "/v1/genome/reset",
1533    tag = "genome",
1534    responses(
1535        (status = 200, description = "Genome reset", body = HashMap<String, String>),
1536        (status = 409, description = "Genome transition in progress"),
1537        (status = 500, description = "Reset failed")
1538    )
1539)]
1540pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1541    let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1542        ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1543    })?;
1544
1545    let genome_service = state.genome_service.as_ref();
1546    genome_service.reset_connectome().await.map_err(|e| {
1547        tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1548        ApiError::internal(format!("Genome reset failed: {}", e))
1549    })?;
1550
1551    info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1552    Ok(Json(HashMap::from([(
1553        "message".to_string(),
1554        "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1555    )])))
1556}
1557
1558/// Get genome metadata (alternative endpoint to properties).
1559#[utoipa::path(
1560    get,
1561    path = "/v1/genome/metadata",
1562    tag = "genome",
1563    responses(
1564        (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1565    )
1566)]
1567pub async fn get_metadata(
1568    State(state): State<ApiState>,
1569) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1570    get_properties(State(state)).await
1571}
1572
1573/// Merge another genome into the current genome, combining their structures.
1574#[utoipa::path(
1575    post,
1576    path = "/v1/genome/merge",
1577    tag = "genome",
1578    responses(
1579        (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1580    )
1581)]
1582pub async fn post_merge(
1583    State(_state): State<ApiState>,
1584    Json(_request): Json<HashMap<String, serde_json::Value>>,
1585) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1586    // TODO: Implement genome merging
1587    let mut response = HashMap::new();
1588    response.insert(
1589        "message".to_string(),
1590        serde_json::json!("Genome merging not yet implemented"),
1591    );
1592
1593    Ok(Json(response))
1594}
1595
1596/// Get a diff comparison between two genomes showing their differences.
1597#[utoipa::path(
1598    get,
1599    path = "/v1/genome/diff",
1600    tag = "genome",
1601    params(
1602        ("genome_a" = String, Query, description = "First genome name"),
1603        ("genome_b" = String, Query, description = "Second genome name")
1604    ),
1605    responses(
1606        (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1607    )
1608)]
1609pub async fn get_diff(
1610    State(_state): State<ApiState>,
1611    Query(_params): Query<HashMap<String, String>>,
1612) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1613    // TODO: Implement genome diffing
1614    let mut response = HashMap::new();
1615    response.insert("differences".to_string(), serde_json::json!([]));
1616
1617    Ok(Json(response))
1618}
1619
1620/// Export genome in a specific format (JSON, YAML, binary, etc.).
1621#[utoipa::path(
1622    post,
1623    path = "/v1/genome/export_format",
1624    tag = "genome",
1625    responses(
1626        (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1627    )
1628)]
1629pub async fn post_export_format(
1630    State(_state): State<ApiState>,
1631    Json(_request): Json<HashMap<String, String>>,
1632) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1633    // TODO: Implement format-specific export
1634    let mut response = HashMap::new();
1635    response.insert(
1636        "message".to_string(),
1637        serde_json::json!("Format export not yet implemented"),
1638    );
1639
1640    Ok(Json(response))
1641}
1642
1643// EXACT Python paths:
1644/// Get current amalgamation status and configuration.
1645#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1646pub async fn get_amalgamation(
1647    State(state): State<ApiState>,
1648) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1649    let lock = state.amalgamation_state.read();
1650    let mut response = HashMap::new();
1651    if let Some(p) = lock.pending.as_ref() {
1652        response.insert(
1653            "pending".to_string(),
1654            amalgamation::pending_summary_to_health_json(&p.summary),
1655        );
1656    } else {
1657        response.insert("pending".to_string(), serde_json::Value::Null);
1658    }
1659    Ok(Json(response))
1660}
1661
1662/// Get history of all genome amalgamation operations performed.
1663#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1664pub async fn get_amalgamation_history_exact(
1665    State(state): State<ApiState>,
1666) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1667    let lock = state.amalgamation_state.read();
1668    let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1669    for entry in &lock.history {
1670        out.push(HashMap::from([
1671            (
1672                "amalgamation_id".to_string(),
1673                serde_json::json!(entry.amalgamation_id),
1674            ),
1675            (
1676                "genome_title".to_string(),
1677                serde_json::json!(entry.genome_title),
1678            ),
1679            (
1680                "circuit_size".to_string(),
1681                serde_json::json!(entry.circuit_size),
1682            ),
1683            ("status".to_string(), serde_json::json!(entry.status)),
1684            (
1685                "timestamp_ms".to_string(),
1686                serde_json::json!(entry.timestamp_ms),
1687            ),
1688        ]));
1689    }
1690    Ok(Json(out))
1691}
1692
1693/// Get metadata about all available cortical types including supported encodings and configurations.
1694#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1695pub async fn get_cortical_template(
1696    State(_state): State<ApiState>,
1697) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1698    use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1699        FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1700    };
1701    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1702    use serde_json::json;
1703
1704    let mut templates = HashMap::new();
1705
1706    // Helper to convert data type to human-readable format.
1707    //
1708    // NOTE: This endpoint is designed for tool/UIs (e.g. BV) and must be
1709    // deterministic across platforms and runs. No fallbacks.
1710    let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1711        let (variant, frame, positioning) = match dt {
1712            IOCorticalAreaConfigurationFlag::Boolean => {
1713                ("Boolean", FrameChangeHandling::Absolute, None)
1714            }
1715            IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1716            IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1717            IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1718            IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1719            IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1720                ("SignedPercentage", f, Some(p))
1721            }
1722            IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1723                ("SignedPercentage2D", f, Some(p))
1724            }
1725            IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1726                ("SignedPercentage3D", f, Some(p))
1727            }
1728            IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1729                ("SignedPercentage4D", f, Some(p))
1730            }
1731            IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1732            IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1733        };
1734
1735        let frame_str = match frame {
1736            FrameChangeHandling::Absolute => "Absolute",
1737            FrameChangeHandling::Incremental => "Incremental",
1738        };
1739
1740        let positioning_str = positioning.map(|p| match p {
1741            PercentageNeuronPositioning::Linear => "Linear",
1742            PercentageNeuronPositioning::Fractional => "Fractional",
1743        });
1744
1745        json!({
1746            "variant": variant,
1747            "frame_change_handling": frame_str,
1748            "percentage_positioning": positioning_str,
1749            "config_value": dt.to_data_type_configuration_flag()
1750        })
1751    };
1752
1753    // Add motor types
1754    for motor_unit in MotorCorticalUnit::list_all() {
1755        let friendly_name = motor_unit.get_friendly_name();
1756        let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1757        let num_areas = motor_unit.get_number_cortical_areas();
1758        let topology = motor_unit.get_unit_default_topology();
1759
1760        // BREAKING CHANGE (unreleased API):
1761        // - Remove unit-level `supported_data_types`.
1762        // - Expose per-subunit metadata, because some units (e.g. Gaze) have heterogeneous subunits
1763        //   with different IOCorticalAreaConfigurationFlag variants (Percentage2D vs Percentage).
1764        //
1765        // We derive supported types by:
1766        // - generating canonical cortical IDs from the MotorCorticalUnit template for each
1767        //   (frame_change_handling, percentage_neuron_positioning) combination
1768        // - extracting the IO configuration flag from each cortical ID
1769        // - grouping supported_data_types per subunit index
1770        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1771        use serde_json::{Map, Value};
1772        use std::collections::HashMap as StdHashMap;
1773
1774        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1775
1776        // Initialize subunits with topology-derived properties.
1777        for (sub_idx, topo) in topology {
1778            subunits.insert(
1779                sub_idx.get().to_string(),
1780                json!({
1781                    "relative_position": topo.relative_position,
1782                    "channel_dimensions_default": topo.channel_dimensions_default,
1783                    "channel_dimensions_min": topo.channel_dimensions_min,
1784                    "channel_dimensions_max": topo.channel_dimensions_max,
1785                    "supported_data_types": Vec::<serde_json::Value>::new(),
1786                }),
1787            );
1788        }
1789
1790        // Build per-subunit supported_data_types (deduped).
1791        let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1792        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1793            Some(allowed) => allowed.to_vec(),
1794            None => vec![
1795                FrameChangeHandling::Absolute,
1796                FrameChangeHandling::Incremental,
1797            ],
1798        };
1799
1800        let positionings = [
1801            PercentageNeuronPositioning::Linear,
1802            PercentageNeuronPositioning::Fractional,
1803        ];
1804
1805        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1806            StdHashMap::new();
1807
1808        for frame in frames {
1809            for positioning in positionings {
1810                let mut map: Map<String, Value> = Map::new();
1811                map.insert(
1812                    "frame_change_handling".to_string(),
1813                    serde_json::to_value(frame).unwrap_or(Value::Null),
1814                );
1815                map.insert(
1816                    "percentage_neuron_positioning".to_string(),
1817                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1818                );
1819
1820                // Use unit index 0 for template enumeration (index does not affect IO flags).
1821                let cortical_ids = motor_unit
1822                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1823                        CorticalUnitIndex::from(0u8),
1824                        map,
1825                    );
1826
1827                if let Ok(ids) = cortical_ids {
1828                    for (i, id) in ids.into_iter().enumerate() {
1829                        if let Ok(flag) = id.extract_io_data_flag() {
1830                            let dt_json = data_type_to_json(flag);
1831                            let subunit_key = i.to_string();
1832
1833                            let dedup_key = format!(
1834                                "{}|{}|{}",
1835                                dt_json
1836                                    .get("variant")
1837                                    .and_then(|v| v.as_str())
1838                                    .unwrap_or(""),
1839                                dt_json
1840                                    .get("frame_change_handling")
1841                                    .and_then(|v| v.as_str())
1842                                    .unwrap_or(""),
1843                                dt_json
1844                                    .get("percentage_positioning")
1845                                    .and_then(|v| v.as_str())
1846                                    .unwrap_or("")
1847                            );
1848
1849                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1850                            if !seen.insert(dedup_key) {
1851                                continue;
1852                            }
1853
1854                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1855                                if let Some(arr) = subunit_obj
1856                                    .get_mut("supported_data_types")
1857                                    .and_then(|v| v.as_array_mut())
1858                                {
1859                                    arr.push(dt_json);
1860                                }
1861                            }
1862                        }
1863                    }
1864                }
1865            }
1866        }
1867
1868        templates.insert(
1869            format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1870            json!({
1871                "type": "motor",
1872                "friendly_name": friendly_name,
1873                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1874                "number_of_cortical_areas": num_areas,
1875                "subunits": subunits,
1876                "description": format!("Motor output: {}", friendly_name)
1877            }),
1878        );
1879    }
1880
1881    // Add sensory types
1882    for sensory_unit in SensoryCorticalUnit::list_all() {
1883        let friendly_name = sensory_unit.get_friendly_name();
1884        let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1885        let num_areas = sensory_unit.get_number_cortical_areas();
1886        let topology = sensory_unit.get_unit_default_topology();
1887
1888        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1889        use serde_json::{Map, Value};
1890        use std::collections::HashMap as StdHashMap;
1891
1892        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1893
1894        for (sub_idx, topo) in topology {
1895            subunits.insert(
1896                sub_idx.get().to_string(),
1897                json!({
1898                    "relative_position": topo.relative_position,
1899                    "channel_dimensions_default": topo.channel_dimensions_default,
1900                    "channel_dimensions_min": topo.channel_dimensions_min,
1901                    "channel_dimensions_max": topo.channel_dimensions_max,
1902                    "supported_data_types": Vec::<serde_json::Value>::new(),
1903                }),
1904            );
1905        }
1906
1907        let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1908        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1909            Some(allowed) => allowed.to_vec(),
1910            None => vec![
1911                FrameChangeHandling::Absolute,
1912                FrameChangeHandling::Incremental,
1913            ],
1914        };
1915
1916        let positionings = [
1917            PercentageNeuronPositioning::Linear,
1918            PercentageNeuronPositioning::Fractional,
1919        ];
1920
1921        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1922            StdHashMap::new();
1923
1924        for frame in frames {
1925            for positioning in positionings {
1926                let mut map: Map<String, Value> = Map::new();
1927                map.insert(
1928                    "frame_change_handling".to_string(),
1929                    serde_json::to_value(frame).unwrap_or(Value::Null),
1930                );
1931                map.insert(
1932                    "percentage_neuron_positioning".to_string(),
1933                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1934                );
1935
1936                let cortical_ids = sensory_unit
1937                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1938                        CorticalUnitIndex::from(0u8),
1939                        map,
1940                    );
1941
1942                if let Ok(ids) = cortical_ids {
1943                    for (i, id) in ids.into_iter().enumerate() {
1944                        if let Ok(flag) = id.extract_io_data_flag() {
1945                            let dt_json = data_type_to_json(flag);
1946                            let subunit_key = i.to_string();
1947
1948                            let dedup_key = format!(
1949                                "{}|{}|{}",
1950                                dt_json
1951                                    .get("variant")
1952                                    .and_then(|v| v.as_str())
1953                                    .unwrap_or(""),
1954                                dt_json
1955                                    .get("frame_change_handling")
1956                                    .and_then(|v| v.as_str())
1957                                    .unwrap_or(""),
1958                                dt_json
1959                                    .get("percentage_positioning")
1960                                    .and_then(|v| v.as_str())
1961                                    .unwrap_or("")
1962                            );
1963
1964                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1965                            if !seen.insert(dedup_key) {
1966                                continue;
1967                            }
1968
1969                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1970                                if let Some(arr) = subunit_obj
1971                                    .get_mut("supported_data_types")
1972                                    .and_then(|v| v.as_array_mut())
1973                                {
1974                                    arr.push(dt_json);
1975                                }
1976                            }
1977                        }
1978                    }
1979                }
1980            }
1981        }
1982
1983        templates.insert(
1984            format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1985            json!({
1986                "type": "sensory",
1987                "friendly_name": friendly_name,
1988                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1989                "number_of_cortical_areas": num_areas,
1990                "subunits": subunits,
1991                "description": format!("Sensory input: {}", friendly_name)
1992            }),
1993        );
1994    }
1995
1996    Ok(Json(templates))
1997}
1998
1999/// Get list of available embedded default genome templates (barebones, essential, test, vision).
2000#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
2001pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
2002    Ok(Json(vec![
2003        "barebones".to_string(),
2004        "essential".to_string(),
2005        "test".to_string(),
2006        "vision".to_string(),
2007    ]))
2008}
2009
2010/// Download a specific brain region from the genome.
2011#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
2012pub async fn get_download_region(
2013    State(state): State<ApiState>,
2014    Query(params): Query<HashMap<String, String>>,
2015) -> ApiResult<Json<serde_json::Value>> {
2016    let region_id = params
2017        .get("region_id")
2018        .cloned()
2019        .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
2020    let json_str = state
2021        .genome_service
2022        .export_region_genome(region_id)
2023        .await
2024        .map_err(ApiError::from)?;
2025    let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
2026        ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
2027    })?;
2028    Ok(Json(value))
2029}
2030
2031/// Get the current genome number or generation identifier.
2032#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
2033pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
2034    Ok(Json(0))
2035}
2036
2037/// Perform genome amalgamation by specifying a filename.
2038#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
2039pub async fn post_amalgamation_by_filename(
2040    State(state): State<ApiState>,
2041    Json(req): Json<HashMap<String, String>>,
2042) -> ApiResult<Json<HashMap<String, String>>> {
2043    // Deterministic implementation:
2044    // - Supports embedded Rust template genomes by name (no filesystem I/O).
2045    // - For all other filenames, require /amalgamation_by_payload.
2046    let file_name = req
2047        .get("file_name")
2048        .or_else(|| req.get("filename"))
2049        .or_else(|| req.get("genome_file_name"))
2050        .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
2051
2052    let genome_json = match file_name.as_str() {
2053        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
2054        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
2055        "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
2056        "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
2057        other => {
2058            return Err(ApiError::invalid_input(format!(
2059                "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
2060                other
2061            )))
2062        }
2063    };
2064
2065    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
2066
2067    Ok(Json(HashMap::from([
2068        ("message".to_string(), "Amalgamation queued".to_string()),
2069        ("amalgamation_id".to_string(), amalgamation_id),
2070    ])))
2071}
2072
2073/// Perform genome amalgamation using a direct JSON payload.
2074#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
2075pub async fn post_amalgamation_by_payload(
2076    State(state): State<ApiState>,
2077    Json(req): Json<serde_json::Value>,
2078) -> ApiResult<Json<HashMap<String, String>>> {
2079    let json_str = serde_json::to_string(&req)
2080        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
2081    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2082
2083    Ok(Json(HashMap::from([
2084        ("message".to_string(), "Amalgamation queued".to_string()),
2085        ("amalgamation_id".to_string(), amalgamation_id),
2086    ])))
2087}
2088
2089/// Perform genome amalgamation by uploading a genome file.
2090#[cfg(feature = "http")]
2091#[utoipa::path(
2092    post,
2093    path = "/v1/genome/amalgamation_by_upload",
2094    tag = "genome",
2095    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2096    responses(
2097        (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
2098        (status = 400, description = "Invalid request"),
2099        (status = 500, description = "Internal server error")
2100    )
2101)]
2102pub async fn post_amalgamation_by_upload(
2103    State(state): State<ApiState>,
2104    mut multipart: Multipart,
2105) -> ApiResult<Json<HashMap<String, String>>> {
2106    let mut genome_json: Option<String> = None;
2107
2108    while let Some(field) = multipart
2109        .next_field()
2110        .await
2111        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2112    {
2113        if field.name() == Some("file") {
2114            let bytes = field.bytes().await.map_err(|e| {
2115                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2116            })?;
2117
2118            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2119                ApiError::invalid_input(format!(
2120                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2121                    e
2122                ))
2123            })?;
2124            genome_json = Some(json_str.to_string());
2125            break;
2126        }
2127    }
2128
2129    let json_str =
2130        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2131    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2132
2133    Ok(Json(HashMap::from([
2134        ("message".to_string(), "Amalgamation queued".to_string()),
2135        ("amalgamation_id".to_string(), amalgamation_id),
2136    ])))
2137}
2138
2139/// Append structures to the genome from a file.
2140#[cfg(feature = "http")]
2141#[utoipa::path(
2142    post,
2143    path = "/v1/genome/append-file",
2144    tag = "genome",
2145    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2146    responses(
2147        (status = 200, description = "Append processed", body = HashMap<String, String>)
2148    )
2149)]
2150pub async fn post_append_file(
2151    State(_state): State<ApiState>,
2152    mut _multipart: Multipart,
2153) -> ApiResult<Json<HashMap<String, String>>> {
2154    Ok(Json(HashMap::from([(
2155        "message".to_string(),
2156        "Not yet implemented".to_string(),
2157    )])))
2158}
2159
2160/// Upload and load a genome from a file.
2161#[cfg(feature = "http")]
2162#[utoipa::path(
2163    post,
2164    path = "/v1/genome/upload/file",
2165    tag = "genome",
2166    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2167    responses(
2168        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
2169        (status = 400, description = "Invalid request"),
2170        (status = 500, description = "Internal server error")
2171    )
2172)]
2173pub async fn post_upload_file(
2174    State(state): State<ApiState>,
2175    mut multipart: Multipart,
2176) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
2177    let mut genome_json: Option<String> = None;
2178
2179    while let Some(field) = multipart
2180        .next_field()
2181        .await
2182        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2183    {
2184        if field.name() == Some("file") {
2185            let bytes = field.bytes().await.map_err(|e| {
2186                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2187            })?;
2188
2189            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2190                ApiError::invalid_input(format!(
2191                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2192                    e
2193                ))
2194            })?;
2195            genome_json = Some(json_str.to_string());
2196            break;
2197        }
2198    }
2199
2200    let json_str =
2201        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2202
2203    let genome_info =
2204        load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
2205            .await?;
2206
2207    let mut response = HashMap::new();
2208    response.insert("success".to_string(), serde_json::json!(true));
2209    response.insert(
2210        "message".to_string(),
2211        serde_json::json!("Genome uploaded successfully"),
2212    );
2213    response.insert(
2214        "cortical_area_count".to_string(),
2215        serde_json::json!(genome_info.cortical_area_count),
2216    );
2217    response.insert(
2218        "brain_region_count".to_string(),
2219        serde_json::json!(genome_info.brain_region_count),
2220    );
2221
2222    Ok(Json(response))
2223}
2224
2225/// Upload a genome file with edit mode enabled.
2226#[cfg(feature = "http")]
2227#[utoipa::path(
2228    post,
2229    path = "/v1/genome/upload/file/edit",
2230    tag = "genome",
2231    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2232    responses(
2233        (status = 200, description = "Upload processed", body = HashMap<String, String>)
2234    )
2235)]
2236pub async fn post_upload_file_edit(
2237    State(_state): State<ApiState>,
2238    mut _multipart: Multipart,
2239) -> ApiResult<Json<HashMap<String, String>>> {
2240    Ok(Json(HashMap::from([(
2241        "message".to_string(),
2242        "Not yet implemented".to_string(),
2243    )])))
2244}
2245
2246/// Upload and load a genome from a JSON string.
2247#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
2248pub async fn post_upload_string(
2249    State(_state): State<ApiState>,
2250    Json(_req): Json<String>,
2251) -> ApiResult<Json<HashMap<String, String>>> {
2252    Ok(Json(HashMap::from([(
2253        "message".to_string(),
2254        "Not yet implemented".to_string(),
2255    )])))
2256}