///|
pub(all) struct PhaseRecommendation {
  next_percent_bp : Int
  next_mode : RolloutMode
  decision : GateDecision
  title : String
  rationale : String
  action_items : Array[String]
} derive(Eq, Debug)

///|
pub fn recommend_next_phase(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
  minute : Int,
) -> PhaseRecommendation {
  let blast = blast_radius_for_users(plan, users, minute)
  let latest = latest_metric(metrics)
  let gate = evaluate_gate(plan.budget, latest, blast)
  let current_percent = match active_phase(plan, minute) {
    Some(phase) => phase.percent_bp
    None => 0
  }
  if gate.decision == Rollback || gate.decision == Abort {
    return {
      next_percent_bp: 0,
      next_mode: KillSwitch,
      decision: Rollback,
      title: "Rollback or kill switch",
      rationale: "Gate returned " +
      decision_text(gate.decision) +
      " at score " +
      gate.score.to_string(),
      action_items: rollback_actions(gate),
    }
  }
  if gate.decision == Hold {
    return {
      next_percent_bp: current_percent,
      next_mode: current_mode(plan, minute),
      decision: Hold,
      title: "Hold current ring",
      rationale: "Risk gate needs more evidence before advancing.",
      action_items: hold_actions(gate),
    }
  }
  let next_percent = next_percent_step(current_percent)
  {
    next_percent_bp: next_percent,
    next_mode: if next_percent == 0 {
      Shadow
    } else {
      Live
    },
    decision: Advance,
    title: "Advance to " + percent_text(next_percent),
    rationale: "All gates passed and blast radius is inside budget.",
    action_items: advance_actions(next_percent),
  }
}

///|
fn current_mode(plan : RolloutPlan, minute : Int) -> RolloutMode {
  match active_phase(plan, minute) {
    Some(phase) => phase.mode
    None => Shadow
  }
}

///|
fn next_percent_step(current_percent : Int) -> Int {
  if current_percent < 100 {
    100
  } else if current_percent < 500 {
    500
  } else if current_percent < 1000 {
    1000
  } else if current_percent < 2500 {
    2500
  } else if current_percent < 5000 {
    5000
  } else if current_percent < 7500 {
    7500
  } else {
    10000
  }
}

///|
fn rollback_actions(gate : GateResult) -> Array[String] {
  let actions = Array::new(capacity=gate.reasons.length() + 3)
  actions.push("Set rollout mode to KillSwitch or reduce percent_bp to 0.")
  actions.push("Record the failing metric window in the runbook.")
  actions.push("Open a follow-up work item before re-advancing.")
  for reason in gate.reasons {
    actions.push("Investigate: " + reason)
  }
  actions
}

///|
fn hold_actions(gate : GateResult) -> Array[String] {
  let actions = Array::new(capacity=gate.reasons.length() + 2)
  actions.push("Keep the current stable bucket assignment.")
  actions.push("Collect another metric window before changing percent_bp.")
  for reason in gate.reasons {
    actions.push("Watch: " + reason)
  }
  actions
}

///|
fn advance_actions(next_percent : Int) -> Array[String] {
  let actions = Array::new(capacity=4)
  actions.push("Create the next phase at " + percent_text(next_percent) + ".")
  actions.push("Keep the same rollout salt to avoid user reshuffle.")
  actions.push("Record CI, test and metric evidence before publishing.")
  if next_percent >= 5000 {
    actions.push("Require manual approval for this large live phase.")
  }
  actions
}

///|
pub fn recommendation_markdown(recommendation : PhaseRecommendation) -> String {
  let out = StringBuilder()
  out.write_string("# CanaryRing Recommendation\n\n")
  out.write_string(
    "- Decision: " + decision_text(recommendation.decision) + "\n",
  )
  out.write_string("- Next mode: " + mode_text(recommendation.next_mode) + "\n")
  out.write_string(
    "- Next percent: " + percent_text(recommendation.next_percent_bp) + "\n",
  )
  out.write_string("- Title: " + recommendation.title + "\n")
  out.write_string("- Rationale: " + recommendation.rationale + "\n\n")
  out.write_string("## Action Items\n\n")
  for item in recommendation.action_items {
    out.write_string("- " + item + "\n")
  }
  out.to_string()
}

///|
pub fn compare_recommendations(
  left : PhaseRecommendation,
  right : PhaseRecommendation,
) -> PhaseRecommendation {
  if decision_rank(left.decision) >= decision_rank(right.decision) {
    left
  } else {
    right
  }
}

///|
fn decision_rank(decision : GateDecision) -> Int {
  match decision {
    Advance => 0
    Hold => 1
    Rollback => 2
    Abort => 3
  }
}

///|
pub fn explain_bucket(
  user : UserProfile,
  plan : RolloutPlan,
  minute : Int,
) -> String {
  let exposure = exposure_for_user(plan, user, minute)
  let out = StringBuilder()
  out.write_string(user.id + " bucket=" + exposure.bucket_bp.to_string())
  out.write_string(" cohort=" + split_bucket(exposure.bucket_bp))
  out.write_string(" phase=" + exposure.phase_name)
  out.write_string(" mode=" + mode_text(exposure.mode))
  out.write_string(" enabled=" + exposure.enabled.to_string())
  out.write_string(" reason=" + exposure.reason)
  out.to_string()
}

///|
pub fn cohort_distribution(
  users : Array[UserProfile],
  salt : String,
  ring_count : Int,
) -> Array[String] {
  let out = Array::new(capacity=users.length())
  for user in users {
    out.push(user.id + "=" + cohort_label(user.id, salt, ring_count))
  }
  out
}