///|
pub(all) struct TrafficForecast {
  base_users : Int
  base_traffic_units : Int
  growth_bp_per_hour : Int
  peak_multiplier_bp : Int
  safety_margin_bp : Int
} derive(Eq, Debug)

///|
pub fn TrafficForecast::TrafficForecast(
  base_users? : Int = 0,
  base_traffic_units? : Int = 0,
  growth_bp_per_hour? : Int = 0,
  peak_multiplier_bp? : Int = 10000,
  safety_margin_bp? : Int = 0,
) -> TrafficForecast {
  {
    base_users,
    base_traffic_units,
    growth_bp_per_hour,
    peak_multiplier_bp,
    safety_margin_bp,
  }
}

///|
pub(all) struct ForecastStep {
  phase_name : String
  minute : Int
  projected_users : Int
  projected_traffic_units : Int
  exposure_bp : Int
  risk_score : Int
  budget_status : Severity
} derive(Eq, Debug)

///|
pub(all) struct ForecastReport {
  verdict : String
  peak_users : Int
  peak_traffic_units : Int
  peak_risk_score : Int
  steps : Array[ForecastStep]
} derive(Eq, Debug)

///|
pub fn forecast_rollout(
  plan : RolloutPlan,
  forecast : TrafficForecast,
) -> ForecastReport {
  let steps = Array::new(capacity=plan.phases.length())
  let mut peak_users = 0
  let mut peak_traffic = 0
  let mut peak_risk = 0
  let mut failed = false
  let mut warned = false
  for phase in plan.phases {
    let minute = phase.start_minute + phase.duration_minutes
    let users = projected_count(
      forecast.base_users,
      minute,
      forecast.growth_bp_per_hour,
      forecast.peak_multiplier_bp,
      forecast.safety_margin_bp,
    )
    let traffic = projected_count(
      forecast.base_traffic_units,
      minute,
      forecast.growth_bp_per_hour,
      forecast.peak_multiplier_bp,
      forecast.safety_margin_bp,
    )
    let exposed_users = users * clamp(phase.percent_bp, 0, 10000) / 10000
    let exposed_traffic = traffic * clamp(phase.percent_bp, 0, 10000) / 10000
    let risk = forecast_risk_score(exposed_users, exposed_traffic, phase)
    let status = forecast_status(plan.budget, phase.percent_bp, risk)
    if status == Fail {
      failed = true
    } else if status == Warn {
      warned = true
    }
    if exposed_users > peak_users {
      peak_users = exposed_users
    }
    if exposed_traffic > peak_traffic {
      peak_traffic = exposed_traffic
    }
    if risk > peak_risk {
      peak_risk = risk
    }
    steps.push({
      phase_name: phase.name,
      minute,
      projected_users: exposed_users,
      projected_traffic_units: exposed_traffic,
      exposure_bp: phase.percent_bp,
      risk_score: risk,
      budget_status: status,
    })
  }
  let verdict = if failed {
    "forecast-blocked"
  } else if warned {
    "forecast-review"
  } else {
    "forecast-ready"
  }
  {
    verdict,
    peak_users,
    peak_traffic_units: peak_traffic,
    peak_risk_score: peak_risk,
    steps,
  }
}

///|
fn projected_count(
  base : Int,
  minute : Int,
  growth_bp_per_hour : Int,
  peak_multiplier_bp : Int,
  safety_margin_bp : Int,
) -> Int {
  let hours = if minute <= 0 { 0 } else { (minute + 59) / 60 }
  let grown = base + base * growth_bp_per_hour * hours / 10000
  let peaked = if peak_multiplier_bp >= 10000 {
    grown + grown * (peak_multiplier_bp - 10000) / 10000
  } else {
    grown * peak_multiplier_bp / 10000
  }
  if safety_margin_bp >= 0 {
    peaked + peaked * safety_margin_bp / 10000
  } else {
    peaked * (10000 + safety_margin_bp) / 10000
  }
}

