///|
pub struct ExperimentSummary {
  name : String
  result : BenchmarkResult
  notes : String
} derive(Debug)

///|
pub fn ExperimentSummary::report(self : ExperimentSummary) -> String {
  self.result.summary() + "\nnotes=" + self.notes
}

///|
pub fn run_random_walk_benchmark(
  config : EvaluationConfig,
) -> ExperimentSummary {
  let env = RandomWalkEnv::new(19, config.seed)
  let result = BenchmarkResult::new("random-walk", config.seed, config.episodes)
  let mut episode = 0
  while episode < config.episodes {
    let mut state = env.reset()
    let mut step = 0
    let mut total = 0.0
    let mut done = false
    while step < config.max_steps && !done {
      let action = if (episode + step) % 2 == 0 { 1 } else { 0 }
      let transition = env.step(action)
      total = total + transition.reward
      state = transition.next_state
      done = transition.done
      step = step + 1
    }
    result.record(episode, total, step, done && state == 18)
    episode = episode + 1
  }
  {
    name: "random-walk",
    result,
    notes: "fixed alternating policy; used as environment smoke benchmark",
  }
}

///|
pub fn run_bandit_benchmark(config : EvaluationConfig) -> ExperimentSummary {
  let env = BanditEnv::new([0.1, 0.25, 0.4, 0.55, 0.7], config.seed)
  let schedule = EpsilonSchedule::new(0.4, 0.05, config.episodes)
  let policy = BanditPolicy::new(env.arm_count(), schedule, config.seed + 1)
  let result = BenchmarkResult::new("bandit", config.seed, config.episodes)
  let mut episode = 0
  while episode < config.episodes {
    let _ = env.reset()
    let action = policy.choose(episode)
    let transition = env.step(action)
    policy.observe(action, transition.reward)
    result.record(episode, transition.reward, 1, action == env.best_arm())
    episode = episode + 1
  }
  {
    name: "bandit",
    result,
    notes: "epsilon-greedy sample-average baseline over five fixed arms",
  }
}

///|
pub fn run_gridworld_benchmark(episodes : Int, seed : Int) -> ExperimentSummary {
  let safe_episodes = if episodes < 0 { 0 } else { episodes }
  let env = GridWorldEnv::new()
  let states = env.state_space()
  let actions = env.actions()
  let policy = EpsilonGreedyPolicy::new(0.1, seed)
  let agent = QLearningAgent::new(states, actions, policy, 0.25, 0.95)
  let trainer = Trainer::new(safe_episodes, 80)
  let report = trainer.train_q_learning(
    env,
    agent,
    ConsoleLogger::new("benchmark"),
  )
  let result = BenchmarkResult::new("gridworld", seed, safe_episodes)
  for i in 0.. ExperimentSummary {
  let safe_episodes = if episodes < 0 { 0 } else { episodes }
  let env = CliffWalkingEnv::new()
  let states = env.state_space()
  let actions = env.actions()
  let policy = EpsilonGreedyPolicy::new(0.1, seed)
  let agent = QLearningAgent::new(states, actions, policy, 0.2, 0.95)
  let result = BenchmarkResult::new("cliff-walking", seed, safe_episodes)
  for episode in 0.. String {
  let grid = run_gridworld_benchmark(config.episodes, config.seed)
  let random = run_random_walk_benchmark(config)
  let bandit = run_bandit_benchmark(config)
  grid.report() + "\n" + random.report() + "\n" + bandit.report()
}

///|
pub fn reproducibility_signature(config : EvaluationConfig) -> String {
  let first = run_random_walk_benchmark(config)
  let second = run_random_walk_benchmark(config)
  let equal = first.result.to_csv() == second.result.to_csv()
  "seed=\{config.seed},episodes=\{config.episodes},reproducible=\{equal}"
}

///|
pub fn boundary_probe() -> String {
  let config = EvaluationConfig::new(-4, 0, -1).with_window(0)
  let schedule = EpsilonSchedule::new(-1.0, 2.0, 0)
  let buffer = ReplayBuffer::new(0)
  let invalid = buffer.at(-1) is None
  "episodes=\{config.episodes},max_steps=\{config.max_steps},seed=\{config.seed},epsilon0=\{schedule.value(0)},invalid_at=\{invalid}"
}

///|
pub fn performance_budget(episodes : Int, max_steps : Int) -> Int {
  let safe_episodes = if episodes < 0 { 0 } else { episodes }
  let safe_steps = if max_steps < 0 { 0 } else { max_steps }
  safe_episodes * safe_steps
}

///|
pub fn benchmark_manifest() -> String {
  "MoonRLLab benchmark manifest v1\n" +
  benchmark_catalog() +
  "budget=bounded episodes and deterministic seeds\n"
}