///|
/// A small in-process decision ledger for tests, demos, and service health
/// dashboards. It stores aggregate counters only; no user identifiers are
/// retained, which keeps the default implementation privacy-friendly.
pub(all) struct DecisionLedger {
  mut total : Int
  mut matches : Int
  mut defaults : Int
  mut disabled : Int
  mut target_misses : Int
  mut rollout_misses : Int
  mut type_mismatches : Int
  mut reason_counts : Map[String, Int]
} derive(Debug)

///|
pub fn new_ledger() -> DecisionLedger {
  {
    total: 0,
    matches: 0,
    defaults: 0,
    disabled: 0,
    target_misses: 0,
    rollout_misses: 0,
    type_mismatches: 0,
    reason_counts: Map([]),
  }
}

///|
pub fn DecisionLedger::record(
  self : DecisionLedger,
  evaluation : ValueEvaluation,
) -> Unit {
  self.total += 1
  if evaluation.is_match() {
    self.matches += 1
  }
  match evaluation.reason {
    "default" => self.defaults += 1
    "disabled" => self.disabled += 1
    "target_miss" => self.target_misses += 1
    "rollout_miss" => self.rollout_misses += 1
    "type_mismatch" => self.type_mismatches += 1
    _ => ()
  }
  match self.reason_counts.get(evaluation.reason) {
    Some(count) => self.reason_counts[evaluation.reason] = count + 1
    None => self.reason_counts[evaluation.reason] = 1
  }
}

///|
pub fn DecisionLedger::record_batch(
  self : DecisionLedger,
  result : BatchResult,
) -> Unit {
  for evaluation in result.evaluations {
    self.record(evaluation)
  }
}

///|
pub fn DecisionLedger::merge(
  self : DecisionLedger,
  other : DecisionLedger,
) -> Unit {
  self.total += other.total
  self.matches += other.matches
  self.defaults += other.defaults
  self.disabled += other.disabled
  self.target_misses += other.target_misses
  self.rollout_misses += other.rollout_misses
  self.type_mismatches += other.type_mismatches
  for reason, count in other.reason_counts {
    match self.reason_counts.get(reason) {
      Some(previous) => self.reason_counts[reason] = previous + count
      None => self.reason_counts[reason] = count
    }
  }
}

///|
pub fn DecisionLedger::count(self : DecisionLedger, reason : String) -> Int {
  self.reason_counts.get_or_default(reason, 0)
}

///|
pub fn DecisionLedger::success_ratio(self : DecisionLedger) -> Double {
  if self.total == 0 {
    0.0
  } else {
    self.matches.to_double() / self.total.to_double()
  }
}

///|
pub fn DecisionLedger::reset(self : DecisionLedger) -> Unit {
  self.total = 0
  self.matches = 0
  self.defaults = 0
  self.disabled = 0
  self.target_misses = 0
  self.rollout_misses = 0
  self.type_mismatches = 0
  self.reason_counts.clear()
}

///|
pub fn DecisionLedger::summary(self : DecisionLedger) -> String {
  "decisions=" +
  self.total.to_string() +
  ", matches=" +
  self.matches.to_string() +
  ", defaults=" +
  self.defaults.to_string() +
  ", disabled=" +
  self.disabled.to_string() +
  ", target_miss=" +
  self.target_misses.to_string() +
  ", rollout_miss=" +
  self.rollout_misses.to_string() +
  ", type_mismatch=" +
  self.type_mismatches.to_string()
}

///|
pub fn DecisionLedger::reason_report(self : DecisionLedger) -> String {
  let keys = []
  for key in self.reason_counts.keys() {
    keys.push(key)
  }
  keys.sort()
  let lines = []
  for key in keys {
    lines.push(key + "=" + self.count(key).to_string())
  }
  lines.join(", ")
}

///|
pub fn[P : FeatureProvider] evaluate_with_ledger(
  provider : P,
  requests : Array[EvaluationRequest],
  ctx : EvalContext,
  ledger : DecisionLedger,
) -> BatchResult {
  let result = evaluate_batch(provider, requests, ctx)
  ledger.record_batch(result)
  result
}