///|
pub(all) struct VirtualClock {
  now_ms : Int
  advances : Int
} derive(Eq, Debug)

///|
pub(all) enum SimulatedResponse {
  SimulatedSuccess(String, Int)
  SimulatedFailure(String, String, Bool, Int)
} derive(Eq, Debug)

///|
pub(all) struct SimulationScenario {
  name : String
  operation : String
  started_at_ms : Int
  responses : Array[SimulatedResponse]
  repeat_last : Bool
} derive(Eq, Debug)

///|
pub(all) struct TimelineEntry {
  at_ms : Int
  label : String
  detail : String
} derive(Eq, Debug)

///|
pub(all) struct SimulationResult {
  scenario : SimulationScenario
  outcome : Result[String, ExecuteError]
  chain : PolicyChain
  attempts : Int
  finished_at_ms : Int
  timeline : Array[TimelineEntry]
  events : EventLog
  metrics : MetricsSnapshot
} derive(Debug)

///|
pub fn virtual_clock(now_ms : Int) -> VirtualClock {
  { now_ms: clamp_non_negative(now_ms), advances: 0 }
}

///|
pub fn clock_advance(clock : VirtualClock, duration_ms : Int) -> VirtualClock {
  {
    now_ms: clock.now_ms + clamp_non_negative(duration_ms),
    advances: clock.advances + 1,
  }
}

///|
pub fn clock_advance_to(clock : VirtualClock, target_ms : Int) -> VirtualClock {
  if target_ms <= clock.now_ms {
    clock
  } else {
    { now_ms: target_ms, advances: clock.advances + 1 }
  }
}

///|
pub fn simulated_success(value : String, latency_ms : Int) -> SimulatedResponse {
  SimulatedSuccess(value, clamp_non_negative(latency_ms))
}

///|
pub fn simulated_failure(
  code : String,
  message : String,
  retryable : Bool,
  latency_ms : Int,
) -> SimulatedResponse {
  SimulatedFailure(code, message, retryable, clamp_non_negative(latency_ms))
}

///|
pub fn simulation_scenario(
  name : String,
  operation : String,
  started_at_ms : Int,
  responses : Array[SimulatedResponse],
  repeat_last? : Bool = false,
) -> SimulationScenario {
  {
    name,
    operation,
    started_at_ms: clamp_non_negative(started_at_ms),
    responses: responses.copy(),
    repeat_last,
  }
}

///|
pub fn simulate(
  chain : PolicyChain,
  scenario : SimulationScenario,
) -> SimulationResult {
  let execution = execute_with(
    chain,
    execution_context(scenario.started_at_ms, scenario.operation),
    fn(attempt, _now_ms) { scenario_outcome(scenario, attempt) },
  )
  let timeline = build_timeline(scenario, execution)
  let finished_at_ms = if timeline.length() == 0 {
    execution.finished_at_ms
  } else {
    timeline[timeline.length() - 1].at_ms
  }
  let events = trace_to_event_log(
    execution.trace,
    scenario.operation,
    scenario.started_at_ms,
  )
  let mut metric_state = metrics_from_log(events)
  metric_state = metrics_observe_latency(
    metric_state,
    max_int(0, finished_at_ms - scenario.started_at_ms),
  )
  {
    scenario,
    outcome: execution.outcome,
    chain: execution.chain,
    attempts: execution.attempts,
    finished_at_ms,
    timeline,
    events,
    metrics: metrics_snapshot(metric_state),
  }
}

///|
pub fn simulation_succeeded(result : SimulationResult) -> Bool {
  result.outcome is Ok(_)
}

///|
pub fn simulation_duration_ms(result : SimulationResult) -> Int {
  max_int(0, result.finished_at_ms - result.scenario.started_at_ms)
}

///|
pub fn timeline_filter(
  timeline : Array[TimelineEntry],
  label : String,
) -> Array[TimelineEntry] {
  let matches : Array[TimelineEntry] = []
  for entry in timeline {
    if entry.label == label {
      matches.push(entry)
    }
  }
  matches
}

///|
pub fn format_timeline(timeline : Array[TimelineEntry]) -> String {
  let mut output = ""
  for index = 0; index < timeline.length(); index = index + 1 {
    let entry = timeline[index]
    let suffix = if entry.detail.length() > 0 { " " + entry.detail } else { "" }
    if index > 0 {
      output = output + "\n"
    }
    output = output + entry.at_ms.to_string() + "ms " + entry.label + suffix
  }
  output
}

