// ============================================================
// Bounded execution trace
//
// Printing every tick is useful during development but is too noisy for a
// live game server or agent service. TraceBuffer keeps a bounded in-memory
// window, records the blackboard size seen by each node, and exports stable
// rows for diagnostics. The ring behavior prevents a long-running process
// from retaining an unbounded history.
// ============================================================

///|
/// One observed node execution in a trace window.
pub struct TraceEntry {
  sequence : Int
  label : String
  status : Status
  blackboard_size : Int
}

///|
/// Monotonic sequence number of this observation.
pub fn TraceEntry::sequence(self : TraceEntry) -> Int {
  self.sequence
}

///|
/// Application-provided node label.
pub fn TraceEntry::label(self : TraceEntry) -> String {
  self.label
}

///|
/// Status returned by the node.
pub fn TraceEntry::status(self : TraceEntry) -> Status {
  self.status
}

///|
/// Number of values visible when the node returned.
pub fn TraceEntry::blackboard_size(self : TraceEntry) -> Int {
  self.blackboard_size
}

///|
/// Convert a trace entry into a stable CSV row.
pub fn TraceEntry::to_csv(self : TraceEntry) -> String {
  "\{self.sequence},\{self.label},\{self.status.to_string()},\{self.blackboard_size}"
}

///|
/// Aggregate counts for the current trace window.
pub struct TraceSummary {
  total : Int
  successes : Int
  failures : Int
  running : Int
}

///|
/// Total observations in the summary.
pub fn TraceSummary::total(self : TraceSummary) -> Int {
  self.total
}

///|
/// Successful observations in the summary.
pub fn TraceSummary::successes(self : TraceSummary) -> Int {
  self.successes
}

///|
/// Failed observations in the summary.
pub fn TraceSummary::failures(self : TraceSummary) -> Int {
  self.failures
}

///|
/// Running observations in the summary.
pub fn TraceSummary::running(self : TraceSummary) -> Int {
  self.running
}

///|
/// A bounded FIFO trace window.
pub struct TraceBuffer {
  entries : Array[TraceEntry]
  capacity : Int
  next_sequence : Ref[Int]
}

///|
/// Create a trace buffer. Non-positive capacities are normalized to one.
pub fn TraceBuffer::new(capacity : Int) -> TraceBuffer {
  {
    entries: [],
    capacity: if capacity > 0 {
      capacity
    } else {
      1
    },
    next_sequence: Ref::new(1),
  }
}

///|
/// Maximum number of entries retained by this buffer.
pub fn TraceBuffer::capacity(self : TraceBuffer) -> Int {
  self.capacity
}

///|
/// Number of entries currently retained.
pub fn TraceBuffer::size(self : TraceBuffer) -> Int {
  self.entries.length()
}

///|
/// Return whether the trace window has no observations.
pub fn TraceBuffer::is_empty(self : TraceBuffer) -> Bool {
  self.entries.length() == 0
}

///|
/// Record one node result and evict the oldest entry when full.
pub fn TraceBuffer::record(
  self : TraceBuffer,
  label : String,
  status : Status,
  bb : Blackboard,
) -> Unit {
  if self.entries.length() >= self.capacity {
    let _ = self.entries.remove(0)
  }
  self.entries.push({
    sequence: self.next_sequence.get(),
    label,
    status,
    blackboard_size: bb.size(),
  })
  self.next_sequence.set(self.next_sequence.get() + 1)
}

///|
/// Return retained entries from oldest to newest.
pub fn TraceBuffer::entries(self : TraceBuffer) -> Array[TraceEntry] {
  let result : Array[TraceEntry] = []
  for entry in self.entries {
    result.push(entry)
  }
  result
}

///|
/// Return the newest retained entry, if any.
pub fn TraceBuffer::last(self : TraceBuffer) -> TraceEntry? {
  if self.entries.length() == 0 {
    None
  } else {
    Some(self.entries[self.entries.length() - 1])
  }
}

///|
/// Remove all observations while keeping sequence numbers monotonic.
pub fn TraceBuffer::clear(self : TraceBuffer) -> Unit {
  self.entries.clear()
}

///|
/// Count observations with one status in the current window.
pub fn TraceBuffer::count_status(self : TraceBuffer, expected : Status) -> Int {
  let mut count = 0
  for entry in self.entries {
    if entry.status == expected {
      count = count + 1
    }
  }
  count
}

///|
/// Summarize the currently retained window.
pub fn TraceBuffer::summary(self : TraceBuffer) -> TraceSummary {
  let mut successes = 0
  let mut failures = 0
  let mut running = 0
  for entry in self.entries {
    match entry.status {
      Status::BTSuccess => successes = successes + 1
      Status::BTFailure => failures = failures + 1
      Status::BTRunning => running = running + 1
    }
  }
  { total: self.entries.length(), successes, failures, running }
}

///|
/// Export a stable header and all retained observations as CSV.
pub fn TraceBuffer::to_csv(self : TraceBuffer) -> String {
  let output = StringBuilder::new()
  output.write_string("sequence,label,status,blackboard_size\n")
  for entry in self.entries {
    output.write_string(entry.to_csv())
    output.write_string("\n")
  }
  output.to_string()
}

///|
/// Wrap a node and record every returned status in a bounded trace.
pub fn trace_buffer_node(
  label : String,
  child : Node,
  trace : TraceBuffer,
) -> Node {
  let tick = fn(bb) {
    let status = child.tick(bb)
    trace.record(label, status, bb)
    status
  }
  let reset = fn() { child.reset() }
  Node::new(tick, reset)
}

///|
/// Record a completed tree-runner frame with a standard label.
pub fn trace_runner_tick(runner : TreeRunner, trace : TraceBuffer) -> Status {
  let status = runner.tick()
  trace.record("TreeRunner", status, runner.blackboard())
  status
}

///|
/// Export only entries whose status matches the requested value.
pub fn TraceBuffer::filter_status(
  self : TraceBuffer,
  expected : Status,
) -> Array[TraceEntry] {
  let result : Array[TraceEntry] = []
  for entry in self.entries {
    if entry.status == expected {
      result.push(entry)
    }
  }
  result
}

///|
/// Return the first retained entry with the supplied label.
pub fn TraceBuffer::find_label(
  self : TraceBuffer,
  label : String,
) -> TraceEntry? {
  for entry in self.entries {
    if entry.label == label {
      return Some(entry)
    }
  }
  None
}

///|
/// Return the total number of observations recorded since construction.
pub fn TraceBuffer::recorded_total(self : TraceBuffer) -> Int {
  self.next_sequence.get() - 1
}

///|
/// Return whether the buffer has reached its retention limit.
pub fn TraceBuffer::is_full(self : TraceBuffer) -> Bool {
  self.entries.length() >= self.capacity
}