///|
pub(all) struct DryExecutionProjection {
  request : @routine.RoutineRequest
  features : @domain.MarketFeatureFrame
  structure_levels : @domain.StructureLevelFrame
  hints : @domain.Stage1FeatureHints
  stage1_prompt : @prompt.PromptPacket
  context_budget : @model.ContextBudgetReport
  stage1_model_request : @model.ModelCallRequest
  stage1_model_response : @model.ModelCallResponse
  experience_memory : @memory.ExperienceMemorySelection
  decision_policy : @policy.DecisionPolicyReport
  stage2_prompt : @prompt.PromptPacket
  stage2_model_request : @model.ModelCallRequest
  stage2_model_response : @model.ModelCallResponse
  result : @routine.RoutineResponse
} derive(Debug, Eq, ToJson, FromJson)

///|
fn fixture_stage1_content(diagnosis : @domain.Stage1Diagnosis) -> String {
  "{\"schema\":\"stage1-diagnosis\",\"confidence\":\{diagnosis.confidence}}"
}

///|
fn fixture_stage2_content(decision : @domain.Stage2Decision) -> String {
  "{\"schema\":\"stage2-decision\",\"confidence\":\{decision.confidence}}"
}

///|
fn append_projection_events(
  base : Array[@routine.RoutineEvent],
  run_id : @domain.RunId,
) -> Array[@routine.RoutineEvent] {
  let events = base.copy()
  let mut index = events.length()
  events.push(
    @routine.routine_event(
      run_id,
      index,
      SnapshotMarketData,
      ToolPlanned,
      "computed deterministic market features",
    ),
  )
  index += 1
  events.push(
    @routine.routine_event(
      run_id,
      index,
      Stage1Diagnosis,
      ToolPlanned,
      "assembled stage1 prompt packet",
    ),
  )
  index += 1
  events.push(
    @routine.routine_event(
      run_id,
      index,
      Stage1Diagnosis,
      ModelOutputCaptured,
      "captured fixture stage1 model output",
    ),
  )
  index += 1
  events.push(
    @routine.routine_event(
      run_id,
      index,
      Stage2Decision,
      ToolPlanned,
      "selected Stage 2 experience memory",
    ),
  )
  index += 1
  events.push(
    @routine.routine_event(
      run_id,
      index,
      Stage2Decision,
      ToolPlanned,
      "assembled stage2 prompt packet",
    ),
  )
  index += 1
  events.push(
    @routine.routine_event(
      run_id,
      index,
      Stage2Decision,
      ModelOutputCaptured,
      "captured fixture stage2 model output",
    ),
  )
  events
}

///|
pub fn dry_execution_projection(
  request : @routine.RoutineRequest,
  gateway : @model.ModelGateway,
  diagnosis : @domain.Stage1Diagnosis,
  decision : @domain.Stage2Decision,
) -> DryExecutionProjection {
  let features = @tools.compute_market_features(request.snapshot)
  let structure_levels = @tools.compute_structure_levels(request.snapshot)
  let hints = @tools.classify_market_structure(features)
  let stage1_prompt = @prompt.assemble_stage1_prompt(
    @prompt.stage1_prompt_input(request, features, hints),
  )
  let stage1_context_budget = @model.prepare_context_budget_plan(
    @model.context_budget_input(request.run_id, gateway, stage1_prompt),
  )
  let stage1_model_request = @model.model_call_request(
    request.run_id,
    gateway,
    stage1_prompt,
  )
  let stage1_model_response = @model.model_call_response(
    stage1_model_request,
    fixture_stage1_content(diagnosis),
    usage=@routine.model_usage(stage1_prompt.text().length(), 32),
  )
  let base = @routine.run_dry_analysis(request, diagnosis, decision)
  let routed = match base.record.stage1 {
    Some(stage1) => stage1
    None => @tools.diagnosis_with_routing(diagnosis)
  }
  let experience_memory = @memory.select_experience_memories(routed)
  let decision_policy = @policy.evaluate_decision_policy(
    @policy.decision_policy_input(request.run_id, routed, decision),
  )
  let stage2_prompt = @prompt.assemble_stage2_prompt(
    @prompt.stage2_prompt_input(
      request,
      routed,
      routed.strategy_files_needed,
      experience_memories=experience_memory.entries,
    ),
  )
  let stage2_context_budget = @model.prepare_context_budget_plan(
    @model.context_budget_input(request.run_id, gateway, stage2_prompt),
  )
  let stage2_model_request = @model.model_call_request(
    request.run_id,
    gateway,
    stage2_prompt,
  )
  let stage2_model_response = @model.model_call_response(
    stage2_model_request,
    fixture_stage2_content(decision),
    usage=@routine.model_usage(stage2_prompt.text().length(), 32),
  )
  let result : @routine.RoutineResponse = {
    request,
    plan: base.plan,
    events: append_projection_events(base.events, request.run_id),
    record: base.record,
  }
  {
    request,
    features,
    structure_levels,
    hints,
    stage1_prompt,
    context_budget: @model.context_budget_report(request.run_id, [
      stage1_context_budget, stage2_context_budget,
    ]),
    stage1_model_request,
    stage1_model_response,
    experience_memory,
    decision_policy,
    stage2_prompt,
    stage2_model_request,
    stage2_model_response,
    result,
  }
}