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