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