///|
/// Logical scheduling counters used by offline simulations. target_batches
/// is not an observed number of model API calls; TreeExperiment separately
/// records actual target_queries from its provider callbacks.
pub struct DecodeMetrics {
  mut target_batches : Int
  mut draft_tokens : Int
  mut emitted_tokens : Int
  mut accepted_tokens : Int
  mut rejected_rounds : Int
}

///|
pub fn DecodeMetrics::empty() -> DecodeMetrics {
  {
    target_batches: 0,
    draft_tokens: 0,
    emitted_tokens: 0,
    accepted_tokens: 0,
    rejected_rounds: 0,
  }
}

///|
/// Copy counters to avoid mutating historical reports through shared state.
pub fn DecodeMetrics::copy(self : DecodeMetrics) -> DecodeMetrics {
  {
    target_batches: self.target_batches,
    draft_tokens: self.draft_tokens,
    emitted_tokens: self.emitted_tokens,
    accepted_tokens: self.accepted_tokens,
    rejected_rounds: self.rejected_rounds,
  }
}

///|
pub fn DecodeMetrics::record(
  self : DecodeMetrics,
  result : VerifyResult,
  proposal : DraftProposal,
) -> Unit {
  self.target_batches = self.target_batches + 1
  self.draft_tokens = self.draft_tokens + proposal.length()
  self.emitted_tokens = self.emitted_tokens + result.emitted.length()
  self.accepted_tokens = self.accepted_tokens + result.accepted_count
  if result.rejected_at is Some(_) {
    self.rejected_rounds = self.rejected_rounds + 1
  }
}

///|
pub fn DecodeMetrics::acceptance_rate(self : DecodeMetrics) -> Double {
  if self.draft_tokens == 0 {
    0.0
  } else {
    self.accepted_tokens.to_double() / self.draft_tokens.to_double()
  }
}

///|
pub fn DecodeMetrics::tokens_per_target_batch(self : DecodeMetrics) -> Double {
  if self.target_batches == 0 {
    0.0
  } else {
    self.emitted_tokens.to_double() / self.target_batches.to_double()
  }
}