Skip to main content

feagi_api/endpoints/
genome.rs

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