///|
/// A mutable conversation: an ordered list of messages plus an optional
/// system prompt, with helpers for appending turns, estimating token usage,
/// and trimming history to fit a budget.
///
/// This is a convenience layer over the raw `Array[Message]`; it does not
/// itself call the API — turn it into a request with `to_request`.
pub struct Conversation {
  mut system : String?
  messages : Array[Message]
}

///|
/// Create an empty conversation, optionally with a system prompt.
pub fn Conversation::new(system? : String) -> Conversation {
  { system, messages: [] }
}

///|
/// Set (or replace) the system prompt.
pub fn Conversation::set_system(self : Conversation, prompt : String) -> Unit {
  self.system = Some(prompt)
}

///|
/// Append a user message.
pub fn Conversation::user(self : Conversation, text : String) -> Unit {
  self.messages.push(Message::user(text))
}

///|
/// Append an assistant message.
pub fn Conversation::assistant(self : Conversation, text : String) -> Unit {
  self.messages.push(Message::assistant(text))
}

///|
/// Append the assistant message from a `ChatResponse` (the first choice), so
/// the reply becomes part of the ongoing history. Returns the appended text.
pub fn Conversation::add_response(
  self : Conversation,
  response : ChatResponse,
) -> String {
  let text = response.text()
  self.messages.push(Message::assistant(text))
  text
}

///|
/// Append a tool-result message.
pub fn Conversation::tool_result(
  self : Conversation,
  tool_call_id : String,
  content : String,
) -> Unit {
  self.messages.push(Message::tool_result(tool_call_id, content))
}

///|
/// Append an arbitrary message.
pub fn Conversation::push(self : Conversation, message : Message) -> Unit {
  self.messages.push(message)
}

///|
/// The number of messages currently in the history (excluding the system
/// prompt).
pub fn Conversation::length(self : Conversation) -> Int {
  self.messages.length()
}

///|
/// Snapshot the full message list including the system prompt (if any) as the
/// leading `system` message.
pub fn Conversation::to_messages(self : Conversation) -> Array[Message] {
  let out = []
  if self.system is Some(s) {
    out.push(Message::system(s))
  }
  for m in self.messages {
    out.push(m)
  }
  out
}

///|
/// Build a `ChatRequest` for `model` from the conversation's current state.
pub fn Conversation::to_request(
  self : Conversation,
  model : String,
) -> ChatRequest {
  ChatRequest::new(model, self.to_messages())
}

///|
/// A rough token-count estimate for a piece of text.
///
/// Uses the widely-cited heuristic of ~4 characters per token. This is an
/// approximation for budgeting only, not an exact tokenizer.
pub fn estimate_tokens(text : String) -> Int {
  let chars = text.length()
  // Round up, and count at least 1 token for any non-empty text.
  if chars == 0 {
    0
  } else {
    (chars + 3) / 4
  }
}

///|
/// Estimate the token count of a single message, including a small overhead
/// per message for role/formatting (mirrors OpenAI's ~4 tokens/message).
pub fn estimate_message_tokens(message : Message) -> Int {
  let text = match message.content {
    Str(s) => s
    Parts(parts) => {
      let buf = StringBuilder::new()
      for p in parts {
        if p is Text(t) {
          buf.write_string(t)
        }
      }
      buf.to_string()
    }
  }
  estimate_tokens(text) + 4
}

///|
/// Estimate the total token count of the conversation (system + all messages).
pub fn Conversation::estimate_tokens(self : Conversation) -> Int {
  let mut total = 0
  if self.system is Some(s) {
    total = total + estimate_tokens(s) + 4
  }
  for m in self.messages {
    total = total + estimate_message_tokens(m)
  }
  total
}

///|
/// Trim the oldest messages until the estimated token count fits within
/// `max_tokens`, preserving the system prompt and the most recent messages.
///
/// Returns the number of messages dropped.
pub fn Conversation::trim_to_budget(
  self : Conversation,
  max_tokens : Int,
) -> Int {
  let mut dropped = 0
  // Drop from the front (oldest) while over budget and messages remain.
  while self.estimate_tokens() > max_tokens && self.messages.length() > 1 {
    self.messages.remove(0) |> ignore
    dropped = dropped + 1
  }
  dropped
}

///|
/// Remove all messages (keeping the system prompt).
pub fn Conversation::clear(self : Conversation) -> Unit {
  self.messages.clear()
}