// High-level tuner: runs CMA-ES optimization loop

/// Configuration for a tuning run
pub(all) struct TuneConfig {
  param_specs : Array[ParamSpec]
  targets : Array[BalanceTarget]
  seeds : Array[Int]
  max_generations : Int
  sigma : Double
  rng_seed : Int
  /// Log interval: print progress every N generations (0 = no logging)
  log_interval : Int
}

pub fn TuneConfig::default(
  param_specs : Array[ParamSpec],
  targets : Array[BalanceTarget],
) -> TuneConfig {
  {
    param_specs,
    targets,
    seeds: [42, 137, 256, 512],
    max_generations: 40,
    sigma: 0.3,
    rng_seed: 42,
    log_interval: 10,
  }
}

/// Run CMA-ES optimization
pub fn tune(
  config : TuneConfig,
  sim : (Array[Double], Int) -> Metrics,
) -> TuneResult {
  let initial = initial_params(config.param_specs)
  let cma = @optimizer.CmaEs::new(initial, config.sigma, seed=config.rng_seed)
  // Evaluate baseline
  let (base_loss, _) = compute_loss_averaged(sim, initial, config.targets, config.seeds)
  cma.best_loss = base_loss
  for i = 0; i < cma.n; i = i + 1 {
    cma.best_params[i] = initial[i]
  }
  for gen = 0; gen < config.max_generations; gen = gen + 1 {
    let population = cma.sample_population()
    let fitnesses : Array[(Double, Int)] = []
    for i, candidate in population {
      let clamped = clamp_params(candidate, config.param_specs)
      let (loss, _) = compute_loss_averaged(sim, clamped, config.targets, config.seeds)
      fitnesses.push((loss, i))
    }
    fitnesses.sort_by(fn(a, b) { a.0.compare(b.0) })
    let sorted_pop : Array[Array[Double]] = []
    for entry in fitnesses {
      sorted_pop.push(population[entry.1])
    }
    cma.update(sorted_pop)
    let gen_best = fitnesses[0].0
    if gen_best < cma.best_loss {
      cma.best_loss = gen_best
      let best_arr = sorted_pop[0]
      for i = 0; i < cma.n; i = i + 1 {
        cma.best_params[i] = best_arr[i]
      }
    }
    if config.log_interval > 0 &&
       ((gen + 1) % config.log_interval == 0 || gen == 0 || gen == config.max_generations - 1) {
      println(
        "[Gen " + (gen + 1).to_string() + "/" + config.max_generations.to_string() +
        "] best=" + fmt_f(cma.best_loss) + " sigma=" + fmt_f(cma.sigma),
      )
    }
  }
  let best = clamp_params(cma.best_result(), config.param_specs)
  let (final_loss, final_metrics) = compute_loss_averaged(
    sim, best, config.targets, config.seeds,
  )
  { params: best, loss: final_loss, generation: cma.generation, metrics: final_metrics }
}

fn fmt_f(v : Double) -> String {
  let sign = if v < 0.0 { "-" } else { "" }
  let abs_v = if v < 0.0 { -v } else { v }
  let int_part = abs_v.to_int()
  let frac = ((abs_v - int_part.to_double()) * 10000.0).to_int()
  let s = frac.to_string()
  let p = if frac < 10 {
    "000" + s
  } else if frac < 100 {
    "00" + s
  } else if frac < 1000 {
    "0" + s
  } else {
    s
  }
  sign + int_part.to_string() + "." + p
}