///|
pub fn simulate_rollout(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> SimulationReport {
  let findings = validate_plan(plan)
  let steps = Array::new(capacity=plan.phases.length())
  let mut final_gate = {
    decision: Advance,
    severity: Pass,
    score: 100,
    reasons: ["simulation has no phases"],
  }
  let mut final_blast = {
    exposed_users: 0,
    exposed_traffic_units: 0,
    region_count: 0,
    tier_count: 0,
    percent_bp: 0,
    risk_score: 0,
  }
  for phase in plan.phases {
    let minute = phase.start_minute + phase.duration_minutes - 1
    let blast = blast_radius_for_users(plan, users, minute)
    let snapshot = metrics_for_minute(metrics, minute)
    let gate = evaluate_gate(plan.budget, snapshot, blast)
    steps.push({
      minute,
      phase_name: phase.name,
      exposed_users: blast.exposed_users,
      exposed_traffic_units: blast.exposed_traffic_units,
      gate,
    })
    final_gate = gate
    final_blast = blast
  }
  let score = simulation_score(findings, final_gate, final_blast)
  {
    plan_name: plan.name,
    verdict: simulation_verdict(findings, final_gate, score),
    score,
    findings,
    steps,
    final_gate,
    blast_radius: final_blast,
  }
}

///|
pub fn simulation_markdown(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> String {
  simulate_rollout(plan, users, metrics).to_markdown()
}

///|
pub fn simulation_json(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> String {
  simulate_rollout(plan, users, metrics).to_json()
}

///|
fn metrics_for_minute(
  metrics : Array[MetricsSnapshot],
  minute : Int,
) -> MetricsSnapshot {
  let mut chosen = MetricsSnapshot::MetricsSnapshot(minute~)
  let mut best_delta = 2147483
  for item in metrics {
    let delta = abs_int(item.minute - minute)
    if delta < best_delta {
      best_delta = delta
      chosen = item
    }
  }
  chosen
}

///|
fn abs_int(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
fn simulation_score(
  findings : Array[RolloutFinding],
  gate : GateResult,
  blast : BlastRadius,
) -> Int {
  let mut score = gate.score
  for finding in findings {
    score -= finding.penalty
  }
  score -= blast.risk_score / 8
  clamp(score, 0, 100)
}

///|
fn simulation_verdict(
  findings : Array[RolloutFinding],
  gate : GateResult,
  score : Int,
) -> String {
  let mut failures = 0
  for finding in findings {
    if finding.severity == Fail {
      failures += 1
    }
  }
  if failures > 0 || gate.decision == Rollback || gate.decision == Abort {
    "blocked"
  } else if gate.decision == Hold {
    "hold"
  } else if score >= 90 {
    "ready-to-advance"
  } else {
    "needs-review"
  }
}

///|
pub fn SimulationReport::has_blockers(self : SimulationReport) -> Bool {
  self.verdict == "blocked"
}

///|
pub fn SimulationReport::warning_count(self : SimulationReport) -> Int {
  let mut total = 0
  for finding in self.findings {
    if finding.severity == Warn {
      total += 1
    }
  }
  if self.final_gate.severity == Warn {
    total += 1
  }
  total
}

///|
pub fn SimulationReport::to_markdown(self : SimulationReport) -> String {
  let out = StringBuilder()
  out.write_string("# CanaryRing Simulation\n\n")
  out.write_string("- Plan: " + self.plan_name + "\n")
  out.write_string("- Verdict: " + self.verdict + "\n")
  out.write_string("- Score: " + self.score.to_string() + "/100\n")
  out.write_string(
    "- Final gate: " + decision_text(self.final_gate.decision) + "\n",
  )
  out.write_string(
    "- Final gate score: " + self.final_gate.score.to_string() + "/100\n",
  )
  out.write_string(
    "- Final blast radius: " +
    percent_text(self.blast_radius.percent_bp) +
    "\n\n",
  )
  out.write_string("## Plan Findings\n\n")
  for finding in self.findings {
    out.write_string("- [" + severity_text(finding.severity) + "] ")
    out.write_string(finding.id + " (" + kind_text(finding.kind) + "): ")
    out.write_string(finding.title)
    if finding.evidence != "" {
      out.write_string(" -- " + finding.evidence)
    }
    if finding.repair != "" && finding.severity != Pass {
      out.write_string(" Fix: " + finding.repair)
    }
    out.write_string("\n")
  }
  out.write_string("\n## Steps\n\n")
  for step in self.steps {
    out.write_string("- minute " + step.minute.to_string() + " ")
    out.write_string(step.phase_name + ": ")
    out.write_string(step.exposed_users.to_string() + " users, ")
    out.write_string(step.exposed_traffic_units.to_string() + " traffic, gate=")
    out.write_string(decision_text(step.gate.decision) + " ")
    out.write_string(step.gate.score.to_string() + "/100\n")
  }
  out.write_string("\n## Gate Reasons\n\n")
  for reason in self.final_gate.reasons {
    out.write_string("- " + reason + "\n")
  }
  out.write_string("\n")
  out.write_string(blast_radius_markdown(self.blast_radius))
  out.to_string()
}

///|
pub fn SimulationReport::to_json(self : SimulationReport) -> String {
  let out = StringBuilder()
  out.write_string("{")
  out.write_string("\"plan\":\"" + json_escape(self.plan_name) + "\",")
  out.write_string("\"verdict\":\"" + json_escape(self.verdict) + "\",")
  out.write_string("\"score\":" + self.score.to_string() + ",")
  out.write_string(
    "\"final_gate\":\"" + decision_text(self.final_gate.decision) + "\",",
  )
  out.write_string(
    "\"final_gate_score\":" + self.final_gate.score.to_string() + ",",
  )
  out.write_string(
    "\"blast_percent_bp\":" + self.blast_radius.percent_bp.to_string() + ",",
  )
  out.write_string("\"steps\":[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    out.write_string("{")
    out.write_string("\"minute\":" + step.minute.to_string() + ",")
    out.write_string("\"phase\":\"" + json_escape(step.phase_name) + "\",")
    out.write_string("\"users\":" + step.exposed_users.to_string() + ",")
    out.write_string(
      "\"traffic\":" + step.exposed_traffic_units.to_string() + ",",
    )
    out.write_string("\"gate\":\"" + decision_text(step.gate.decision) + "\"")
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}