///|
struct Tool {
  mut enabled : Bool
  tool : @tool.AgentTool
}

///|
fn Tool::new(tool : @tool.AgentTool, enabled~ : Bool) -> Tool {
  { enabled, tool }
}

///|
pub fn Tool::desc(self : Tool) -> @tool.ToolDesc {
  self.tool.desc()
}

///|
async fn Tool::call(
  self : Tool,
  args : Json,
) -> @tool.ToolResult[(Json, String)] noraise {
  guard self.enabled else {
    return @tool.error("Tool '\{self.tool.desc().name}' is disabled.")
  }
  self.tool.call(args)
}

///|
pub fn Tool::enabled(self : Tool) -> Bool {
  self.enabled
}

///|
/// Enables or disables tools based on the provided set of tool names.
///
/// Parameters:
///
/// * `agent` : The agent instance whose tools will be enabled or disabled.
/// * `tool_names` : A set containing the names of tools that should be enabled.
///   Tools not in this set will be disabled.
pub fn Agent::set_enabled_tools(
  agent : Agent,
  tool_names : Set[String],
) -> Unit {
  for name, tool in agent.tools {
    if tool_names.contains(name) {
      tool.enabled = true
    } else {
      tool.enabled = false
    }
  }
}

///|
pub fn Agent::tools(self : Agent) -> Map[String, Tool] {
  self.tools
}

///|
/// Executes a tool call requested by the AI model and returns the result.
///
/// This function handles the complete tool execution lifecycle:
/// 1. Validates that the requested tool exists
/// 2. Emits a `PreToolCall` event for logging/monitoring
/// 3. Parses and validates the tool arguments
/// 4. Executes the tool with the provided arguments
/// 5. Emits a `PostToolCall` event with the result
/// 6. Returns the result formatted as a tool message
///
/// Parameters:
///
/// * `agent` : The agent instance containing the available tools.
/// * `tool_call` : The tool call request from the AI model, containing the tool
///   name, arguments, and call ID.
///
/// Returns a `@ai.Message` containing either:
/// * The tool's successful output
/// * An error message if the tool doesn't exist, arguments are invalid, or
///   execution fails
///
/// The returned message maintains the `tool_call_id` to correlate with the
/// original request.
async fn Agent::execute_tool(
  agent : Agent,
  tool_call : @ai.ToolCall,
) -> @ai.Message {
  agent.emit(PreToolCall(tool_call))
  guard agent.tools.get(tool_call.name) is Some(tool) else {
    let rendered = "Unknown tool: \{tool_call.name}"
    let result : Result[Json, Json] = Err(
      Json::object({ "error": Json::string(rendered) }),
    )
    agent.emit(PostToolCall(tool_call, result~, rendered~))
    return @ai.tool_message(content=rendered, tool_call_id=tool_call.id)
  }
  let arguments : Json = match tool_call.arguments {
    None | Some("") =>
      // when models like haiku return empty arguments, we treat it as empty object 
      {}
    Some(arguments) =>
      @json.parse(arguments) catch {
        error => {
          let result = Err(error.to_json())
          let rendered = "Error parsing tool arguments: \{error}, arguments: \{arguments}"
          agent.emit(PostToolCall(tool_call, result~, rendered~))
          return @ai.tool_message(content=rendered, tool_call_id=tool_call.id)
        }
      }
  }
  let (result, rendered) = match tool.call(arguments) {
    Ok((json, text)) => (Ok(json), text)
    Error(error, text) => (Err(error.to_json()), text)
  }
  agent.emit(PostToolCall(tool_call, result~, rendered~))
  @ai.tool_message(content=rendered, tool_call_id=tool_call.id)
}

///|
/// Adds multiple tools to the agent's available tools collection.
///
/// This is a convenience function for adding multiple `AgentTool` instances
/// at once. Each tool is added individually and triggers a `ToolAdded` event.
///
/// Parameters:
///
/// * `agent` : The agent instance to add the tools to.
/// * `tools` : An array of `AgentTool` instances to be registered with the agent.
pub fn Agent::add_tools(agent : Agent, tools : Array[@tool.AgentTool]) -> Unit {
  for tool in tools {
    agent.add_agent_tool(tool)
  }
}

///|
/// Adds a single agent tool to the agent's tools collection.
///
/// The tool is indexed by its name in the agent's tool map and a `ToolAdded`
/// event is emitted for logging and monitoring purposes.
///
/// Parameters:
///
/// * `agent` : The agent instance to add the tool to.
/// * `tool` : The agent tool to register.
fn Agent::add_agent_tool(
  agent : Agent,
  tool : @tool.AgentTool,
  enabled? : Bool = true,
) -> Unit {
  let desc = tool.desc()
  agent.tools[desc.name] = Tool::new(tool, enabled~)
  agent.emit(ToolAdded(desc))
}

///|
/// Adds a tool to the agent's available tools collection.
///
/// Parameters:
///
/// * `agent` : The agent instance to add the tool to.
/// * `tool` : The tool to be added, which will be indexed by its name for
///   future tool calls.
pub fn[Output : ToJson] Agent::add_tool(
  agent : Agent,
  tool : @tool.Tool[Output],
  enabled? : Bool = true,
) -> Unit {
  agent.add_agent_tool(tool.to_agent_tool(), enabled~)
}

///|
///  Build the tools array for this API call
/// Convert each registered tool into OpenAI's tool parameter format
fn Agent::tools_desc(self : Agent) -> Array[@tool.ToolDesc] {
  let descs = []
  for _, tool in self.tools {
    if tool.enabled {
      descs.push(tool.desc())
    }
  }
  descs
}