// Copyright (c) 2026 Yingjie Shang
// agent-observability is licensed under Mulan PSL v2.

///|
/// An agent that orchestrates LLM chat with tool calls.
///
/// The agent maintains conversation history and automatically executes
/// any tool calls requested by the LLM, feeding results back until a
/// final response is produced.
pub struct Agent {
  client : Client
  messages : Array[Message]
  tools : Array[Tool]
  max_tool_turns : Int
  capture_content : Bool
}

///|
/// Create a new agent with the given client and optional tools.
pub fn Agent::new(
  client : Client,
  tools? : Array[Tool] = [],
  max_tool_turns? : Int = 10,
  capture_content? : Bool = true,
) -> Agent {
  { client, messages: [], tools, max_tool_turns, capture_content }
}

///|
/// Record of a single tool call executed during an agent turn.
pub struct ToolCallRecord {
  call : ToolCall
  result : String
} derive(Debug)

///|
/// Result of a single agent turn, including the assistant's reply
/// and any tool calls that were executed.
pub struct AgentTurnResult {
  reply : String
  tool_calls : Array[ToolCallRecord]
} derive(Debug)

///|
/// Run one turn of conversation with the agent.
///
/// The prompt is appended to the conversation history, then the agent
/// loops over LLM responses and tool calls until a final text response
/// is received. Returns the assistant's reply and a record of any tool
/// calls executed during the turn.
pub async fn Agent::run(self : Agent, prompt : String) -> AgentTurnResult {
  let tracer = @telemetry.tracer("cybershang/agent-o11y-demo/agent")
  let meter = @telemetry.meter("cybershang/agent-o11y-demo/agent")
  let span_input = if self.capture_content { prompt } else { "" }
  let span = @telemetry.start_agent_turn_span(
    tracer,
    span_input,
    self.max_tool_turns,
  )
  // No async context storage by design — explicit is better than implicit.
  let parent_context = span.context()

  @telemetry.set_int(
    span,
    "app.agent.tool_count",
    self.tools.length().to_int64(),
  )
  @telemetry.set_int(span, "app.prompt.length", prompt.length().to_int64())

  self.messages.push(Message::new(content=Some(prompt)))

  let mut turn = 0
  let mut has_tool_calls = true
  let mut final_reply = ""
  let executed : Array[ToolCallRecord] = []
  while has_tool_calls && turn < self.max_tool_turns {
    turn = turn + 1
    let response = self.client.chat(
      self.messages,
      tools=self.tools,
      parent_context~,
    )
    if response.finish_reason == "tool_calls" {
      // Add assistant message with tool_calls
      self.messages.push(
        Message::new(role="assistant", tool_calls=Some(response.tool_calls)),
      )
      // Execute each tool call and add tool results
      for call in response.tool_calls {
        let result = execute_tool(call.name, call.arguments, parent_context~)
        executed.push({ call, result })
        self.messages.push(
          Message::new(
            role="tool",
            content=Some(result),
            tool_call_id=Some(call.id),
          ),
        )
      }
      // Continue loop to send tool results back to LLM
    } else {
      has_tool_calls = false
      final_reply = match response.content {
        Some(content) => content
        None => ""
      }
    }
  }

  // If we hit the turn limit without a final reply, report it
  if has_tool_calls {
    final_reply = "[Agent reached maximum tool-call turns without producing a final reply.]"
    @telemetry.set_turn_exhausted(span)
  }

  @telemetry.set_int(
    span,
    "app.response.length",
    final_reply.length().to_int64(),
  )
  @telemetry.set_bool(span, "app.agent.reached_max_turns", has_tool_calls)

  let span_output = if self.capture_content { final_reply } else { "" }
  @telemetry.set_turn(span, turn, executed.length(), span_output)
  @telemetry.record_turn(meter, max_tool_turns_reached=has_tool_calls)
  @telemetry.end_span(span)

  { reply: final_reply, tool_calls: executed }
}