mockforge_chaos/
api.rs

1//! Management API for chaos engineering
2
3use crate::{
4    ab_testing::{ABTestConfig, ABTestingEngine, TestConclusion, VariantResults},
5    analytics::{ChaosAnalytics, TimeBucket},
6    auto_remediation::{RemediationConfig, RemediationEngine},
7    config::{
8        BulkheadConfig, ChaosConfig, CircuitBreakerConfig, FaultInjectionConfig, LatencyConfig,
9        NetworkProfile, RateLimitConfig, TrafficShapingConfig,
10    },
11    latency_metrics::LatencyMetricsTracker,
12    recommendations::{
13        Recommendation, RecommendationCategory, RecommendationEngine, RecommendationSeverity,
14    },
15    scenario_orchestrator::{OrchestratedScenario, ScenarioOrchestrator},
16    scenario_recorder::{RecordedScenario, ScenarioRecorder},
17    scenario_replay::{ReplayOptions, ReplaySpeed, ScenarioReplayEngine},
18    scenario_scheduler::{ScenarioScheduler, ScheduleType, ScheduledScenario},
19    scenarios::{ChaosScenario, PredefinedScenarios, ScenarioEngine},
20};
21use axum::{
22    extract::{Path, Query, State},
23    http::StatusCode,
24    response::{IntoResponse, Json, Response},
25    routing::{delete, get, post, put},
26    Router,
27};
28use parking_lot::RwLock as ParkingRwLock;
29use serde::{Deserialize, Serialize};
30use std::sync::Arc;
31use tokio::sync::RwLock;
32use tracing::info;
33
34/// Profile manager for storing custom profiles
35#[derive(Clone)]
36pub struct ProfileManager {
37    /// Custom user-created profiles
38    custom_profiles: Arc<ParkingRwLock<std::collections::HashMap<String, NetworkProfile>>>,
39}
40
41impl ProfileManager {
42    /// Create a new profile manager
43    pub fn new() -> Self {
44        Self {
45            custom_profiles: Arc::new(ParkingRwLock::new(std::collections::HashMap::new())),
46        }
47    }
48
49    /// Get all profiles (built-in + custom)
50    pub fn get_all_profiles(&self) -> Vec<NetworkProfile> {
51        let mut profiles = NetworkProfile::predefined_profiles();
52        let custom = self.custom_profiles.read();
53        profiles.extend(custom.values().cloned());
54        profiles
55    }
56
57    /// Get a profile by name
58    pub fn get_profile(&self, name: &str) -> Option<NetworkProfile> {
59        // Check built-in profiles first
60        for profile in NetworkProfile::predefined_profiles() {
61            if profile.name == name {
62                return Some(profile);
63            }
64        }
65        // Check custom profiles
66        let custom = self.custom_profiles.read();
67        custom.get(name).cloned()
68    }
69
70    /// Add or update a custom profile
71    pub fn save_profile(&self, profile: NetworkProfile) {
72        let mut custom = self.custom_profiles.write();
73        custom.insert(profile.name.clone(), profile);
74    }
75
76    /// Delete a custom profile
77    pub fn delete_profile(&self, name: &str) -> bool {
78        let mut custom = self.custom_profiles.write();
79        custom.remove(name).is_some()
80    }
81
82    /// Get only custom profiles
83    pub fn get_custom_profiles(&self) -> Vec<NetworkProfile> {
84        let custom = self.custom_profiles.read();
85        custom.values().cloned().collect()
86    }
87}
88
89impl Default for ProfileManager {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95/// API state
96#[derive(Clone)]
97pub struct ChaosApiState {
98    pub config: Arc<RwLock<ChaosConfig>>,
99    pub scenario_engine: Arc<ScenarioEngine>,
100    pub orchestrator: Arc<tokio::sync::RwLock<ScenarioOrchestrator>>,
101    pub analytics: Arc<ChaosAnalytics>,
102    pub recommendation_engine: Arc<RecommendationEngine>,
103    pub remediation_engine: Arc<RemediationEngine>,
104    pub ab_testing_engine: Arc<tokio::sync::RwLock<ABTestingEngine>>,
105    pub recorder: Arc<ScenarioRecorder>,
106    pub replay_engine: Arc<tokio::sync::RwLock<ScenarioReplayEngine>>,
107    pub scheduler: Arc<tokio::sync::RwLock<ScenarioScheduler>>,
108    pub latency_tracker: Arc<LatencyMetricsTracker>,
109    pub profile_manager: Arc<ProfileManager>,
110    pub mockai:
111        Option<std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
112}
113
114/// Create the chaos management API router
115///
116/// # Arguments
117/// * `config` - Initial chaos configuration
118/// * `mockai` - Optional MockAI instance for dynamic error message generation
119///
120/// # Returns
121/// Tuple of (Router, Config, LatencyTracker, ChaosApiState) - The router, config, latency tracker, and API state for hot-reload support
122pub fn create_chaos_api_router(
123    config: ChaosConfig,
124    mockai: Option<
125        std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
126    >,
127) -> (Router, Arc<RwLock<ChaosConfig>>, Arc<LatencyMetricsTracker>, Arc<ChaosApiState>) {
128    let config_arc = Arc::new(RwLock::new(config));
129    let scenario_engine = Arc::new(ScenarioEngine::new());
130    let orchestrator = Arc::new(tokio::sync::RwLock::new(ScenarioOrchestrator::new()));
131    let analytics = Arc::new(ChaosAnalytics::new());
132    let recommendation_engine = Arc::new(RecommendationEngine::new());
133    let remediation_engine = Arc::new(RemediationEngine::new());
134    let ab_testing_engine =
135        Arc::new(tokio::sync::RwLock::new(ABTestingEngine::new(analytics.clone())));
136    let recorder = Arc::new(ScenarioRecorder::new());
137    let replay_engine = Arc::new(tokio::sync::RwLock::new(ScenarioReplayEngine::new()));
138    let scheduler = Arc::new(tokio::sync::RwLock::new(ScenarioScheduler::new()));
139    let latency_tracker = Arc::new(LatencyMetricsTracker::new());
140    let profile_manager = Arc::new(ProfileManager::new());
141
142    // Clone latency_tracker for return value (state will own the original)
143    let latency_tracker_for_return = latency_tracker.clone();
144
145    let state = ChaosApiState {
146        config: config_arc.clone(),
147        scenario_engine,
148        orchestrator,
149        analytics,
150        recommendation_engine,
151        remediation_engine,
152        ab_testing_engine,
153        recorder,
154        replay_engine,
155        scheduler,
156        latency_tracker,
157        profile_manager,
158        mockai,
159    };
160
161    let router = Router::new()
162        // Configuration endpoints
163        .route("/api/chaos/config", get(get_config))
164        .route("/api/chaos/config", put(update_config))
165        .route("/api/chaos/config/latency", put(update_latency_config))
166        .route("/api/chaos/config/faults", put(update_fault_config))
167        .route("/api/chaos/config/rate-limit", put(update_rate_limit_config))
168        .route("/api/chaos/config/traffic", put(update_traffic_config))
169        .route("/api/chaos/config/circuit-breaker", put(update_circuit_breaker_config))
170        .route("/api/chaos/config/bulkhead", put(update_bulkhead_config))
171
172        // Protocol-specific configuration endpoints
173        .route("/api/chaos/protocols/grpc/status-codes", post(inject_grpc_status_codes))
174        .route("/api/chaos/protocols/grpc/stream-interruption", post(set_grpc_stream_interruption))
175        .route("/api/chaos/protocols/websocket/close-codes", post(inject_websocket_close_codes))
176        .route("/api/chaos/protocols/websocket/message-drop", post(set_websocket_message_drop))
177        .route("/api/chaos/protocols/websocket/message-corruption", post(set_websocket_message_corruption))
178        .route("/api/chaos/protocols/graphql/error-codes", post(inject_graphql_error_codes))
179        .route("/api/chaos/protocols/graphql/partial-data", post(set_graphql_partial_data))
180        .route("/api/chaos/protocols/graphql/resolver-latency", post(toggle_graphql_resolver_latency))
181
182        // Control endpoints
183        .route("/api/chaos/enable", post(enable_chaos))
184        .route("/api/chaos/disable", post(disable_chaos))
185        .route("/api/chaos/reset", post(reset_chaos))
186
187        // Scenario endpoints
188        .route("/api/chaos/scenarios", get(list_scenarios))
189        .route("/api/chaos/scenarios/predefined", get(list_predefined_scenarios))
190        .route("/api/chaos/scenarios/:name", post(start_scenario))
191        .route("/api/chaos/scenarios/:name", delete(stop_scenario))
192        .route("/api/chaos/scenarios", delete(stop_all_scenarios))
193
194        // Status endpoint
195        .route("/api/chaos/status", get(get_status))
196
197        // Metrics endpoints
198        .route("/api/chaos/metrics/latency", get(get_latency_metrics))
199        .route("/api/chaos/metrics/latency/stats", get(get_latency_stats))
200
201        // Profile management endpoints
202        .route("/api/chaos/profiles", get(list_profiles))
203        .route("/api/chaos/profiles/:name", get(get_profile))
204        .route("/api/chaos/profiles/:name/apply", post(apply_profile))
205        .route("/api/chaos/profiles", post(create_profile))
206        .route("/api/chaos/profiles/:name", delete(delete_profile))
207        .route("/api/chaos/profiles/:name/export", get(export_profile))
208        .route("/api/chaos/profiles/import", post(import_profile))
209
210        // Scenario recording endpoints
211        .route("/api/chaos/recording/start", post(start_recording))
212        .route("/api/chaos/recording/stop", post(stop_recording))
213        .route("/api/chaos/recording/status", get(recording_status))
214        .route("/api/chaos/recording/export", post(export_recording))
215
216        // Scenario replay endpoints
217        .route("/api/chaos/replay/start", post(start_replay))
218        .route("/api/chaos/replay/pause", post(pause_replay))
219        .route("/api/chaos/replay/resume", post(resume_replay))
220        .route("/api/chaos/replay/stop", post(stop_replay))
221        .route("/api/chaos/replay/status", get(replay_status))
222
223        // Scenario orchestration endpoints
224        .route("/api/chaos/orchestration/start", post(start_orchestration))
225        .route("/api/chaos/orchestration/stop", post(stop_orchestration))
226        .route("/api/chaos/orchestration/status", get(orchestration_status))
227        .route("/api/chaos/orchestration/import", post(import_orchestration))
228
229        // Scenario scheduling endpoints
230        .route("/api/chaos/schedule", post(add_schedule))
231        .route("/api/chaos/schedule/:id", get(get_schedule))
232        .route("/api/chaos/schedule/:id", delete(remove_schedule))
233        .route("/api/chaos/schedule/:id/enable", post(enable_schedule))
234        .route("/api/chaos/schedule/:id/disable", post(disable_schedule))
235        // NOTE: Manual trigger endpoint has a known Rust/Axum type inference issue
236        // when combining State + Path extractors with nested async calls.
237        // The trigger_schedule_by_path handler is implemented but cannot be registered.
238        // Workaround: Use the scheduler's automatic execution or recreate the schedule.
239        // .route("/api/chaos/schedule/:id/trigger", post(trigger_schedule_by_path))
240        .route("/api/chaos/schedules", get(list_schedules))
241
242        // AI-powered recommendation endpoints
243        .route("/api/chaos/recommendations", get(get_recommendations))
244        .route("/api/chaos/recommendations/analyze", post(analyze_and_recommend))
245        .route("/api/chaos/recommendations/category/:category", get(get_recommendations_by_category))
246        .route("/api/chaos/recommendations/severity/:severity", get(get_recommendations_by_severity))
247        .route("/api/chaos/recommendations", delete(clear_recommendations))
248
249        // Auto-remediation endpoints
250        .route("/api/chaos/remediation/config", get(get_remediation_config))
251        .route("/api/chaos/remediation/config", put(update_remediation_config))
252        .route("/api/chaos/remediation/process", post(process_remediation))
253        .route("/api/chaos/remediation/approve/:id", post(approve_remediation))
254        .route("/api/chaos/remediation/reject/:id", post(reject_remediation))
255        .route("/api/chaos/remediation/rollback/:id", post(rollback_remediation))
256        .route("/api/chaos/remediation/actions", get(get_remediation_actions))
257        .route("/api/chaos/remediation/actions/:id", get(get_remediation_action))
258        .route("/api/chaos/remediation/approvals", get(get_approval_queue))
259        .route("/api/chaos/remediation/effectiveness/:id", get(get_remediation_effectiveness))
260        .route("/api/chaos/remediation/stats", get(get_remediation_stats))
261
262        // A/B testing endpoints
263        .route("/api/chaos/ab-tests", post(create_ab_test))
264        .route("/api/chaos/ab-tests", get(get_ab_tests))
265        .route("/api/chaos/ab-tests/:id", get(get_ab_test))
266        .route("/api/chaos/ab-tests/:id/start", post(start_ab_test))
267        .route("/api/chaos/ab-tests/:id/stop", post(stop_ab_test))
268        .route("/api/chaos/ab-tests/:id/pause", post(pause_ab_test))
269        .route("/api/chaos/ab-tests/:id/resume", post(resume_ab_test))
270        .route("/api/chaos/ab-tests/:id/record/:variant", post(record_ab_test_result))
271        .route("/api/chaos/ab-tests/:id", delete(delete_ab_test))
272        .route("/api/chaos/ab-tests/stats", get(get_ab_test_stats))
273
274        .with_state(state.clone());
275
276    (router, config_arc, latency_tracker_for_return, Arc::new(state))
277}
278
279/// Get current configuration
280async fn get_config(State(state): State<ChaosApiState>) -> Json<ChaosConfig> {
281    let config = state.config.read().await;
282    Json(config.clone())
283}
284
285/// Update full configuration
286async fn update_config(
287    State(state): State<ChaosApiState>,
288    Json(new_config): Json<ChaosConfig>,
289) -> Json<StatusResponse> {
290    let mut config = state.config.write().await;
291    *config = new_config;
292    info!("Chaos configuration updated");
293    Json(StatusResponse {
294        message: "Configuration updated".to_string(),
295    })
296}
297
298/// Update latency configuration
299async fn update_latency_config(
300    State(state): State<ChaosApiState>,
301    Json(latency_config): Json<LatencyConfig>,
302) -> Json<StatusResponse> {
303    let mut config = state.config.write().await;
304    config.latency = Some(latency_config);
305    info!("Latency configuration updated");
306    Json(StatusResponse {
307        message: "Latency configuration updated".to_string(),
308    })
309}
310
311/// Update fault injection configuration
312async fn update_fault_config(
313    State(state): State<ChaosApiState>,
314    Json(fault_config): Json<FaultInjectionConfig>,
315) -> Json<StatusResponse> {
316    let mut config = state.config.write().await;
317    config.fault_injection = Some(fault_config);
318    info!("Fault injection configuration updated");
319    Json(StatusResponse {
320        message: "Fault injection configuration updated".to_string(),
321    })
322}
323
324/// Update rate limit configuration
325async fn update_rate_limit_config(
326    State(state): State<ChaosApiState>,
327    Json(rate_config): Json<RateLimitConfig>,
328) -> Json<StatusResponse> {
329    let mut config = state.config.write().await;
330    config.rate_limit = Some(rate_config);
331    info!("Rate limit configuration updated");
332    Json(StatusResponse {
333        message: "Rate limit configuration updated".to_string(),
334    })
335}
336
337/// Update traffic shaping configuration
338async fn update_traffic_config(
339    State(state): State<ChaosApiState>,
340    Json(traffic_config): Json<TrafficShapingConfig>,
341) -> Json<StatusResponse> {
342    let mut config = state.config.write().await;
343    config.traffic_shaping = Some(traffic_config);
344    info!("Traffic shaping configuration updated");
345    Json(StatusResponse {
346        message: "Traffic shaping configuration updated".to_string(),
347    })
348}
349
350/// Update circuit breaker configuration
351async fn update_circuit_breaker_config(
352    State(state): State<ChaosApiState>,
353    Json(cb_config): Json<CircuitBreakerConfig>,
354) -> Json<StatusResponse> {
355    let mut config = state.config.write().await;
356    config.circuit_breaker = Some(cb_config);
357    info!("Circuit breaker configuration updated");
358    Json(StatusResponse {
359        message: "Circuit breaker configuration updated".to_string(),
360    })
361}
362
363/// Update bulkhead configuration
364async fn update_bulkhead_config(
365    State(state): State<ChaosApiState>,
366    Json(bulkhead_config): Json<BulkheadConfig>,
367) -> Json<StatusResponse> {
368    let mut config = state.config.write().await;
369    config.bulkhead = Some(bulkhead_config);
370    info!("Bulkhead configuration updated");
371    Json(StatusResponse {
372        message: "Bulkhead configuration updated".to_string(),
373    })
374}
375
376/// Enable chaos engineering
377async fn enable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
378    let mut config = state.config.write().await;
379    config.enabled = true;
380    info!("Chaos engineering enabled");
381    Json(StatusResponse {
382        message: "Chaos engineering enabled".to_string(),
383    })
384}
385
386/// Disable chaos engineering
387async fn disable_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
388    let mut config = state.config.write().await;
389    config.enabled = false;
390    info!("Chaos engineering disabled");
391    Json(StatusResponse {
392        message: "Chaos engineering disabled".to_string(),
393    })
394}
395
396/// Reset chaos configuration to defaults
397async fn reset_chaos(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
398    let mut config = state.config.write().await;
399    *config = ChaosConfig::default();
400    state.scenario_engine.stop_all_scenarios();
401    info!("Chaos configuration reset to defaults");
402    Json(StatusResponse {
403        message: "Chaos configuration reset".to_string(),
404    })
405}
406
407/// List active scenarios
408async fn list_scenarios(State(state): State<ChaosApiState>) -> Json<Vec<ChaosScenario>> {
409    let scenarios = state.scenario_engine.get_active_scenarios();
410    Json(scenarios)
411}
412
413/// List predefined scenarios
414async fn list_predefined_scenarios() -> Json<Vec<PredefinedScenarioInfo>> {
415    Json(vec![
416        PredefinedScenarioInfo {
417            name: "network_degradation".to_string(),
418            description: "Simulates degraded network conditions with high latency and packet loss"
419                .to_string(),
420            tags: vec!["network".to_string(), "latency".to_string()],
421        },
422        PredefinedScenarioInfo {
423            name: "service_instability".to_string(),
424            description: "Simulates an unstable service with random errors and timeouts"
425                .to_string(),
426            tags: vec!["service".to_string(), "errors".to_string()],
427        },
428        PredefinedScenarioInfo {
429            name: "cascading_failure".to_string(),
430            description: "Simulates a cascading failure with multiple simultaneous issues"
431                .to_string(),
432            tags: vec!["critical".to_string(), "cascading".to_string()],
433        },
434        PredefinedScenarioInfo {
435            name: "peak_traffic".to_string(),
436            description: "Simulates peak traffic conditions with aggressive rate limiting"
437                .to_string(),
438            tags: vec!["traffic".to_string(), "load".to_string()],
439        },
440        PredefinedScenarioInfo {
441            name: "slow_backend".to_string(),
442            description: "Simulates a consistently slow backend service".to_string(),
443            tags: vec!["latency".to_string(), "performance".to_string()],
444        },
445    ])
446}
447
448/// Start a scenario
449async fn start_scenario(
450    State(state): State<ChaosApiState>,
451    Path(name): Path<String>,
452) -> Result<Json<StatusResponse>, ChaosApiError> {
453    let scenario = match name.as_str() {
454        "network_degradation" => PredefinedScenarios::network_degradation(),
455        "service_instability" => PredefinedScenarios::service_instability(),
456        "cascading_failure" => PredefinedScenarios::cascading_failure(),
457        "peak_traffic" => PredefinedScenarios::peak_traffic(),
458        "slow_backend" => PredefinedScenarios::slow_backend(),
459        _ => return Err(ChaosApiError::NotFound(format!("Scenario '{}' not found", name))),
460    };
461
462    state.scenario_engine.start_scenario(scenario.clone());
463
464    // Update config with scenario's chaos config
465    let mut config = state.config.write().await;
466    *config = scenario.chaos_config;
467
468    info!("Started scenario: {}", name);
469    Ok(Json(StatusResponse {
470        message: format!("Scenario '{}' started", name),
471    }))
472}
473
474/// Stop a scenario
475async fn stop_scenario(
476    State(state): State<ChaosApiState>,
477    Path(name): Path<String>,
478) -> Result<Json<StatusResponse>, ChaosApiError> {
479    if state.scenario_engine.stop_scenario(&name) {
480        info!("Stopped scenario: {}", name);
481        Ok(Json(StatusResponse {
482            message: format!("Scenario '{}' stopped", name),
483        }))
484    } else {
485        Err(ChaosApiError::NotFound(format!("Scenario '{}' not found or not running", name)))
486    }
487}
488
489/// Stop all scenarios
490async fn stop_all_scenarios(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
491    state.scenario_engine.stop_all_scenarios();
492    info!("Stopped all scenarios");
493    Json(StatusResponse {
494        message: "All scenarios stopped".to_string(),
495    })
496}
497
498/// Get chaos status
499async fn get_status(State(state): State<ChaosApiState>) -> Json<ChaosStatus> {
500    let config = state.config.read().await;
501    let scenarios = state.scenario_engine.get_active_scenarios();
502
503    Json(ChaosStatus {
504        enabled: config.enabled,
505        active_scenarios: scenarios.iter().map(|s| s.name.clone()).collect(),
506        latency_enabled: config.latency.as_ref().is_some_and(|l| l.enabled),
507        fault_injection_enabled: config.fault_injection.as_ref().is_some_and(|f| f.enabled),
508        rate_limit_enabled: config.rate_limit.as_ref().is_some_and(|r| r.enabled),
509        traffic_shaping_enabled: config.traffic_shaping.as_ref().is_some_and(|t| t.enabled),
510    })
511}
512
513// Protocol-specific handlers
514
515/// Inject gRPC status codes
516async fn inject_grpc_status_codes(
517    State(state): State<ChaosApiState>,
518    Json(req): Json<GrpcStatusCodesRequest>,
519) -> Json<StatusResponse> {
520    let mut config = state.config.write().await;
521
522    // Add gRPC-specific HTTP error codes that map to the requested gRPC status codes
523    let mut http_errors = config
524        .fault_injection
525        .as_ref()
526        .map(|f| f.http_errors.clone())
527        .unwrap_or_default();
528
529    for code in &req.status_codes {
530        // Map gRPC codes to HTTP codes
531        let http_code = match code {
532            3 => 400,  // INVALID_ARGUMENT
533            16 => 401, // UNAUTHENTICATED
534            7 => 403,  // PERMISSION_DENIED
535            5 => 404,  // NOT_FOUND
536            8 => 429,  // RESOURCE_EXHAUSTED
537            13 => 500, // INTERNAL
538            12 => 501, // UNIMPLEMENTED
539            14 => 503, // UNAVAILABLE
540            4 => 504,  // DEADLINE_EXCEEDED
541            _ => 500,  // Default to internal error
542        };
543        if !http_errors.contains(&http_code) {
544            http_errors.push(http_code);
545        }
546    }
547
548    if let Some(fault_config) = &mut config.fault_injection {
549        fault_config.http_errors = http_errors;
550        fault_config.http_error_probability = req.probability;
551    }
552
553    info!("gRPC status codes configured: {:?}", &req.status_codes);
554    Json(StatusResponse {
555        message: "gRPC status codes configured".to_string(),
556    })
557}
558
559/// Set gRPC stream interruption
560async fn set_grpc_stream_interruption(
561    State(state): State<ChaosApiState>,
562    Json(req): Json<ProbabilityRequest>,
563) -> Json<StatusResponse> {
564    let mut config = state.config.write().await;
565
566    if let Some(fault_config) = &mut config.fault_injection {
567        fault_config.partial_response_probability = req.probability;
568    }
569
570    info!("gRPC stream interruption probability set to {}", req.probability);
571    Json(StatusResponse {
572        message: "gRPC stream interruption configured".to_string(),
573    })
574}
575
576/// Inject WebSocket close codes
577async fn inject_websocket_close_codes(
578    State(state): State<ChaosApiState>,
579    Json(req): Json<WebSocketCloseCodesRequest>,
580) -> Json<StatusResponse> {
581    let mut config = state.config.write().await;
582
583    let mut http_errors = config
584        .fault_injection
585        .as_ref()
586        .map(|f| f.http_errors.clone())
587        .unwrap_or_default();
588
589    for code in &req.close_codes {
590        // Map WebSocket close codes to HTTP codes
591        let http_code = match code {
592            1002 => 400, // Protocol error
593            1001 => 408, // Going away (timeout)
594            1008 => 429, // Policy violation
595            1011 => 500, // Server error
596            _ => 500,
597        };
598        if !http_errors.contains(&http_code) {
599            http_errors.push(http_code);
600        }
601    }
602
603    if let Some(fault_config) = &mut config.fault_injection {
604        fault_config.http_errors = http_errors;
605        fault_config.http_error_probability = req.probability;
606    }
607
608    info!("WebSocket close codes configured: {:?}", &req.close_codes);
609    Json(StatusResponse {
610        message: "WebSocket close codes configured".to_string(),
611    })
612}
613
614/// Set WebSocket message drop probability
615async fn set_websocket_message_drop(
616    State(state): State<ChaosApiState>,
617    Json(req): Json<ProbabilityRequest>,
618) -> Json<StatusResponse> {
619    let mut config = state.config.write().await;
620
621    if let Some(traffic_config) = &mut config.traffic_shaping {
622        traffic_config.packet_loss_percent = req.probability * 100.0;
623    }
624
625    info!("WebSocket message drop probability set to {}", req.probability);
626    Json(StatusResponse {
627        message: "WebSocket message drop configured".to_string(),
628    })
629}
630
631/// Set WebSocket message corruption probability
632async fn set_websocket_message_corruption(
633    State(state): State<ChaosApiState>,
634    Json(req): Json<ProbabilityRequest>,
635) -> Json<StatusResponse> {
636    let mut config = state.config.write().await;
637
638    if let Some(fault_config) = &mut config.fault_injection {
639        fault_config.partial_response_probability = req.probability;
640    }
641
642    info!("WebSocket message corruption probability set to {}", req.probability);
643    Json(StatusResponse {
644        message: "WebSocket message corruption configured".to_string(),
645    })
646}
647
648/// Inject GraphQL error codes
649async fn inject_graphql_error_codes(
650    State(state): State<ChaosApiState>,
651    Json(req): Json<GraphQLErrorCodesRequest>,
652) -> Json<StatusResponse> {
653    let mut config = state.config.write().await;
654
655    let mut http_errors = config
656        .fault_injection
657        .as_ref()
658        .map(|f| f.http_errors.clone())
659        .unwrap_or_default();
660
661    for code in &req.error_codes {
662        let http_code = match code.as_str() {
663            "BAD_USER_INPUT" => 400,
664            "UNAUTHENTICATED" => 401,
665            "FORBIDDEN" => 403,
666            "NOT_FOUND" => 404,
667            "INTERNAL_SERVER_ERROR" => 500,
668            "SERVICE_UNAVAILABLE" => 503,
669            _ => 500,
670        };
671        if !http_errors.contains(&http_code) {
672            http_errors.push(http_code);
673        }
674    }
675
676    if let Some(fault_config) = &mut config.fault_injection {
677        fault_config.http_errors = http_errors;
678        fault_config.http_error_probability = req.probability;
679    }
680
681    info!("GraphQL error codes configured: {:?}", &req.error_codes);
682    Json(StatusResponse {
683        message: "GraphQL error codes configured".to_string(),
684    })
685}
686
687/// Set GraphQL partial data probability
688async fn set_graphql_partial_data(
689    State(state): State<ChaosApiState>,
690    Json(req): Json<ProbabilityRequest>,
691) -> Json<StatusResponse> {
692    let mut config = state.config.write().await;
693
694    if let Some(fault_config) = &mut config.fault_injection {
695        fault_config.partial_response_probability = req.probability;
696    }
697
698    info!("GraphQL partial data probability set to {}", req.probability);
699    Json(StatusResponse {
700        message: "GraphQL partial data configured".to_string(),
701    })
702}
703
704/// Toggle GraphQL resolver latency
705async fn toggle_graphql_resolver_latency(
706    State(state): State<ChaosApiState>,
707    Json(req): Json<EnableRequest>,
708) -> Json<StatusResponse> {
709    let mut config = state.config.write().await;
710
711    if let Some(latency_config) = &mut config.latency {
712        latency_config.enabled = req.enabled;
713    }
714
715    info!("GraphQL resolver latency {}", if req.enabled { "enabled" } else { "disabled" });
716    Json(StatusResponse {
717        message: format!(
718            "GraphQL resolver latency {}",
719            if req.enabled { "enabled" } else { "disabled" }
720        ),
721    })
722}
723
724// Request/Response types
725
726#[derive(Debug, Serialize)]
727struct StatusResponse {
728    message: String,
729}
730
731#[derive(Debug, Serialize)]
732struct PredefinedScenarioInfo {
733    name: String,
734    description: String,
735    tags: Vec<String>,
736}
737
738#[derive(Debug, Serialize)]
739struct ChaosStatus {
740    enabled: bool,
741    active_scenarios: Vec<String>,
742    latency_enabled: bool,
743    fault_injection_enabled: bool,
744    rate_limit_enabled: bool,
745    traffic_shaping_enabled: bool,
746}
747
748#[derive(Debug, Deserialize)]
749struct GrpcStatusCodesRequest {
750    status_codes: Vec<i32>,
751    probability: f64,
752}
753
754#[derive(Debug, Deserialize)]
755struct WebSocketCloseCodesRequest {
756    close_codes: Vec<u16>,
757    probability: f64,
758}
759
760#[derive(Debug, Deserialize)]
761struct GraphQLErrorCodesRequest {
762    error_codes: Vec<String>,
763    probability: f64,
764}
765
766#[derive(Debug, Deserialize)]
767struct ProbabilityRequest {
768    probability: f64,
769}
770
771#[derive(Debug, Deserialize)]
772struct EnableRequest {
773    enabled: bool,
774}
775
776// Scenario management handlers
777
778/// Start recording a scenario
779async fn start_recording(
780    State(state): State<ChaosApiState>,
781    Json(req): Json<StartRecordingRequest>,
782) -> Result<Json<StatusResponse>, ChaosApiError> {
783    // Get the scenario based on name
784    let scenario = match req.scenario_name.as_str() {
785        "network_degradation" => PredefinedScenarios::network_degradation(),
786        "service_instability" => PredefinedScenarios::service_instability(),
787        "cascading_failure" => PredefinedScenarios::cascading_failure(),
788        "peak_traffic" => PredefinedScenarios::peak_traffic(),
789        "slow_backend" => PredefinedScenarios::slow_backend(),
790        _ => {
791            // Check if it's an active scenario
792            let active_scenarios = state.scenario_engine.get_active_scenarios();
793            active_scenarios
794                .into_iter()
795                .find(|s| s.name == req.scenario_name)
796                .ok_or_else(|| {
797                    ChaosApiError::NotFound(format!("Scenario '{}' not found", req.scenario_name))
798                })?
799        }
800    };
801
802    // Start recording
803    match state.recorder.start_recording(scenario) {
804        Ok(_) => {
805            info!("Recording started for scenario: {}", req.scenario_name);
806            Ok(Json(StatusResponse {
807                message: format!("Recording started for scenario: {}", req.scenario_name),
808            }))
809        }
810        Err(err) => Err(ChaosApiError::NotFound(err)),
811    }
812}
813
814/// Stop recording
815async fn stop_recording(
816    State(state): State<ChaosApiState>,
817) -> Result<Json<StatusResponse>, ChaosApiError> {
818    match state.recorder.stop_recording() {
819        Ok(recording) => {
820            info!(
821                "Recording stopped for scenario: {} ({} events)",
822                recording.scenario.name,
823                recording.events.len()
824            );
825            Ok(Json(StatusResponse {
826                message: format!(
827                    "Recording stopped for scenario: {} ({} events, {}ms)",
828                    recording.scenario.name,
829                    recording.events.len(),
830                    recording.total_duration_ms
831                ),
832            }))
833        }
834        Err(err) => Err(ChaosApiError::NotFound(err)),
835    }
836}
837
838/// Get recording status
839async fn recording_status(State(state): State<ChaosApiState>) -> Json<RecordingStatusResponse> {
840    if let Some(recording) = state.recorder.get_current_recording() {
841        Json(RecordingStatusResponse {
842            is_recording: true,
843            scenario_name: Some(recording.scenario.name),
844            events_recorded: recording.events.len(),
845        })
846    } else {
847        Json(RecordingStatusResponse {
848            is_recording: false,
849            scenario_name: None,
850            events_recorded: 0,
851        })
852    }
853}
854
855/// Export recording
856async fn export_recording(
857    State(state): State<ChaosApiState>,
858    Json(req): Json<ExportRequest>,
859) -> Result<Json<StatusResponse>, ChaosApiError> {
860    // Check if there's a current recording first
861    if state.recorder.get_current_recording().is_some() {
862        return Err(ChaosApiError::NotFound(
863            "Cannot export while recording is in progress. Stop recording first.".to_string(),
864        ));
865    }
866
867    // Get the most recent recording
868    let recordings = state.recorder.get_recordings();
869    if recordings.is_empty() {
870        return Err(ChaosApiError::NotFound("No recordings available to export".to_string()));
871    }
872
873    let recording = recordings.last().unwrap();
874
875    // Export to the specified path
876    match recording.save_to_file(&req.path) {
877        Ok(_) => {
878            info!("Recording exported to: {}", req.path);
879            Ok(Json(StatusResponse {
880                message: format!(
881                    "Recording exported to: {} ({} events)",
882                    req.path,
883                    recording.events.len()
884                ),
885            }))
886        }
887        Err(err) => Err(ChaosApiError::NotFound(format!("Failed to export recording: {}", err))),
888    }
889}
890
891/// Start replay
892async fn start_replay(
893    State(state): State<ChaosApiState>,
894    Json(req): Json<StartReplayRequest>,
895) -> Result<Json<StatusResponse>, ChaosApiError> {
896    // Load the recorded scenario from file
897    let recorded = RecordedScenario::load_from_file(&req.path)
898        .map_err(|e| ChaosApiError::NotFound(format!("Failed to load recording: {}", e)))?;
899
900    // Build replay options
901    let speed = match req.speed {
902        Some(s) if s > 0.0 => ReplaySpeed::Custom(s),
903        Some(0.0) => ReplaySpeed::Fast,
904        _ => ReplaySpeed::RealTime,
905    };
906
907    let options = ReplayOptions {
908        speed,
909        loop_replay: req.loop_replay.unwrap_or(false),
910        skip_initial_delay: false,
911        event_type_filter: None,
912    };
913
914    // Start replay
915    let mut replay_engine = state.replay_engine.write().await;
916    match replay_engine.replay(recorded.clone(), options).await {
917        Ok(_) => {
918            info!("Replay started for scenario: {}", recorded.scenario.name);
919            Ok(Json(StatusResponse {
920                message: format!(
921                    "Replay started for scenario: {} ({} events)",
922                    recorded.scenario.name,
923                    recorded.events.len()
924                ),
925            }))
926        }
927        Err(err) => Err(ChaosApiError::NotFound(err)),
928    }
929}
930
931/// Pause replay
932async fn pause_replay(
933    State(state): State<ChaosApiState>,
934) -> Result<Json<StatusResponse>, ChaosApiError> {
935    let replay_engine = state.replay_engine.read().await;
936    match replay_engine.pause().await {
937        Ok(_) => {
938            info!("Replay paused");
939            Ok(Json(StatusResponse {
940                message: "Replay paused".to_string(),
941            }))
942        }
943        Err(err) => Err(ChaosApiError::NotFound(err)),
944    }
945}
946
947/// Resume replay
948async fn resume_replay(
949    State(state): State<ChaosApiState>,
950) -> Result<Json<StatusResponse>, ChaosApiError> {
951    let replay_engine = state.replay_engine.read().await;
952    match replay_engine.resume().await {
953        Ok(_) => {
954            info!("Replay resumed");
955            Ok(Json(StatusResponse {
956                message: "Replay resumed".to_string(),
957            }))
958        }
959        Err(err) => Err(ChaosApiError::NotFound(err)),
960    }
961}
962
963/// Stop replay
964async fn stop_replay(
965    State(state): State<ChaosApiState>,
966) -> Result<Json<StatusResponse>, ChaosApiError> {
967    let replay_engine = state.replay_engine.read().await;
968    match replay_engine.stop().await {
969        Ok(_) => {
970            info!("Replay stopped");
971            Ok(Json(StatusResponse {
972                message: "Replay stopped".to_string(),
973            }))
974        }
975        Err(err) => Err(ChaosApiError::NotFound(err)),
976    }
977}
978
979/// Get replay status
980async fn replay_status(State(state): State<ChaosApiState>) -> Json<ReplayStatusResponse> {
981    let replay_engine = state.replay_engine.read().await;
982    if let Some(status) = replay_engine.get_status() {
983        Json(ReplayStatusResponse {
984            is_replaying: status.is_playing,
985            scenario_name: Some(status.scenario_name),
986            progress: status.progress,
987        })
988    } else {
989        Json(ReplayStatusResponse {
990            is_replaying: false,
991            scenario_name: None,
992            progress: 0.0,
993        })
994    }
995}
996
997/// Start orchestration
998async fn start_orchestration(
999    State(state): State<ChaosApiState>,
1000    Json(req): Json<OrchestratedScenarioRequest>,
1001) -> Result<Json<StatusResponse>, ChaosApiError> {
1002    use crate::scenario_orchestrator::ScenarioStep;
1003
1004    // Build orchestrated scenario from request
1005    let mut orchestrated = OrchestratedScenario::new(req.name.clone());
1006
1007    // Parse steps
1008    for step_value in req.steps {
1009        let step = serde_json::from_value::<ScenarioStep>(step_value)
1010            .map_err(|e| ChaosApiError::NotFound(format!("Invalid step: {}", e)))?;
1011        orchestrated = orchestrated.add_step(step);
1012    }
1013
1014    // Set parallel if specified
1015    if req.parallel.unwrap_or(false) {
1016        orchestrated = orchestrated.with_parallel_execution();
1017    }
1018
1019    // Start the orchestration
1020    let mut orchestrator = state.orchestrator.write().await;
1021    orchestrator
1022        .execute(orchestrated.clone())
1023        .await
1024        .map_err(|e| ChaosApiError::NotFound(format!("Failed to start orchestration: {}", e)))?;
1025
1026    info!("Started orchestration '{}'", req.name);
1027
1028    Ok(Json(StatusResponse {
1029        message: format!("Orchestration '{}' started successfully", req.name),
1030    }))
1031}
1032
1033/// Stop orchestration
1034async fn stop_orchestration(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
1035    let orchestrator = state.orchestrator.read().await;
1036
1037    if orchestrator.is_running() {
1038        // Note: ScenarioOrchestrator doesn't expose stop() publicly yet
1039        // This would require adding that method or using the control channel
1040        info!("Orchestration stop requested");
1041        Json(StatusResponse {
1042            message: "Orchestration stop requested (will complete current step)".to_string(),
1043        })
1044    } else {
1045        Json(StatusResponse {
1046            message: "No orchestration currently running".to_string(),
1047        })
1048    }
1049}
1050
1051/// Get orchestration status
1052async fn orchestration_status(
1053    State(state): State<ChaosApiState>,
1054) -> Json<OrchestrationStatusResponse> {
1055    let orchestrator = state.orchestrator.read().await;
1056
1057    if let Some(status) = orchestrator.get_status() {
1058        Json(OrchestrationStatusResponse {
1059            is_running: status.is_running,
1060            name: Some(status.name.clone()),
1061            progress: status.progress,
1062        })
1063    } else {
1064        Json(OrchestrationStatusResponse {
1065            is_running: false,
1066            name: None,
1067            progress: 0.0,
1068        })
1069    }
1070}
1071
1072/// Import orchestration from JSON/YAML
1073async fn import_orchestration(
1074    State(_state): State<ChaosApiState>,
1075    Json(req): Json<ImportRequest>,
1076) -> Result<Json<StatusResponse>, ChaosApiError> {
1077    // Parse based on format
1078    let orchestrated = if req.format == "json" {
1079        OrchestratedScenario::from_json(&req.content)
1080            .map_err(|e| ChaosApiError::NotFound(format!("Invalid JSON: {}", e)))?
1081    } else if req.format == "yaml" {
1082        OrchestratedScenario::from_yaml(&req.content)
1083            .map_err(|e| ChaosApiError::NotFound(format!("Invalid YAML: {}", e)))?
1084    } else {
1085        return Err(ChaosApiError::NotFound(
1086            "Unsupported format. Use 'json' or 'yaml'".to_string(),
1087        ));
1088    };
1089
1090    info!("Imported orchestration: {}", orchestrated.name);
1091
1092    Ok(Json(StatusResponse {
1093        message: format!(
1094            "Orchestration '{}' imported successfully ({} steps)",
1095            orchestrated.name,
1096            orchestrated.steps.len()
1097        ),
1098    }))
1099}
1100
1101/// Add a schedule
1102async fn add_schedule(
1103    State(state): State<ChaosApiState>,
1104    Json(req): Json<ScheduledScenarioRequest>,
1105) -> Result<Json<StatusResponse>, ChaosApiError> {
1106    // Parse scenario from JSON
1107    let scenario = serde_json::from_value::<ChaosScenario>(req.scenario)
1108        .map_err(|e| ChaosApiError::NotFound(format!("Invalid scenario: {}", e)))?;
1109
1110    // Parse schedule from JSON
1111    let schedule = serde_json::from_value::<ScheduleType>(req.schedule)
1112        .map_err(|e| ChaosApiError::NotFound(format!("Invalid schedule: {}", e)))?;
1113
1114    // Create scheduled scenario
1115    let scheduled = ScheduledScenario::new(req.id.clone(), scenario, schedule);
1116
1117    // Add to scheduler
1118    let scheduler = state.scheduler.read().await;
1119    scheduler.add_schedule(scheduled);
1120
1121    info!("Schedule '{}' added", req.id);
1122    Ok(Json(StatusResponse {
1123        message: format!("Schedule '{}' added", req.id),
1124    }))
1125}
1126
1127/// Get a schedule
1128async fn get_schedule(
1129    State(state): State<ChaosApiState>,
1130    Path(id): Path<String>,
1131) -> Result<Json<ScheduledScenario>, ChaosApiError> {
1132    let scheduler = state.scheduler.read().await;
1133    match scheduler.get_schedule(&id) {
1134        Some(scheduled) => Ok(Json(scheduled)),
1135        None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1136    }
1137}
1138
1139/// Remove a schedule
1140async fn remove_schedule(
1141    State(state): State<ChaosApiState>,
1142    Path(id): Path<String>,
1143) -> Result<Json<StatusResponse>, ChaosApiError> {
1144    let scheduler = state.scheduler.read().await;
1145    match scheduler.remove_schedule(&id) {
1146        Some(_) => {
1147            info!("Schedule '{}' removed", id);
1148            Ok(Json(StatusResponse {
1149                message: format!("Schedule '{}' removed", id),
1150            }))
1151        }
1152        None => Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id))),
1153    }
1154}
1155
1156/// Enable a schedule
1157async fn enable_schedule(
1158    State(state): State<ChaosApiState>,
1159    Path(id): Path<String>,
1160) -> Result<Json<StatusResponse>, ChaosApiError> {
1161    let scheduler = state.scheduler.read().await;
1162    match scheduler.enable_schedule(&id) {
1163        Ok(_) => {
1164            info!("Schedule '{}' enabled", id);
1165            Ok(Json(StatusResponse {
1166                message: format!("Schedule '{}' enabled", id),
1167            }))
1168        }
1169        Err(err) => Err(ChaosApiError::NotFound(err)),
1170    }
1171}
1172
1173/// Disable a schedule
1174async fn disable_schedule(
1175    State(state): State<ChaosApiState>,
1176    Path(id): Path<String>,
1177) -> Result<Json<StatusResponse>, ChaosApiError> {
1178    let scheduler = state.scheduler.read().await;
1179    match scheduler.disable_schedule(&id) {
1180        Ok(_) => {
1181            info!("Schedule '{}' disabled", id);
1182            Ok(Json(StatusResponse {
1183                message: format!("Schedule '{}' disabled", id),
1184            }))
1185        }
1186        Err(err) => Err(ChaosApiError::NotFound(err)),
1187    }
1188}
1189
1190/// Manually trigger a schedule (using Path parameter)
1191///
1192/// NOTE: This handler is fully implemented but cannot be registered as a route
1193/// due to a Rust/Axum type inference issue. The problem occurs when:
1194/// 1. A handler has State + Path/Json extractors
1195/// 2. The handler makes two consecutive `.await` calls:
1196///    - First await: acquiring the RwLock (`scheduler.read().await`)
1197///    - Second await: calling an async method (`trigger_now(&id).await`)
1198///
1199/// This causes Axum's Handler trait inference to fail with:
1200/// "the trait `Handler<_, _>` is not implemented for fn item..."
1201///
1202/// Root cause: Complex interaction between Rust's type inference, async/await
1203/// semantics, and Axum's Handler trait bounds when futures are composed.
1204///
1205/// Workarounds:
1206/// - Use the scheduler's automatic time-based execution
1207/// - Recreate the schedule to reset its execution state
1208/// - Call scheduler.trigger_now() directly from application code
1209#[allow(dead_code)]
1210async fn trigger_schedule_by_path(
1211    State(state): State<ChaosApiState>,
1212    Path(id): Path<String>,
1213) -> Result<Json<StatusResponse>, ChaosApiError> {
1214    let scheduler = state.scheduler.read().await;
1215    let schedule_exists = scheduler.get_schedule(&id).is_some();
1216
1217    if !schedule_exists {
1218        return Err(ChaosApiError::NotFound(format!("Schedule '{}' not found", id)));
1219    }
1220
1221    let trigger_result = scheduler.trigger_now(&id).await;
1222
1223    match trigger_result {
1224        Ok(_) => {
1225            info!("Schedule '{}' triggered", id);
1226            Ok(Json(StatusResponse {
1227                message: format!("Schedule '{}' triggered", id),
1228            }))
1229        }
1230        Err(err) => Err(ChaosApiError::NotFound(err)),
1231    }
1232}
1233
1234/// List all schedules
1235async fn list_schedules(State(state): State<ChaosApiState>) -> Json<Vec<ScheduleSummary>> {
1236    let scheduler = state.scheduler.read().await;
1237    let schedules = scheduler.get_all_schedules();
1238    let summaries = schedules
1239        .into_iter()
1240        .map(|s| ScheduleSummary {
1241            id: s.id,
1242            scenario_name: s.scenario.name,
1243            enabled: s.enabled,
1244            next_execution: s.next_execution.map(|t| t.to_rfc3339()),
1245        })
1246        .collect();
1247    Json(summaries)
1248}
1249
1250// Request/Response types for scenario management
1251
1252#[derive(Debug, Deserialize)]
1253struct StartRecordingRequest {
1254    scenario_name: String,
1255}
1256
1257#[derive(Debug, Deserialize)]
1258struct ExportRequest {
1259    path: String,
1260}
1261
1262#[derive(Debug, Serialize)]
1263struct RecordingStatusResponse {
1264    is_recording: bool,
1265    scenario_name: Option<String>,
1266    events_recorded: usize,
1267}
1268
1269#[derive(Debug, Deserialize)]
1270struct StartReplayRequest {
1271    path: String,
1272    speed: Option<f64>,
1273    loop_replay: Option<bool>,
1274}
1275
1276#[derive(Debug, Serialize)]
1277struct ReplayStatusResponse {
1278    is_replaying: bool,
1279    scenario_name: Option<String>,
1280    progress: f64,
1281}
1282
1283#[derive(Debug, Deserialize)]
1284struct OrchestratedScenarioRequest {
1285    name: String,
1286    steps: Vec<serde_json::Value>,
1287    parallel: Option<bool>,
1288}
1289
1290#[derive(Debug, Serialize)]
1291struct OrchestrationStatusResponse {
1292    is_running: bool,
1293    name: Option<String>,
1294    progress: f64,
1295}
1296
1297#[derive(Debug, Deserialize)]
1298struct ImportRequest {
1299    content: String,
1300    format: String, // json or yaml
1301}
1302
1303#[derive(Debug, Deserialize)]
1304struct ScheduledScenarioRequest {
1305    id: String,
1306    scenario: serde_json::Value,
1307    schedule: serde_json::Value,
1308}
1309
1310#[derive(Debug, Deserialize, Serialize)]
1311struct ScheduleSummary {
1312    id: String,
1313    scenario_name: String,
1314    enabled: bool,
1315    next_execution: Option<String>,
1316}
1317
1318// AI-powered recommendation handlers
1319
1320/// Get all recommendations
1321async fn get_recommendations(
1322    State(state): State<ChaosApiState>,
1323) -> Json<Vec<crate::recommendations::Recommendation>> {
1324    Json(state.recommendation_engine.get_recommendations())
1325}
1326
1327/// Analyze metrics and generate recommendations
1328async fn analyze_and_recommend(State(state): State<ChaosApiState>) -> Json<AnalyzeResponse> {
1329    use chrono::{Duration, Utc};
1330
1331    // Get metrics from last 24 hours
1332    let end = Utc::now();
1333    let start = end - Duration::hours(24);
1334
1335    let buckets = state.analytics.get_metrics(start, end, TimeBucket::Hour);
1336    let impact = state.analytics.get_impact_analysis(start, end, TimeBucket::Hour);
1337
1338    let recommendations = state.recommendation_engine.analyze_and_recommend(&buckets, &impact);
1339
1340    Json(AnalyzeResponse {
1341        total_recommendations: recommendations.len(),
1342        high_priority: recommendations
1343            .iter()
1344            .filter(|r| {
1345                matches!(
1346                    r.severity,
1347                    RecommendationSeverity::High | RecommendationSeverity::Critical
1348                )
1349            })
1350            .count(),
1351        recommendations,
1352    })
1353}
1354
1355/// Get recommendations by category
1356async fn get_recommendations_by_category(
1357    State(state): State<ChaosApiState>,
1358    Path(category): Path<String>,
1359) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1360    let category = match category.as_str() {
1361        "latency" => RecommendationCategory::Latency,
1362        "fault_injection" => RecommendationCategory::FaultInjection,
1363        "rate_limit" => RecommendationCategory::RateLimit,
1364        "traffic_shaping" => RecommendationCategory::TrafficShaping,
1365        "circuit_breaker" => RecommendationCategory::CircuitBreaker,
1366        "bulkhead" => RecommendationCategory::Bulkhead,
1367        "scenario" => RecommendationCategory::Scenario,
1368        "coverage" => RecommendationCategory::Coverage,
1369        _ => return Err(StatusCode::BAD_REQUEST),
1370    };
1371
1372    Ok(Json(state.recommendation_engine.get_recommendations_by_category(category)))
1373}
1374
1375/// Get recommendations by severity
1376async fn get_recommendations_by_severity(
1377    State(state): State<ChaosApiState>,
1378    Path(severity): Path<String>,
1379) -> Result<Json<Vec<crate::recommendations::Recommendation>>, StatusCode> {
1380    let severity = match severity.as_str() {
1381        "info" => RecommendationSeverity::Info,
1382        "low" => RecommendationSeverity::Low,
1383        "medium" => RecommendationSeverity::Medium,
1384        "high" => RecommendationSeverity::High,
1385        "critical" => RecommendationSeverity::Critical,
1386        _ => return Err(StatusCode::BAD_REQUEST),
1387    };
1388
1389    Ok(Json(state.recommendation_engine.get_recommendations_by_severity(severity)))
1390}
1391
1392/// Clear all recommendations
1393async fn clear_recommendations(State(state): State<ChaosApiState>) -> Json<StatusResponse> {
1394    state.recommendation_engine.clear();
1395    Json(StatusResponse {
1396        message: "Recommendations cleared".to_string(),
1397    })
1398}
1399
1400#[derive(Debug, Serialize)]
1401struct AnalyzeResponse {
1402    total_recommendations: usize,
1403    high_priority: usize,
1404    recommendations: Vec<crate::recommendations::Recommendation>,
1405}
1406
1407// Auto-remediation endpoints
1408
1409/// Get remediation configuration
1410async fn get_remediation_config(State(state): State<ChaosApiState>) -> Json<RemediationConfig> {
1411    Json(state.remediation_engine.get_config())
1412}
1413
1414/// Update remediation configuration
1415async fn update_remediation_config(
1416    State(state): State<ChaosApiState>,
1417    Json(config): Json<RemediationConfig>,
1418) -> Json<StatusResponse> {
1419    state.remediation_engine.update_config(config);
1420    Json(StatusResponse {
1421        message: "Remediation configuration updated".to_string(),
1422    })
1423}
1424
1425#[derive(Debug, Deserialize)]
1426struct ProcessRemediationRequest {
1427    recommendation: Recommendation,
1428}
1429
1430/// Process a recommendation for auto-remediation
1431async fn process_remediation(
1432    State(state): State<ChaosApiState>,
1433    Json(req): Json<ProcessRemediationRequest>,
1434) -> Result<Json<serde_json::Value>, StatusCode> {
1435    match state.remediation_engine.process_recommendation(&req.recommendation) {
1436        Ok(action_id) => Ok(Json(serde_json::json!({
1437            "success": true,
1438            "action_id": action_id,
1439            "message": "Recommendation processed"
1440        }))),
1441        Err(err) => Ok(Json(serde_json::json!({
1442            "success": false,
1443            "error": err
1444        }))),
1445    }
1446}
1447
1448#[derive(Debug, Deserialize)]
1449struct ApproveRequest {
1450    approver: String,
1451}
1452
1453/// Approve a remediation action
1454async fn approve_remediation(
1455    State(state): State<ChaosApiState>,
1456    Path(id): Path<String>,
1457    Json(req): Json<ApproveRequest>,
1458) -> Result<Json<StatusResponse>, StatusCode> {
1459    match state.remediation_engine.approve_action(&id, &req.approver) {
1460        Ok(_) => Ok(Json(StatusResponse {
1461            message: format!("Action {} approved", id),
1462        })),
1463        Err(_err) => Err(StatusCode::BAD_REQUEST),
1464    }
1465}
1466
1467#[derive(Debug, Deserialize)]
1468struct RejectRequest {
1469    reason: String,
1470}
1471
1472/// Reject a remediation action
1473async fn reject_remediation(
1474    State(state): State<ChaosApiState>,
1475    Path(id): Path<String>,
1476    Json(req): Json<RejectRequest>,
1477) -> Result<Json<StatusResponse>, StatusCode> {
1478    match state.remediation_engine.reject_action(&id, &req.reason) {
1479        Ok(_) => Ok(Json(StatusResponse {
1480            message: format!("Action {} rejected", id),
1481        })),
1482        Err(_err) => Err(StatusCode::BAD_REQUEST),
1483    }
1484}
1485
1486/// Rollback a remediation action
1487async fn rollback_remediation(
1488    State(state): State<ChaosApiState>,
1489    Path(id): Path<String>,
1490) -> Result<Json<StatusResponse>, StatusCode> {
1491    match state.remediation_engine.rollback_action(&id) {
1492        Ok(_) => Ok(Json(StatusResponse {
1493            message: format!("Action {} rolled back", id),
1494        })),
1495        Err(_err) => Err(StatusCode::BAD_REQUEST),
1496    }
1497}
1498
1499/// Get all remediation actions
1500async fn get_remediation_actions(
1501    State(state): State<ChaosApiState>,
1502) -> Json<Vec<crate::auto_remediation::RemediationAction>> {
1503    Json(state.remediation_engine.get_active_actions())
1504}
1505
1506/// Get a specific remediation action
1507async fn get_remediation_action(
1508    State(state): State<ChaosApiState>,
1509    Path(id): Path<String>,
1510) -> Result<Json<crate::auto_remediation::RemediationAction>, StatusCode> {
1511    match state.remediation_engine.get_action(&id) {
1512        Some(action) => Ok(Json(action)),
1513        None => Err(StatusCode::NOT_FOUND),
1514    }
1515}
1516
1517/// Get approval queue
1518async fn get_approval_queue(
1519    State(state): State<ChaosApiState>,
1520) -> Json<Vec<crate::auto_remediation::ApprovalRequest>> {
1521    Json(state.remediation_engine.get_approval_queue())
1522}
1523
1524/// Get effectiveness metrics for an action
1525async fn get_remediation_effectiveness(
1526    State(state): State<ChaosApiState>,
1527    Path(id): Path<String>,
1528) -> Result<Json<crate::auto_remediation::EffectivenessMetrics>, StatusCode> {
1529    match state.remediation_engine.get_effectiveness(&id) {
1530        Some(metrics) => Ok(Json(metrics)),
1531        None => Err(StatusCode::NOT_FOUND),
1532    }
1533}
1534
1535/// Get remediation statistics
1536async fn get_remediation_stats(
1537    State(state): State<ChaosApiState>,
1538) -> Json<crate::auto_remediation::RemediationStats> {
1539    Json(state.remediation_engine.get_stats())
1540}
1541
1542// A/B testing endpoints
1543
1544/// Create a new A/B test
1545async fn create_ab_test(
1546    State(state): State<ChaosApiState>,
1547    Json(config): Json<ABTestConfig>,
1548) -> Result<Json<serde_json::Value>, StatusCode> {
1549    let engine = state.ab_testing_engine.read().await;
1550    match engine.create_test(config) {
1551        Ok(test_id) => Ok(Json(serde_json::json!({
1552            "success": true,
1553            "test_id": test_id
1554        }))),
1555        Err(err) => Ok(Json(serde_json::json!({
1556            "success": false,
1557            "error": err
1558        }))),
1559    }
1560}
1561
1562/// Get all A/B tests
1563async fn get_ab_tests(State(state): State<ChaosApiState>) -> Json<Vec<crate::ab_testing::ABTest>> {
1564    let engine = state.ab_testing_engine.read().await;
1565    Json(engine.get_all_tests())
1566}
1567
1568/// Get a specific A/B test
1569async fn get_ab_test(
1570    State(state): State<ChaosApiState>,
1571    Path(id): Path<String>,
1572) -> Result<Json<crate::ab_testing::ABTest>, StatusCode> {
1573    let engine = state.ab_testing_engine.read().await;
1574    match engine.get_test(&id) {
1575        Some(test) => Ok(Json(test)),
1576        None => Err(StatusCode::NOT_FOUND),
1577    }
1578}
1579
1580/// Start an A/B test
1581async fn start_ab_test(
1582    State(state): State<ChaosApiState>,
1583    Path(id): Path<String>,
1584) -> Result<Json<StatusResponse>, StatusCode> {
1585    let engine = state.ab_testing_engine.read().await;
1586    match engine.start_test(&id) {
1587        Ok(_) => Ok(Json(StatusResponse {
1588            message: format!("Test {} started", id),
1589        })),
1590        Err(_err) => Err(StatusCode::BAD_REQUEST),
1591    }
1592}
1593
1594/// Stop an A/B test
1595async fn stop_ab_test(
1596    State(state): State<ChaosApiState>,
1597    Path(id): Path<String>,
1598) -> Result<Json<TestConclusion>, StatusCode> {
1599    let engine = state.ab_testing_engine.read().await;
1600    match engine.stop_test(&id) {
1601        Ok(conclusion) => Ok(Json(conclusion)),
1602        Err(_err) => Err(StatusCode::BAD_REQUEST),
1603    }
1604}
1605
1606/// Pause an A/B test
1607async fn pause_ab_test(
1608    State(state): State<ChaosApiState>,
1609    Path(id): Path<String>,
1610) -> Result<Json<StatusResponse>, StatusCode> {
1611    let engine = state.ab_testing_engine.read().await;
1612    match engine.pause_test(&id) {
1613        Ok(_) => Ok(Json(StatusResponse {
1614            message: format!("Test {} paused", id),
1615        })),
1616        Err(_err) => Err(StatusCode::BAD_REQUEST),
1617    }
1618}
1619
1620/// Resume an A/B test
1621async fn resume_ab_test(
1622    State(state): State<ChaosApiState>,
1623    Path(id): Path<String>,
1624) -> Result<Json<StatusResponse>, StatusCode> {
1625    let engine = state.ab_testing_engine.read().await;
1626    match engine.resume_test(&id) {
1627        Ok(_) => Ok(Json(StatusResponse {
1628            message: format!("Test {} resumed", id),
1629        })),
1630        Err(_err) => Err(StatusCode::BAD_REQUEST),
1631    }
1632}
1633
1634/// Record variant results
1635async fn record_ab_test_result(
1636    State(state): State<ChaosApiState>,
1637    Path((id, variant)): Path<(String, String)>,
1638    Json(results): Json<VariantResults>,
1639) -> Result<Json<StatusResponse>, StatusCode> {
1640    let engine = state.ab_testing_engine.read().await;
1641    match engine.record_variant_result(&id, &variant, results) {
1642        Ok(_) => Ok(Json(StatusResponse {
1643            message: format!("Results recorded for variant {}", variant),
1644        })),
1645        Err(_err) => Err(StatusCode::BAD_REQUEST),
1646    }
1647}
1648
1649/// Delete an A/B test
1650async fn delete_ab_test(
1651    State(state): State<ChaosApiState>,
1652    Path(id): Path<String>,
1653) -> Result<Json<StatusResponse>, StatusCode> {
1654    let engine = state.ab_testing_engine.read().await;
1655    match engine.delete_test(&id) {
1656        Ok(_) => Ok(Json(StatusResponse {
1657            message: format!("Test {} deleted", id),
1658        })),
1659        Err(_err) => Err(StatusCode::BAD_REQUEST),
1660    }
1661}
1662
1663/// Get A/B test statistics
1664async fn get_ab_test_stats(
1665    State(state): State<ChaosApiState>,
1666) -> Json<crate::ab_testing::ABTestStats> {
1667    let engine = state.ab_testing_engine.read().await;
1668    Json(engine.get_stats())
1669}
1670
1671// Latency metrics endpoints
1672
1673/// Get latency metrics (time-series data)
1674async fn get_latency_metrics(State(state): State<ChaosApiState>) -> Json<LatencyMetricsResponse> {
1675    let samples = state.latency_tracker.get_samples();
1676    Json(LatencyMetricsResponse { samples })
1677}
1678
1679/// Get latency statistics
1680async fn get_latency_stats(
1681    State(state): State<ChaosApiState>,
1682) -> Json<crate::latency_metrics::LatencyStats> {
1683    let stats = state.latency_tracker.get_stats();
1684    Json(stats)
1685}
1686
1687#[derive(Debug, Serialize)]
1688struct LatencyMetricsResponse {
1689    samples: Vec<crate::latency_metrics::LatencySample>,
1690}
1691
1692// Profile management endpoints
1693
1694/// List all profiles (built-in + custom)
1695async fn list_profiles(State(state): State<ChaosApiState>) -> Json<Vec<NetworkProfile>> {
1696    let profiles = state.profile_manager.get_all_profiles();
1697    Json(profiles)
1698}
1699
1700/// Get a specific profile by name
1701async fn get_profile(
1702    State(state): State<ChaosApiState>,
1703    Path(name): Path<String>,
1704) -> Result<Json<NetworkProfile>, ChaosApiError> {
1705    match state.profile_manager.get_profile(&name) {
1706        Some(profile) => Ok(Json(profile)),
1707        None => Err(ChaosApiError::NotFound(format!("Profile '{}' not found", name))),
1708    }
1709}
1710
1711/// Apply a profile (update chaos config)
1712async fn apply_profile(
1713    State(state): State<ChaosApiState>,
1714    Path(name): Path<String>,
1715) -> Result<Json<StatusResponse>, ChaosApiError> {
1716    let profile = state
1717        .profile_manager
1718        .get_profile(&name)
1719        .ok_or_else(|| ChaosApiError::NotFound(format!("Profile '{}' not found", name)))?;
1720
1721    // Apply the profile's chaos config
1722    let mut config = state.config.write().await;
1723    *config = profile.chaos_config.clone();
1724
1725    info!("Applied profile: {}", name);
1726    Ok(Json(StatusResponse {
1727        message: format!("Profile '{}' applied successfully", name),
1728    }))
1729}
1730
1731/// Create a new custom profile
1732async fn create_profile(
1733    State(state): State<ChaosApiState>,
1734    Json(profile): Json<NetworkProfile>,
1735) -> Result<Json<StatusResponse>, ChaosApiError> {
1736    // Check if it's a built-in profile name
1737    for builtin in NetworkProfile::predefined_profiles() {
1738        if builtin.name == profile.name {
1739            return Err(ChaosApiError::NotFound(format!(
1740                "Cannot create profile '{}': name conflicts with built-in profile",
1741                profile.name
1742            )));
1743        }
1744    }
1745
1746    // Mark as custom
1747    let mut custom_profile = profile;
1748    custom_profile.builtin = false;
1749
1750    state.profile_manager.save_profile(custom_profile.clone());
1751    info!("Created custom profile: {}", custom_profile.name);
1752    Ok(Json(StatusResponse {
1753        message: format!("Profile '{}' created successfully", custom_profile.name),
1754    }))
1755}
1756
1757/// Delete a custom profile
1758async fn delete_profile(
1759    State(state): State<ChaosApiState>,
1760    Path(name): Path<String>,
1761) -> Result<Json<StatusResponse>, ChaosApiError> {
1762    // Check if it's a built-in profile
1763    for builtin in NetworkProfile::predefined_profiles() {
1764        if builtin.name == name {
1765            return Err(ChaosApiError::NotFound(format!(
1766                "Cannot delete built-in profile '{}'",
1767                name
1768            )));
1769        }
1770    }
1771
1772    if state.profile_manager.delete_profile(&name) {
1773        info!("Deleted custom profile: {}", name);
1774        Ok(Json(StatusResponse {
1775            message: format!("Profile '{}' deleted successfully", name),
1776        }))
1777    } else {
1778        Err(ChaosApiError::NotFound(format!("Profile '{}' not found", name)))
1779    }
1780}
1781
1782/// Export a profile as JSON or YAML
1783async fn export_profile(
1784    State(state): State<ChaosApiState>,
1785    Path(name): Path<String>,
1786    Query(params): Query<std::collections::HashMap<String, String>>,
1787) -> Result<Response, ChaosApiError> {
1788    let profile = state
1789        .profile_manager
1790        .get_profile(&name)
1791        .ok_or_else(|| ChaosApiError::NotFound(format!("Profile '{}' not found", name)))?;
1792
1793    let format = params.get("format").map(|s| s.as_str()).unwrap_or("json");
1794
1795    if format == "yaml" {
1796        let yaml = serde_yaml::to_string(&profile).map_err(|e| {
1797            ChaosApiError::NotFound(format!("Failed to serialize profile to YAML: {}", e))
1798        })?;
1799        Ok(Response::builder()
1800            .status(StatusCode::OK)
1801            .header("Content-Type", "application/yaml")
1802            .body(axum::body::Body::from(yaml))
1803            .unwrap()
1804            .into_response())
1805    } else {
1806        // Default to JSON
1807        let json = serde_json::to_value(&profile)
1808            .map_err(|e| ChaosApiError::NotFound(format!("Failed to serialize profile: {}", e)))?;
1809        Ok(Json(json).into_response())
1810    }
1811}
1812
1813/// Import a profile from JSON or YAML
1814async fn import_profile(
1815    State(state): State<ChaosApiState>,
1816    Json(req): Json<ImportProfileRequest>,
1817) -> Result<Json<StatusResponse>, ChaosApiError> {
1818    let profile: NetworkProfile = if req.format == "yaml" {
1819        serde_yaml::from_str(&req.content)
1820            .map_err(|e| ChaosApiError::NotFound(format!("Failed to parse YAML: {}", e)))?
1821    } else {
1822        serde_json::from_str(&req.content)
1823            .map_err(|e| ChaosApiError::NotFound(format!("Failed to parse JSON: {}", e)))?
1824    };
1825
1826    // Check if it's a built-in profile name
1827    for builtin in NetworkProfile::predefined_profiles() {
1828        if builtin.name == profile.name {
1829            return Err(ChaosApiError::NotFound(format!(
1830                "Cannot import profile '{}': name conflicts with built-in profile",
1831                profile.name
1832            )));
1833        }
1834    }
1835
1836    // Mark as custom
1837    let mut custom_profile = profile;
1838    custom_profile.builtin = false;
1839
1840    state.profile_manager.save_profile(custom_profile.clone());
1841    info!("Imported profile: {}", custom_profile.name);
1842    Ok(Json(StatusResponse {
1843        message: format!("Profile '{}' imported successfully", custom_profile.name),
1844    }))
1845}
1846
1847#[derive(Debug, Deserialize)]
1848struct ImportProfileRequest {
1849    content: String,
1850    format: String, // "json" or "yaml"
1851}
1852
1853// Error handling
1854
1855#[derive(Debug)]
1856enum ChaosApiError {
1857    NotFound(String),
1858}
1859
1860impl IntoResponse for ChaosApiError {
1861    fn into_response(self) -> Response {
1862        let (status, message) = match self {
1863            ChaosApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
1864        };
1865
1866        (status, Json(serde_json::json!({ "error": message }))).into_response()
1867    }
1868}