///|
pub fn format_simulation(result : SimulationResult) -> String {
  let status = match result.outcome {
    Ok(value) => "success value=" + value
    Err(err) => "failure " + format_execute_error(err)
  }
  "scenario=" +
  result.scenario.name +
  " status=" +
  status +
  " attempts=" +
  result.attempts.to_string() +
  " duration_ms=" +
  simulation_duration_ms(result).to_string() +
  "\n" +
  format_timeline(result.timeline) +
  "\nmetrics " +
  format_metrics_snapshot(result.metrics)
}

///|
pub fn run_scenarios(
  chain : PolicyChain,
  scenarios : Array[SimulationScenario],
) -> Array[SimulationResult] {
  let results : Array[SimulationResult] = []
  let mut current = chain
  for scenario in scenarios {
    let result = simulate(current, scenario)
    current = result.chain
    results.push(result)
  }
  results
}

///|
pub fn simulation_summary(results : Array[SimulationResult]) -> String {
  let mut succeeded = 0
  let mut failed = 0
  let mut attempts = 0
  let mut duration = 0
  for result in results {
    if simulation_succeeded(result) {
      succeeded = succeeded + 1
    } else {
      failed = failed + 1
    }
    attempts = attempts + result.attempts
    duration = duration + simulation_duration_ms(result)
  }
  "scenarios=" +
  results.length().to_string() +
  " succeeded=" +
  succeeded.to_string() +
  " failed=" +
  failed.to_string() +
  " attempts=" +
  attempts.to_string() +
  " virtual_duration_ms=" +
  duration.to_string()
}

///|
fn scenario_outcome(
  scenario : SimulationScenario,
  attempt : Int,
) -> ActionOutcome[String] {
  if scenario.responses.length() == 0 {
    return Failure(
      permanent_failure("empty_scenario", "scenario has no responses"),
    )
  }
  let index = attempt - 1
  if index >= scenario.responses.length() && !scenario.repeat_last {
    return Failure(
      permanent_failure(
        "scenario_exhausted",
        "scenario has no response for attempt " + attempt.to_string(),
      ),
    )
  }
  let selected = scenario.responses[min_int(
      index,
      scenario.responses.length() - 1,
    )]
  match selected {
    SimulatedSuccess(value, _) => Success(value)
    SimulatedFailure(code, message, retryable, _) =>
      Failure({ code, message, retryable })
  }
}

///|
fn build_timeline(
  scenario : SimulationScenario,
  execution : ExecutionResult[String],
) -> Array[TimelineEntry] {
  let timeline : Array[TimelineEntry] = []
  timeline.push({
    at_ms: scenario.started_at_ms,
    label: "scenario.started",
    detail: scenario.name,
  })
  let mut current_ms = scenario.started_at_ms
  for attempt = 1; attempt <= execution.attempts; attempt = attempt + 1 {
    if attempt > 1 {
      current_ms = current_ms +
        retry_delay(execution.chain.retry.backoff, attempt - 1)
    }
    let response = response_for_timeline(scenario, attempt)
    timeline.push({
      at_ms: current_ms,
      label: "attempt.started",
      detail: "number=" + attempt.to_string(),
    })
    let latency = response_latency(response)
    current_ms = current_ms + latency
    timeline.push({
      at_ms: current_ms,
      label: response_label(response),
      detail: response_detail(response),
    })
  }
  timeline.push({
    at_ms: max_int(current_ms, execution.finished_at_ms),
    label: "scenario.finished",
    detail: if execution_succeeded(execution) {
      "success"
    } else {
      "failure"
    },
  })
  timeline
}

///|
fn response_for_timeline(
  scenario : SimulationScenario,
  attempt : Int,
) -> SimulatedResponse {
  if scenario.responses.length() == 0 {
    return simulated_failure("empty_scenario", "no response", false, 0)
  }
  scenario.responses[min_int(attempt - 1, scenario.responses.length() - 1)]
}

///|
fn response_latency(response : SimulatedResponse) -> Int {
  match response {
    SimulatedSuccess(_, latency) => latency
    SimulatedFailure(_, _, _, latency) => latency
  }
}

///|
fn response_label(response : SimulatedResponse) -> String {
  match response {
    SimulatedSuccess(_, _) => "attempt.succeeded"
    SimulatedFailure(_, _, _, _) => "attempt.failed"
  }
}

///|
fn response_detail(response : SimulatedResponse) -> String {
  match response {
    SimulatedSuccess(value, latency) =>
      "value=" + value + " latency_ms=" + latency.to_string()
    SimulatedFailure(code, _, retryable, latency) =>
      "code=" +
      code +
      " retryable=" +
      retryable.to_string() +
      " latency_ms=" +
      latency.to_string()
  }
}