///|
pub struct ReportTable {
  headers : Array[String]
  rows : Array[Array[String]]
} derive(Debug)

///|
pub fn ReportTable::new(headers : Array[String]) -> ReportTable {
  { headers, rows: [] }
}

///|
pub fn ReportTable::add_row(self : ReportTable, row : Array[String]) -> Unit {
  self.rows.push(row)
}

///|
pub fn ReportTable::row_count(self : ReportTable) -> Int {
  self.rows.length()
}

///|
pub fn ReportTable::column_count(self : ReportTable) -> Int {
  self.headers.length()
}

///|
pub fn ReportTable::valid_shape(self : ReportTable) -> Bool {
  let mut valid = true
  for row in self.rows {
    if row.length() != self.headers.length() {
      valid = false
    }
  }
  valid
}

///|
pub fn ReportTable::to_csv(self : ReportTable) -> String {
  let mut output = self.headers.join(",") + "\n"
  for row in self.rows {
    output = output + row.join(",") + "\n"
  }
  output
}

///|
pub fn ReportTable::to_markdown(self : ReportTable) -> String {
  let mut output = "|" + self.headers.join("|") + "|\n|"
  let separators = Array::make(self.headers.length(), "---")
  output = output + separators.join("|") + "|\n"
  for row in self.rows {
    output = output + "|" + row.join("|") + "|\n"
  }
  output
}

///|
pub fn report_table_from_result(result : BenchmarkResult) -> ReportTable {
  let table = ReportTable::new(["episode", "reward", "steps", "solved"])
  for i in 0.. ReportTable {
  let rewards = result.reward_stats()
  let steps = result.step_stats()
  let table = ReportTable::new(["metric", "value"])
  table.add_row(["episodes", "\{result.episodes}"])
  table.add_row(["solve_rate", "\{result.solve_rate()}"])
  table.add_row(["reward_mean", "\{rewards.average()}"])
  table.add_row(["reward_variance", "\{rewards.variance()}"])
  table.add_row(["steps_mean", "\{steps.average()}"])
  table.add_row(["tail_reward", "\{result.tail_average(20)}"])
  table
}

///|
pub fn compare_results_table(results : Array[BenchmarkResult]) -> ReportTable {
  let table = ReportTable::new([
    "benchmark", "episodes", "solve_rate", "reward_mean", "steps_mean",
  ])
  for result in results {
    table.add_row([
      result.name,
      "\{result.episodes}",
      "\{result.solve_rate()}",
      "\{result.reward_stats().average()}",
      "\{result.step_stats().average()}",
    ])
  }
  table
}

///|
pub fn run_standard_reports(config : EvaluationConfig) -> String {
  let grid = run_gridworld_benchmark(config.episodes, config.seed).result
  let random = run_random_walk_benchmark(config).result
  let bandit = run_bandit_benchmark(config).result
  compare_results_table([grid, random, bandit]).to_markdown()
}

///|
pub fn report_header(title : String, seed : Int) -> String {
  "# \{title}\n\nseed: \{seed}\n\n"
}

///|
pub fn report_footer() -> String {
  "\nGenerated by MoonRLLab deterministic benchmark tooling.\n"
}

///|
pub fn full_benchmark_report(config : EvaluationConfig) -> String {
  report_header("MoonRLLab benchmark report", config.seed) +
  run_standard_reports(config) +
  report_footer()
}

///|
pub fn csv_escape(value : String) -> String {
  if value.contains(",") || value.contains("\n") {
    "\"" + value + "\""
  } else {
    value
  }
}

///|
pub fn join_lines(lines : Array[String]) -> String {
  lines.join("\n")
}

///|
pub fn percentile(values : Array[Double], ratio : Double) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let sorted = values.copy()
    for i in 0.. String {
  let stats = RunningStats::new()
  for value in values {
    stats.push(value)
  }
  let margin = 1.96 * stats.standard_error()
  "mean=\{stats.average()},lower=\{stats.average() - margin},upper=\{stats.average() + margin}"
}

///|
pub fn report_quality(result : BenchmarkResult) -> ScorecardSet {
  let scores = ScorecardSet::new()
  scores.add(
    Scorecard::new(
      "has_episodes",
      1.0,
      if result.episodes > 0 {
        1.0
      } else {
        0.0
      },
      0.0,
    ),
  )
  scores.add(
    Scorecard::new(
      "bounded_solve_rate",
      1.0,
      if result.solve_rate() >= 0.0 && result.solve_rate() <= 1.0 {
        1.0
      } else {
        0.0
      },
      0.0,
    ),
  )
  scores.add(
    Scorecard::new(
      "csv_rows",
      result.episodes.to_double() + 1.0,
      result.to_csv().split("\n").length().to_double(),
      0.0,
    ),
  )
  scores
}

///|
pub fn result_digest(result : BenchmarkResult) -> String {
  let stats = result.reward_stats()
  "\{result.name}|\{result.seed}|\{result.episodes}|\{stats.average()}|\{result.solve_rate()}"
}

///|
pub fn merge_reward_series(
  left : Array[Double],
  right : Array[Double],
) -> Array[Double] {
  let result = left.copy()
  for value in right {
    result.push(value)
  }
  result
}

///|
pub fn moving_average(values : Array[Double], window : Int) -> Array[Double] {
  let result = Array::make(values.length(), 0.0)
  let size = if window < 1 { 1 } else { window }
  for i in 0.. size { i + 1 - size } else { 0 }
    let mut total = 0.0
    let mut count = 0
    for j in start..<(i + 1) {
      total = total + values[j]
      count = count + 1
    }
    result[i] = total / count.to_double()
  }
  result
}

