///|
/// A trait-driven training entry point. The agent receives the next action as
/// an optional hint: SARSA uses it, while Q-learning ignores it. This keeps
/// the episode loop independent of a concrete environment or agent type.
pub fn[Env : Environment, Ag : Agent, Log : Logger] Trainer::train(
  self : Trainer,
  env : Env,
  agent : Ag,
  logger : Log,
) -> TrainingReport {
  let report = TrainingReport::{
    label: "generic-agent",
    episodes: self.episodes,
    rewards: Array::make(self.episodes, 0.0),
    steps: Array::make(self.episodes, 0),
    goal_hits: 0,
    final_epsilon: 0.0,
  }
  for episode in 0.. TrainingReport {
  let env = GridWorldEnv::new()
  let agent = SARSAAgent::new(
    env.state_space(),
    env.actions(),
    EpsilonGreedyPolicy::new(0.12, seed),
    0.25,
    0.95,
  )
  Trainer::new(episodes, max_steps).train(
    env,
    agent,
    ConsoleLogger::new("generic-sarsa"),
  )
}

///|
pub fn train_gridworld_qlearning_generic(
  episodes : Int,
  max_steps : Int,
  seed : Int,
) -> TrainingReport {
  let env = GridWorldEnv::new()
  let agent = QLearningAgent::new(
    env.state_space(),
    env.actions(),
    EpsilonGreedyPolicy::new(0.12, seed),
    0.25,
    0.95,
  )
  Trainer::new(episodes, max_steps).train(
    env,
    agent,
    ConsoleLogger::new("generic-q-learning"),
  )
}

///|
/// A silent logger for library and CI consumers that need structured results
/// without writing progress lines to stdout.
pub struct MemoryLogger {
  mut started : Int
  mut transitions : Int
  mut finished : Int
  mut last_report : TrainingReport?
} derive(Debug)

///|
pub fn MemoryLogger::new() -> MemoryLogger {
  { started: 0, transitions: 0, finished: 0, last_report: None }
}

///|
pub fn MemoryLogger::started_count(self : MemoryLogger) -> Int {
  self.started
}

///|
pub fn MemoryLogger::transition_count(self : MemoryLogger) -> Int {
  self.transitions
}

///|
pub fn MemoryLogger::finished_count(self : MemoryLogger) -> Int {
  self.finished
}

///|
pub impl Logger for MemoryLogger with fn start_episode(self, _episode) {
  self.started = self.started + 1
}

///|
pub impl Logger for MemoryLogger with fn step(
  self,
  _episode,
  _state,
  _action,
  _reward,
  _next_state,
  _done,
  _step,
) {
  self.transitions = self.transitions + 1
}

///|
pub impl Logger for MemoryLogger with fn finish_episode(self, _record) {
  self.finished = self.finished + 1
}

///|
pub impl Logger for MemoryLogger with fn finish(self, report) {
  self.last_report = Some(report)
}

///|
pub fn train_with_memory_logger(
  episodes : Int,
  max_steps : Int,
  seed : Int,
) -> TrainingReport {
  let env = GridWorldEnv::new()
  let agent = SARSAAgent::new(
    env.state_space(),
    env.actions(),
    EpsilonGreedyPolicy::new(0.12, seed),
    0.25,
    0.95,
  )
  Trainer::new(episodes, max_steps).train(env, agent, MemoryLogger::new())
}

///|
pub fn train_gridworld_expected_sarsa_generic(
  episodes : Int,
  max_steps : Int,
  seed : Int,
) -> TrainingReport {
  let env = GridWorldEnv::new()
  let agent = ExpectedSARSAAgent::new(
    env.state_space(),
    env.actions(),
    EpsilonGreedyPolicy::new(0.12, seed),
    0.25,
    0.95,
  )
  Trainer::new(episodes, max_steps).train(env, agent, MemoryLogger::new())
}

///|
pub fn train_cliff_qlearning_generic(
  episodes : Int,
  max_steps : Int,
  seed : Int,
) -> TrainingReport {
  let env = CliffWalkingEnv::new()
  let agent = QLearningAgent::new(
    env.state_space(),
    env.actions(),
    EpsilonGreedyPolicy::new(0.10, seed),
    0.20,
    0.95,
  )
  Trainer::new(episodes, max_steps).train(env, agent, MemoryLogger::new())
}