///|
fn forecast_risk_score(users : Int, traffic : Int, phase : RolloutPhase) -> Int {
  let user_part = users / 100
  let traffic_part = traffic / 250
  let mode_part = if phase.mode == Live {
    20
  } else if phase.mode == DryRun {
    10
  } else if phase.mode == KillSwitch {
    5
  } else {
    2
  }
  clamp(user_part + traffic_part + mode_part + phase.percent_bp / 500, 0, 100)
}

///|
fn forecast_status(
  budget : RiskBudget,
  exposure_bp : Int,
  risk_score : Int,
) -> Severity {
  if exposure_bp > budget.max_blast_radius_bp || risk_score >= 85 {
    Fail
  } else if exposure_bp > budget.max_blast_radius_bp * 8 / 10 ||
    risk_score >= 65 {
    Warn
  } else {
    Pass
  }
}

///|
pub fn default_traffic_forecast() -> TrafficForecast {
  TrafficForecast(
    base_users=20000,
    base_traffic_units=125000,
    growth_bp_per_hour=250,
    peak_multiplier_bp=14000,
    safety_margin_bp=1200,
  )
}

///|
pub fn ForecastReport::warning_count(self : ForecastReport) -> Int {
  let mut total = 0
  for step in self.steps {
    if step.budget_status == Warn {
      total += 1
    }
  }
  total
}

///|
pub fn ForecastReport::failure_count(self : ForecastReport) -> Int {
  let mut total = 0
  for step in self.steps {
    if step.budget_status == Fail {
      total += 1
    }
  }
  total
}

///|
pub fn forecast_markdown(report : ForecastReport) -> String {
  let out = StringBuilder()
  out.write_string("# CanaryRing Traffic Forecast\n\n")
  out.write_string("- Verdict: " + report.verdict + "\n")
  out.write_string("- Peak users: " + report.peak_users.to_string() + "\n")
  out.write_string(
    "- Peak traffic units: " + report.peak_traffic_units.to_string() + "\n",
  )
  out.write_string(
    "- Peak risk score: " + report.peak_risk_score.to_string() + "/100\n\n",
  )
  for step in report.steps {
    out.write_string("- [" + severity_text(step.budget_status) + "] ")
    out.write_string(step.phase_name + " minute " + step.minute.to_string())
    out.write_string(" users=" + step.projected_users.to_string())
    out.write_string(" traffic=" + step.projected_traffic_units.to_string())
    out.write_string(" exposure=" + percent_text(step.exposure_bp))
    out.write_string(" risk=" + step.risk_score.to_string() + "/100\n")
  }
  out.to_string()
}

///|
pub fn forecast_json(report : ForecastReport) -> String {
  let out = StringBuilder()
  out.write_string("{")
  out.write_string("\"verdict\":\"" + json_escape(report.verdict) + "\",")
  out.write_string("\"peak_users\":" + report.peak_users.to_string() + ",")
  out.write_string(
    "\"peak_traffic_units\":" + report.peak_traffic_units.to_string() + ",",
  )
  out.write_string(
    "\"peak_risk_score\":" + report.peak_risk_score.to_string() + ",",
  )
  out.write_string("\"steps\":[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    out.write_string("{")
    out.write_string("\"phase\":\"" + json_escape(step.phase_name) + "\",")
    out.write_string("\"minute\":" + step.minute.to_string() + ",")
    out.write_string("\"users\":" + step.projected_users.to_string() + ",")
    out.write_string(
      "\"traffic\":" + step.projected_traffic_units.to_string() + ",",
    )
    out.write_string("\"exposure_bp\":" + step.exposure_bp.to_string() + ",")
    out.write_string("\"risk_score\":" + step.risk_score.to_string() + ",")
    out.write_string("\"status\":\"" + severity_text(step.budget_status) + "\"")
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}