///|
pub fn trend(values : Array[Double]) -> Double {
  if values.length() < 2 {
    0.0
  } else {
    values[values.length() - 1] - values[0]
  }
}

///|
pub fn quality_text(result : BenchmarkResult) -> String {
  let quality = report_quality(result)
  "\{result_digest(result)}\n\{quality.to_text()}"
}

///|
pub fn report_sections(config : EvaluationConfig) -> Array[String] {
  let result = run_random_walk_benchmark(config).result
  [
    report_header("Experiment", config.seed),
    report_table_from_stats(result).to_markdown(),
    quality_text(result),
    report_footer(),
  ]
}

///|
pub fn report_sections_text(config : EvaluationConfig) -> String {
  join_lines(report_sections(config))
}

///|
pub fn reward_floor(values : Array[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mut result = values[0]
    for value in values {
      if value < result {
        result = value
      }
    }
    result
  }
}

///|
pub fn reward_ceiling(values : Array[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mut result = values[0]
    for value in values {
      if value > result {
        result = value
      }
    }
    result
  }
}

///|
pub fn solved_prefix(values : Array[Bool]) -> Int {
  let mut count = 0
  for value in values {
    if value {
      count = count + 1
    } else {
      break
    }
  }
  count
}

///|
pub fn result_range(result : BenchmarkResult) -> String {
  let stats = result.reward_stats()
  "min=\{stats.minimum},max=\{stats.maximum},span=\{stats.maximum - stats.minimum}"
}

///|
pub fn result_health(result : BenchmarkResult) -> String {
  let valid = result.episodes >= 0 &&
    result.rewards.length() == result.episodes &&
    result.steps.length() == result.episodes
  "valid=\{valid},\{result_range(result)}"
}

///|
pub fn markdown_escape(value : String) -> String {
  value.replace(old="|", new="\\|").replace(old="\n", new=" ")
}

///|
pub fn table_preview(table : ReportTable, limit : Int) -> String {
  let mut output = table.headers.join("|") + "\n"
  let size = if limit < 0 {
    0
  } else if limit > table.rows.length() {
    table.rows.length()
  } else {
    limit
  }
  for i in 0.. Bool {
  table.row_count() == 0
}

///|
pub fn table_last_row(table : ReportTable) -> Array[String]? {
  if table.rows.length() == 0 {
    None
  } else {
    Some(table.rows[table.rows.length() - 1])
  }
}

///|
pub fn result_is_nontrivial(result : BenchmarkResult) -> Bool {
  result.episodes > 0 && result.reward_stats().count > 0
}

///|
pub fn result_has_learning_signal(result : BenchmarkResult) -> Bool {
  trend(result.rewards) != 0.0 || result.solve_rate() > 0.0
}

///|
pub fn compact_result_line(result : BenchmarkResult) -> String {
  "\{result.name}:\{result.episodes}:\{result.solve_rate()}:\{result.tail_average(10)}"
}

///|
pub fn compact_results(results : Array[BenchmarkResult]) -> String {
  let lines = []
  for result in results {
    lines.push(compact_result_line(result))
  }
  join_lines(lines)
}

///|
pub fn report_version() -> String {
  "MoonRLLab-report-v1"
}

///|
pub fn result_episode_ids(result : BenchmarkResult) -> Array[Int] {
  let ids = Array::make(result.episodes, 0)
  for i in 0.. Bool {
  result_episode_ids(result).length() == result.episodes
}

///|
pub fn report_contract() -> String {
  "contract=csv,markdown,summary,confidence,health"
}