1use axum::{
6 extract::{Path, Query, State},
7 http::StatusCode,
8 response::Json,
9};
10use mockforge_core::consistency::{
12 enrich_order_response, enrich_response_via_graph, enrich_user_response,
13 get_user_orders_via_graph, ConsistencyEngine, EntityState, UnifiedState,
14};
15use mockforge_core::reality::RealityLevel;
16use mockforge_data::{LifecycleState, PersonaLifecycle, PersonaProfile};
17use serde::{Deserialize, Serialize};
18use serde_json::Value as JsonValue;
19use serde_json::Value;
20use std::collections::HashMap;
21use std::sync::Arc;
22use tracing::{error, info};
23
24#[derive(Clone)]
26pub struct ConsistencyState {
27 pub engine: Arc<ConsistencyEngine>,
29}
30
31#[derive(Debug, Deserialize)]
33pub struct SetPersonaRequest {
34 pub persona: PersonaProfile,
36}
37
38#[derive(Debug, Deserialize)]
40pub struct SetScenarioRequest {
41 pub scenario_id: String,
43}
44
45#[derive(Debug, Deserialize)]
47pub struct SetRealityLevelRequest {
48 pub level: u8,
50}
51
52#[derive(Debug, Deserialize)]
54pub struct SetRealityRatioRequest {
55 pub ratio: f64,
57}
58
59#[derive(Debug, Deserialize)]
61pub struct RegisterEntityRequest {
62 pub entity_type: String,
64 pub entity_id: String,
66 pub data: Value,
68 pub persona_id: Option<String>,
70}
71
72#[derive(Debug, Deserialize)]
74pub struct ActivateChaosRuleRequest {
75 pub rule: JsonValue, }
78
79#[derive(Debug, Deserialize)]
81pub struct DeactivateChaosRuleRequest {
82 pub rule_name: String,
84}
85
86#[derive(Debug, Deserialize)]
88pub struct SetPersonaLifecycleRequest {
89 pub persona_id: String,
91 pub initial_state: String,
93}
94
95#[derive(Debug, Deserialize)]
97pub struct WorkspaceQuery {
98 #[serde(default = "default_workspace")]
100 pub workspace: String,
101}
102
103fn default_workspace() -> String {
104 "default".to_string()
105}
106
107pub async fn get_state(
111 State(state): State<ConsistencyState>,
112 Query(params): Query<WorkspaceQuery>,
113) -> Result<Json<UnifiedState>, StatusCode> {
114 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
115 error!("State not found for workspace: {}", params.workspace);
116 StatusCode::NOT_FOUND
117 })?;
118
119 Ok(Json(unified_state))
120}
121
122pub async fn set_persona(
126 State(state): State<ConsistencyState>,
127 Query(params): Query<WorkspaceQuery>,
128 Json(request): Json<SetPersonaRequest>,
129) -> Result<Json<Value>, StatusCode> {
130 state
131 .engine
132 .set_active_persona(¶ms.workspace, request.persona)
133 .await
134 .map_err(|e| {
135 error!("Failed to set persona: {}", e);
136 StatusCode::INTERNAL_SERVER_ERROR
137 })?;
138
139 info!("Set persona for workspace: {}", params.workspace);
140 Ok(Json(serde_json::json!({
141 "success": true,
142 "workspace": params.workspace,
143 })))
144}
145
146pub async fn set_scenario(
150 State(state): State<ConsistencyState>,
151 Query(params): Query<WorkspaceQuery>,
152 Json(request): Json<SetScenarioRequest>,
153) -> Result<Json<Value>, StatusCode> {
154 state
155 .engine
156 .set_active_scenario(¶ms.workspace, request.scenario_id)
157 .await
158 .map_err(|e| {
159 error!("Failed to set scenario: {}", e);
160 StatusCode::INTERNAL_SERVER_ERROR
161 })?;
162
163 info!("Set scenario for workspace: {}", params.workspace);
164 Ok(Json(serde_json::json!({
165 "success": true,
166 "workspace": params.workspace,
167 })))
168}
169
170pub async fn set_reality_level(
174 State(state): State<ConsistencyState>,
175 Query(params): Query<WorkspaceQuery>,
176 Json(request): Json<SetRealityLevelRequest>,
177) -> Result<Json<Value>, StatusCode> {
178 let level = RealityLevel::from_value(request.level).ok_or_else(|| {
179 error!("Invalid reality level: {}", request.level);
180 StatusCode::BAD_REQUEST
181 })?;
182
183 state.engine.set_reality_level(¶ms.workspace, level).await.map_err(|e| {
184 error!("Failed to set reality level: {}", e);
185 StatusCode::INTERNAL_SERVER_ERROR
186 })?;
187
188 info!("Set reality level for workspace: {}", params.workspace);
189 Ok(Json(serde_json::json!({
190 "success": true,
191 "workspace": params.workspace,
192 "level": request.level,
193 })))
194}
195
196pub async fn set_reality_ratio(
200 State(state): State<ConsistencyState>,
201 Query(params): Query<WorkspaceQuery>,
202 Json(request): Json<SetRealityRatioRequest>,
203) -> Result<Json<Value>, StatusCode> {
204 if !(0.0..=1.0).contains(&request.ratio) {
205 return Err(StatusCode::BAD_REQUEST);
206 }
207
208 state
209 .engine
210 .set_reality_ratio(¶ms.workspace, request.ratio)
211 .await
212 .map_err(|e| {
213 error!("Failed to set reality ratio: {}", e);
214 StatusCode::INTERNAL_SERVER_ERROR
215 })?;
216
217 info!("Set reality ratio for workspace: {}", params.workspace);
218 Ok(Json(serde_json::json!({
219 "success": true,
220 "workspace": params.workspace,
221 "ratio": request.ratio,
222 })))
223}
224
225pub async fn register_entity(
229 State(state): State<ConsistencyState>,
230 Query(params): Query<WorkspaceQuery>,
231 Json(request): Json<RegisterEntityRequest>,
232) -> Result<Json<Value>, StatusCode> {
233 let mut entity = EntityState::new(request.entity_type, request.entity_id, request.data);
234 if let Some(persona_id) = request.persona_id {
235 entity.persona_id = Some(persona_id);
236 }
237
238 state
239 .engine
240 .register_entity(¶ms.workspace, entity.clone())
241 .await
242 .map_err(|e| {
243 error!("Failed to register entity: {}", e);
244 StatusCode::INTERNAL_SERVER_ERROR
245 })?;
246
247 info!(
248 "Registered entity {}:{} for workspace: {}",
249 entity.entity_type, entity.entity_id, params.workspace
250 );
251 Ok(Json(serde_json::json!({
252 "success": true,
253 "workspace": params.workspace,
254 "entity": entity,
255 })))
256}
257
258pub async fn get_entity(
262 State(state): State<ConsistencyState>,
263 Path((entity_type, entity_id)): Path<(String, String)>,
264 Query(params): Query<WorkspaceQuery>,
265) -> Result<Json<EntityState>, StatusCode> {
266 let entity = state
267 .engine
268 .get_entity(¶ms.workspace, &entity_type, &entity_id)
269 .await
270 .ok_or_else(|| {
271 error!(
272 "Entity not found: {}:{} in workspace: {}",
273 entity_type, entity_id, params.workspace
274 );
275 StatusCode::NOT_FOUND
276 })?;
277
278 Ok(Json(entity))
279}
280
281pub async fn list_entities(
285 State(state): State<ConsistencyState>,
286 Query(params): Query<WorkspaceQuery>,
287) -> Result<Json<Value>, StatusCode> {
288 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
289 error!("State not found for workspace: {}", params.workspace);
290 StatusCode::NOT_FOUND
291 })?;
292
293 let entities: Vec<&EntityState> = unified_state.entity_state.values().collect();
294 Ok(Json(serde_json::json!({
295 "workspace": params.workspace,
296 "entities": entities,
297 "count": entities.len(),
298 })))
299}
300
301pub async fn activate_chaos_rule(
305 State(state): State<ConsistencyState>,
306 Query(params): Query<WorkspaceQuery>,
307 Json(request): Json<ActivateChaosRuleRequest>,
308) -> Result<Json<Value>, StatusCode> {
309 state
310 .engine
311 .activate_chaos_rule(¶ms.workspace, request.rule)
312 .await
313 .map_err(|e| {
314 error!("Failed to activate chaos rule: {}", e);
315 StatusCode::INTERNAL_SERVER_ERROR
316 })?;
317
318 info!("Activated chaos rule for workspace: {}", params.workspace);
319 Ok(Json(serde_json::json!({
320 "success": true,
321 "workspace": params.workspace,
322 })))
323}
324
325pub async fn deactivate_chaos_rule(
329 State(state): State<ConsistencyState>,
330 Query(params): Query<WorkspaceQuery>,
331 Json(request): Json<DeactivateChaosRuleRequest>,
332) -> Result<Json<Value>, StatusCode> {
333 state
334 .engine
335 .deactivate_chaos_rule(¶ms.workspace, &request.rule_name)
336 .await
337 .map_err(|e| {
338 error!("Failed to deactivate chaos rule: {}", e);
339 StatusCode::INTERNAL_SERVER_ERROR
340 })?;
341
342 info!("Deactivated chaos rule for workspace: {}", params.workspace);
343 Ok(Json(serde_json::json!({
344 "success": true,
345 "workspace": params.workspace,
346 "rule_name": request.rule_name,
347 })))
348}
349
350pub async fn set_persona_lifecycle(
354 State(state): State<ConsistencyState>,
355 Query(params): Query<WorkspaceQuery>,
356 Json(request): Json<SetPersonaLifecycleRequest>,
357) -> Result<Json<Value>, StatusCode> {
358 let lifecycle_state = match request.initial_state.to_lowercase().as_str() {
360 "new" | "new_signup" => LifecycleState::NewSignup,
361 "active" => LifecycleState::Active,
362 "power_user" | "poweruser" => LifecycleState::PowerUser,
363 "churn_risk" | "churnrisk" => LifecycleState::ChurnRisk,
364 "churned" => LifecycleState::Churned,
365 "upgrade_pending" | "upgradepending" => LifecycleState::UpgradePending,
366 "payment_failed" | "paymentfailed" => LifecycleState::PaymentFailed,
367 _ => {
368 error!("Invalid lifecycle state: {}", request.initial_state);
369 return Err(StatusCode::BAD_REQUEST);
370 }
371 };
372
373 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
375 error!("State not found for workspace: {}", params.workspace);
376 StatusCode::NOT_FOUND
377 })?;
378
379 if let Some(ref persona) = unified_state.active_persona {
381 if persona.id == request.persona_id {
382 let mut persona_mut = persona.clone();
383 let lifecycle = PersonaLifecycle::new(request.persona_id.clone(), lifecycle_state);
384 persona_mut.set_lifecycle(lifecycle);
385
386 if let Some(ref lifecycle) = persona_mut.lifecycle {
388 let effects = lifecycle.apply_lifecycle_effects();
389 for (key, value) in effects {
390 persona_mut.set_trait(key, value);
391 }
392 }
393
394 state
396 .engine
397 .set_active_persona(¶ms.workspace, persona_mut)
398 .await
399 .map_err(|e| {
400 error!("Failed to set persona lifecycle: {}", e);
401 StatusCode::INTERNAL_SERVER_ERROR
402 })?;
403
404 info!(
405 "Set lifecycle state {} for persona {} in workspace: {}",
406 request.initial_state, request.persona_id, params.workspace
407 );
408
409 return Ok(Json(serde_json::json!({
410 "success": true,
411 "workspace": params.workspace,
412 "persona_id": request.persona_id,
413 "lifecycle_state": request.initial_state,
414 })));
415 }
416 }
417
418 error!(
419 "Persona {} not found or not active in workspace: {}",
420 request.persona_id, params.workspace
421 );
422 Err(StatusCode::NOT_FOUND)
423}
424
425pub async fn get_user_with_graph(
430 State(state): State<ConsistencyState>,
431 Path(user_id): Path<String>,
432 Query(params): Query<WorkspaceQuery>,
433) -> Result<Json<Value>, StatusCode> {
434 let mut user_entity = state
436 .engine
437 .get_entity(¶ms.workspace, "user", &user_id)
438 .await
439 .ok_or_else(|| {
440 error!("User not found: {} in workspace: {}", user_id, params.workspace);
441 StatusCode::NOT_FOUND
442 })?;
443
444 let mut response = user_entity.data.clone();
446 enrich_user_response(&state.engine, ¶ms.workspace, &user_id, &mut response).await;
447
448 Ok(Json(response))
449}
450
451pub async fn get_user_orders_with_graph(
456 State(state): State<ConsistencyState>,
457 Path(user_id): Path<String>,
458 Query(params): Query<WorkspaceQuery>,
459) -> Result<Json<Value>, StatusCode> {
460 state
462 .engine
463 .get_entity(¶ms.workspace, "user", &user_id)
464 .await
465 .ok_or_else(|| {
466 error!("User not found: {} in workspace: {}", user_id, params.workspace);
467 StatusCode::NOT_FOUND
468 })?;
469
470 let orders = get_user_orders_via_graph(&state.engine, ¶ms.workspace, &user_id).await;
472
473 let orders_json: Vec<Value> = orders.iter().map(|e| e.data.clone()).collect();
475
476 Ok(Json(serde_json::json!({
477 "user_id": user_id,
478 "orders": orders_json,
479 "count": orders_json.len(),
480 })))
481}
482
483pub async fn get_order_with_graph(
488 State(state): State<ConsistencyState>,
489 Path(order_id): Path<String>,
490 Query(params): Query<WorkspaceQuery>,
491) -> Result<Json<Value>, StatusCode> {
492 let mut order_entity = state
494 .engine
495 .get_entity(¶ms.workspace, "order", &order_id)
496 .await
497 .ok_or_else(|| {
498 error!("Order not found: {} in workspace: {}", order_id, params.workspace);
499 StatusCode::NOT_FOUND
500 })?;
501
502 let mut response = order_entity.data.clone();
504 enrich_order_response(&state.engine, ¶ms.workspace, &order_id, &mut response).await;
505
506 Ok(Json(response))
507}
508
509pub async fn update_persona_lifecycles(
515 State(state): State<ConsistencyState>,
516 Query(params): Query<WorkspaceQuery>,
517) -> Result<Json<Value>, StatusCode> {
518 use mockforge_core::time_travel::now as get_virtual_time;
519
520 let mut unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
522 error!("State not found for workspace: {}", params.workspace);
523 StatusCode::NOT_FOUND
524 })?;
525
526 let current_time = get_virtual_time();
528
529 let mut updated = false;
531 if let Some(ref mut persona) = unified_state.active_persona {
532 let old_state = persona
533 .lifecycle
534 .as_ref()
535 .map(|l| l.current_state)
536 .unwrap_or(mockforge_data::LifecycleState::Active);
537
538 persona.update_lifecycle_state(current_time);
540
541 let new_state = persona
542 .lifecycle
543 .as_ref()
544 .map(|l| l.current_state)
545 .unwrap_or(mockforge_data::LifecycleState::Active);
546
547 if old_state != new_state {
548 updated = true;
549 info!(
550 "Persona {} lifecycle state updated: {:?} -> {:?}",
551 persona.id, old_state, new_state
552 );
553
554 state
556 .engine
557 .set_active_persona(¶ms.workspace, persona.clone())
558 .await
559 .map_err(|e| {
560 error!("Failed to update persona lifecycle: {}", e);
561 StatusCode::INTERNAL_SERVER_ERROR
562 })?;
563 }
564 }
565
566 Ok(Json(serde_json::json!({
567 "success": true,
568 "workspace": params.workspace,
569 "updated": updated,
570 "current_time": current_time.to_rfc3339(),
571 })))
572}
573
574pub fn consistency_router(state: ConsistencyState) -> axum::Router {
576 use axum::routing::{get, post};
577
578 axum::Router::new()
579 .route("/api/v1/consistency/state", get(get_state))
581 .route("/api/v1/consistency/persona", post(set_persona))
583 .route("/api/v1/consistency/persona/lifecycle", post(set_persona_lifecycle))
584 .route("/api/v1/consistency/persona/update-lifecycles", post(update_persona_lifecycles))
585 .route("/api/v1/consistency/scenario", post(set_scenario))
587 .route("/api/v1/consistency/reality-level", post(set_reality_level))
589 .route("/api/v1/consistency/reality-ratio", post(set_reality_ratio))
591 .route("/api/v1/consistency/entities", get(list_entities).post(register_entity))
593 .route(
594 "/api/v1/consistency/entities/{entity_type}/{entity_id}",
595 get(get_entity),
596 )
597 .route("/api/v1/consistency/users/{id}", get(get_user_with_graph))
599 .route("/api/v1/consistency/users/{id}/orders", get(get_user_orders_with_graph))
600 .route("/api/v1/consistency/orders/{id}", get(get_order_with_graph))
601 .route("/api/v1/consistency/chaos/activate", post(activate_chaos_rule))
603 .route("/api/v1/consistency/chaos/deactivate", post(deactivate_chaos_rule))
604 .with_state(state)
605}