/*
 * Copyright 2024 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package compose

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"runtime/debug"
	"sort"
	"strings"
	"sync"

	"github.com/bytedance/sonic"

	"github.com/cloudwego/eino/callbacks"
	"github.com/cloudwego/eino/components"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/internal/safe"
	"github.com/cloudwego/eino/schema"
)

type toolsNodeOptions struct {
	ToolOptions []tool.Option
	ToolList    []tool.BaseTool

	ToolAliases map[string]ToolAliasConfig
}

// ToolsNodeOption is the option func type for ToolsNode.
type ToolsNodeOption func(o *toolsNodeOptions)

// WithToolOption adds tool options to the ToolsNode.
func WithToolOption(opts ...tool.Option) ToolsNodeOption {
	return func(o *toolsNodeOptions) {
		o.ToolOptions = append(o.ToolOptions, opts...)
	}
}

// WithToolList sets the tool list for the ToolsNode.
func WithToolList(tool ...tool.BaseTool) ToolsNodeOption {
	return func(o *toolsNodeOptions) {
		o.ToolList = tool
	}
}

// WithToolAliases sets the tool aliases for the ToolsNode call option.
// When used with WithToolList, it overrides the global alias configuration for the dynamic tool list.
// When used alone (without WithToolList), it replaces the global alias configuration while keeping the original tool list.
func WithToolAliases(toolAliases map[string]ToolAliasConfig) ToolsNodeOption {
	return func(o *toolsNodeOptions) {
		o.ToolAliases = toolAliases
	}
}

// ToolsNode represents a node capable of executing tools within a graph.
// The Graph Node interface is defined as follows:
//
//	Invoke(ctx context.Context, input *schema.Message, opts ...ToolsNodeOption) ([]*schema.Message, error)
//	Stream(ctx context.Context, input *schema.Message, opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error)
//
// Input: An AssistantMessage containing ToolCalls
// Output: An array of ToolMessage where the order of elements corresponds to the order of ToolCalls in the input
type ToolsNode struct {
	tuple                             *toolsTuple
	tools                             []tool.BaseTool
	unknownToolHandler                func(ctx context.Context, name, input string) (string, error)
	executeSequentially               bool
	toolArgumentsHandler              func(ctx context.Context, name, input string) (string, error)
	toolCallMiddlewares               []InvokableToolMiddleware
	streamToolCallMiddlewares         []StreamableToolMiddleware
	enhancedToolCallMiddlewares       []EnhancedInvokableToolMiddleware
	enhancedStreamToolCallMiddlewares []EnhancedStreamableToolMiddleware
	toolAliasConfigs                  map[string]ToolAliasConfig
}

// ToolInput represents the input parameters for a tool call execution.
type ToolInput struct {
	// Name is the name of the tool to be executed.
	Name string
	// Arguments contains the arguments for the tool call.
	Arguments string
	// CallID is the unique identifier for this tool call.
	CallID string
	// CallOptions contains tool options for the execution.
	CallOptions []tool.Option
}

// ToolOutput represents the result of a non-streaming tool call execution.
type ToolOutput struct {
	// Result contains the string output from the tool execution.
	Result string
}

// StreamToolOutput represents the result of a streaming tool call execution.
type StreamToolOutput struct {
	// Result is a stream reader that provides access to the tool's streaming output.
	Result *schema.StreamReader[string]
}

// EnhancedInvokableToolOutput represents the result of a non-streaming enhanced tool call execution.
// It supports returning structured multimodal content (text, images, audio, video, files) from tools.
type EnhancedInvokableToolOutput struct {
	// Result contains the structured multimodal output from the tool execution.
	Result *schema.ToolResult
}

// EnhancedStreamableToolOutput represents the result of a streaming enhanced tool call execution.
// It provides a stream reader for accessing multimodal content progressively.
type EnhancedStreamableToolOutput struct {
	// Result is a stream reader that provides access to the tool's streaming multimodal output.
	Result *schema.StreamReader[*schema.ToolResult]
}

// InvokableToolEndpoint is the function signature for non-streaming tool calls.
type InvokableToolEndpoint func(ctx context.Context, input *ToolInput) (*ToolOutput, error)

// StreamableToolEndpoint is the function signature for streaming tool calls.
type StreamableToolEndpoint func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error)

type EnhancedInvokableToolEndpoint func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error)

type EnhancedStreamableToolEndpoint func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error)

// InvokableToolMiddleware is a function that wraps InvokableToolEndpoint to add custom processing logic.
// It can be used to intercept, modify, or enhance tool call execution for non-streaming tools.
type InvokableToolMiddleware func(InvokableToolEndpoint) InvokableToolEndpoint

// StreamableToolMiddleware is a function that wraps StreamableToolEndpoint to add custom processing logic.
// It can be used to intercept, modify, or enhance tool call execution for streaming tools.
type StreamableToolMiddleware func(StreamableToolEndpoint) StreamableToolEndpoint

type EnhancedInvokableToolMiddleware func(EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint

type EnhancedStreamableToolMiddleware func(EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint

// ToolMiddleware groups middleware hooks for invokable and streamable tool calls.
type ToolMiddleware struct {
	// Invokable contains middleware function for non-streaming tool calls.
	// Note: This middleware only applies to tools that implement the InvokableTool interface.
	Invokable InvokableToolMiddleware

	// Streamable contains middleware function for streaming tool calls.
	// Note: This middleware only applies to tools that implement the StreamableTool interface.
	Streamable StreamableToolMiddleware

	// EnhancedInvokable contains middleware function for non-streaming enhanced tool calls.
	// Note: This middleware only applies to tools that implement the EnhancedInvokableTool interface.
	EnhancedInvokable EnhancedInvokableToolMiddleware

	// EnhancedStreamable contains middleware function for streaming enhanced tool calls.
	// Note: This middleware only applies to tools that implement the EnhancedStreamableTool interface.
	EnhancedStreamable EnhancedStreamableToolMiddleware
}

// ToolAliasConfig configures name and argument aliases for a single tool.
type ToolAliasConfig struct {
	// NameAliases are alternative names for this tool.
	// If the model returns any of these names, it will be resolved to the canonical tool name.
	NameAliases []string

	// ArgumentsAliases maps canonical argument keys to their alias lists.
	// key=canonical, value=[]alias. Applied to top-level JSON keys before tool execution.
	// Example: {"query": ["q", "search_term"], "limit": ["max_results", "count"]}
	ArgumentsAliases map[string][]string
}

// ToolsNodeConfig is the config for ToolsNode.
type ToolsNodeConfig struct {
	// Tools specify the list of tools can be called which are BaseTool but must implement InvokableTool or StreamableTool.
	Tools []tool.BaseTool

	// ToolAliases configures name and argument aliases for tools.
	// Key is the canonical tool name, value defines its aliases.
	// This field is optional. When provided, tool name aliases will be resolved during tool dispatch,
	// and argument aliases will be remapped before ToolArgumentsHandler (if configured) and tool execution.
	// Execution order: ArgumentsAliases remapping → ToolArgumentsHandler → tool execution
	ToolAliases map[string]ToolAliasConfig

	// UnknownToolsHandler handles tool calls for non-existent tools when LLM hallucinates.
	// This field is optional. When not set, calling a non-existent tool will result in an error.
	// When provided, if the LLM attempts to call a tool that doesn't exist in the Tools list,
	// this handler will be invoked instead of returning an error, allowing graceful handling of hallucinated tools.
	// Parameters:
	//   - ctx: The context for the tool call
	//   - name: The name of the non-existent tool
	//   - input: The tool call input generated by llm
	// Returns:
	//   - string: The response to be returned as if the tool was executed
	//   - error: Any error that occurred during handling
	UnknownToolsHandler func(ctx context.Context, name, input string) (string, error)

	// ExecuteSequentially determines whether tool calls should be executed sequentially (in order) or in parallel.
	// When set to true, tool calls will be executed one after another in the order they appear in the input message.
	// When set to false (default), tool calls will be executed in parallel.
	ExecuteSequentially bool

	// ToolArgumentsHandler allows handling of tool arguments before execution.
	// When provided, this function will be called for each tool call to process the arguments.
	// Parameters:
	//   - ctx: The context for the tool call
	//   - name: The name of the tool being called
	//   - arguments: The original arguments string for the tool
	// Returns:
	//   - string: The processed arguments string to be used for tool execution
	//   - error: Any error that occurred during preprocessing
	ToolArgumentsHandler func(ctx context.Context, name, arguments string) (string, error)

	// ToolCallMiddlewares configures middleware for tool calls.
	// Each element can contain Invokable and/or Streamable middleware.
	// Invokable middleware only applies to tools implementing InvokableTool interface.
	// Streamable middleware only applies to tools implementing StreamableTool interface.
	ToolCallMiddlewares []ToolMiddleware
}

// NewToolNode creates a new ToolsNode.
// e.g.
//
//	conf := &ToolsNodeConfig{
//		Tools: []tool.BaseTool{invokableTool1, streamableTool2},
//	}
//	toolsNode, err := NewToolNode(ctx, conf)
func NewToolNode(ctx context.Context, conf *ToolsNodeConfig) (*ToolsNode, error) {
	var middlewares []InvokableToolMiddleware
	var streamMiddlewares []StreamableToolMiddleware
	var enhancedInvokableMiddlewares []EnhancedInvokableToolMiddleware
	var enhancedStreamableMiddlewares []EnhancedStreamableToolMiddleware

	for _, m := range conf.ToolCallMiddlewares {
		if m.Invokable != nil {
			middlewares = append(middlewares, m.Invokable)
		}
		if m.Streamable != nil {
			streamMiddlewares = append(streamMiddlewares, m.Streamable)
		}
		if m.EnhancedInvokable != nil {
			enhancedInvokableMiddlewares = append(enhancedInvokableMiddlewares, m.EnhancedInvokable)
		}
		if m.EnhancedStreamable != nil {
			enhancedStreamableMiddlewares = append(enhancedStreamableMiddlewares, m.EnhancedStreamable)
		}
	}

	params := convToolsParams{
		tools:        conf.Tools,
		aliasConfigs: conf.ToolAliases,
	}
	params.middlewares.invokable = middlewares
	params.middlewares.streamable = streamMiddlewares
	params.middlewares.enhancedInvokable = enhancedInvokableMiddlewares
	params.middlewares.enhancedStreamable = enhancedStreamableMiddlewares
	tuple, err := convTools(ctx, params)
	if err != nil {
		return nil, err
	}

	return &ToolsNode{
		tuple:                             tuple,
		tools:                             conf.Tools,
		unknownToolHandler:                conf.UnknownToolsHandler,
		executeSequentially:               conf.ExecuteSequentially,
		toolArgumentsHandler:              conf.ToolArgumentsHandler,
		toolCallMiddlewares:               middlewares,
		streamToolCallMiddlewares:         streamMiddlewares,
		enhancedToolCallMiddlewares:       enhancedInvokableMiddlewares,
		enhancedStreamToolCallMiddlewares: enhancedStreamableMiddlewares,
		toolAliasConfigs:                  conf.ToolAliases,
	}, nil
}

// ToolsInterruptAndRerunExtra carries interrupt metadata for ToolsNode reruns.
type ToolsInterruptAndRerunExtra struct {
	// ToolCalls contains all tool calls from the original assistant message.
	ToolCalls []schema.ToolCall

	// ExecutedTools maps tool call IDs to their string output for successfully executed standard tools.
	ExecutedTools map[string]string

	// ExecutedEnhancedTools maps tool call IDs to their structured multimodal output for successfully executed enhanced tools.
	ExecutedEnhancedTools map[string]*schema.ToolResult

	// RerunTools contains the IDs of tool calls that need to be re-executed.
	RerunTools []string

	// RerunExtraMap stores additional metadata for each tool call that needs rerun, keyed by tool call ID.
	RerunExtraMap map[string]any
}

func init() {
	schema.RegisterName[*ToolsInterruptAndRerunExtra]("_eino_compose_tools_interrupt_and_rerun_extra")
	schema.RegisterName[*toolsInterruptAndRerunState]("_eino_compose_tools_interrupt_and_rerun_state")
}

type toolsInterruptAndRerunState struct {
	Input                 *schema.Message
	ExecutedTools         map[string]string
	ExecutedEnhancedTools map[string]*schema.ToolResult
	RerunTools            []string
}

type toolsTuple struct {
	indexes                     map[string]int
	meta                        []*executorMeta
	endpoints                   []InvokableToolEndpoint
	streamEndpoints             []StreamableToolEndpoint
	enhancedInvokableEndpoints  []EnhancedInvokableToolEndpoint
	enhancedStreamableEndpoints []EnhancedStreamableToolEndpoint
	// argsAliasMap stores reverse argument alias mappings for each tool.
	// key: canonical tool name, value: map[aliasKey]canonicalKey (alias → canonical direction)
	argsAliasMap map[string]map[string]string
	// canonicalNames stores the canonical name for each tool index
	canonicalNames []string
	// toolInfos stores the ToolInfo for each tool index, used for alias validation
	toolInfos []*schema.ToolInfo
}

// remapArgs replaces alias keys in the JSON arguments string with canonical keys.
// aliasMap: alias → canonical mapping
func remapArgs(args string, aliasMap map[string]string) (string, error) {
	if len(aliasMap) == 0 {
		return args, nil
	}

	trimmed := strings.TrimSpace(args)
	if trimmed == "" || trimmed[0] != '{' {
		return args, nil
	}

	var m map[string]json.RawMessage
	if err := sonic.Unmarshal([]byte(args), &m); err != nil {
		return args, nil
	}

	changed := false
	for alias, canonical := range aliasMap {
		if v, ok := m[alias]; ok {
			// Only replace if canonical key doesn't exist.
			// If both alias and canonical are present (e.g. {"q":"a","query":"b"}),
			// the alias key is kept as-is and passed through as an unknown field.
			if _, exists := m[canonical]; !exists {
				m[canonical] = v
				delete(m, alias)
				changed = true
			}
		}
	}

	if !changed {
		return args, nil
	}

	b, err := sonic.Marshal(m)
	return string(b), err
}

type convToolsParams struct {
	tools       []tool.BaseTool
	middlewares struct {
		invokable          []InvokableToolMiddleware
		streamable         []StreamableToolMiddleware
		enhancedInvokable  []EnhancedInvokableToolMiddleware
		enhancedStreamable []EnhancedStreamableToolMiddleware
	}
	aliasConfigs map[string]ToolAliasConfig
}

func (t *toolsTuple) applyAliasConfigs(aliasConfigs map[string]ToolAliasConfig) error {
	t.argsAliasMap = make(map[string]map[string]string)

	sortedToolNames := make([]string, 0, len(aliasConfigs))
	for toolName := range aliasConfigs {
		sortedToolNames = append(sortedToolNames, toolName)
	}
	sort.Strings(sortedToolNames)

	for _, toolName := range sortedToolNames {
		aliasConfig := aliasConfigs[toolName]
		var (
			toolIdx int
			exists  bool
		)
		if toolIdx, exists = t.indexes[toolName]; !exists {
			continue
		}

		if err := t.applyNameAliases(toolName, toolIdx, aliasConfig.NameAliases); err != nil {
			return err
		}

		if err := t.applyArgsAliases(toolName, toolIdx, aliasConfig.ArgumentsAliases); err != nil {
			return err
		}
	}

	return nil
}

// applyNameAliases validates and registers name aliases for a single tool into the indexes map.
func (t *toolsTuple) applyNameAliases(toolName string, toolIdx int, nameAliases []string) error {
	for _, alias := range nameAliases {
		if strings.TrimSpace(alias) == "" {
			return fmt.Errorf("tool '%s' has empty name alias", toolName)
		}
		if existingIdx, conflict := t.indexes[alias]; conflict {
			if existingIdx != toolIdx {
				conflictToolName := t.canonicalNames[existingIdx]
				if alias == conflictToolName {
					return fmt.Errorf("tool '%s': name alias '%s' conflicts with existing tool's canonical name", toolName, alias)
				}
				return fmt.Errorf("tool '%s': name alias '%s' conflicts with an alias already registered for tool '%s'", toolName, alias, conflictToolName)
			}
			continue
		}
		t.indexes[alias] = toolIdx
	}
	return nil
}

// applyArgsAliases validates argument aliases against the tool schema and builds a reverse alias map for a single tool.
func (t *toolsTuple) applyArgsAliases(toolName string, toolIdx int, argumentsAliases map[string][]string) error {
	if len(argumentsAliases) == 0 {
		return nil
	}

	schemaKeys := make(map[string]bool)
	if info := t.toolInfos[toolIdx]; info != nil && info.ParamsOneOf != nil {
		js, err := info.ParamsOneOf.ToJSONSchema()
		if err != nil {
			return fmt.Errorf("tool '%s': failed to parse JSON schema for alias validation: %w", toolName, err)
		}
		if js != nil && js.Properties != nil {
			for pair := js.Properties.Oldest(); pair != nil; pair = pair.Next() {
				schemaKeys[pair.Key] = true
			}
		}
	}

	reverseMap := make(map[string]string)
	sortedCanonicals := make([]string, 0, len(argumentsAliases))
	for canonical := range argumentsAliases {
		sortedCanonicals = append(sortedCanonicals, canonical)
	}
	sort.Strings(sortedCanonicals)

	for _, canonical := range sortedCanonicals {
		aliases := argumentsAliases[canonical]
		if strings.TrimSpace(canonical) == "" {
			return fmt.Errorf("tool '%s' has empty canonical argument key", toolName)
		}
		if strings.Contains(canonical, ".") {
			return fmt.Errorf("tool '%s' has unsupported '.' in canonical argument key '%s': nested field matching is not yet supported",
				toolName, canonical)
		}
		for _, alias := range aliases {
			if strings.TrimSpace(alias) == "" {
				return fmt.Errorf("tool '%s' has empty argument alias for canonical key '%s'", toolName, canonical)
			}
			if schemaKeys[alias] {
				return fmt.Errorf("tool '%s' has arg alias '%s' that conflicts with existing schema property '%s'",
					toolName, alias, alias)
			}
			if existingCanonical, conflict := reverseMap[alias]; conflict {
				return fmt.Errorf("tool '%s' has conflicting arg alias '%s' mapped to both '%s' and '%s'",
					toolName, alias, existingCanonical, canonical)
			}
			reverseMap[alias] = canonical
		}
	}
	t.argsAliasMap[toolName] = reverseMap

	return nil
}

func convTools(ctx context.Context, params convToolsParams) (*toolsTuple, error) {
	ret := &toolsTuple{
		indexes:                     make(map[string]int),
		meta:                        make([]*executorMeta, len(params.tools)),
		endpoints:                   make([]InvokableToolEndpoint, len(params.tools)),
		streamEndpoints:             make([]StreamableToolEndpoint, len(params.tools)),
		enhancedInvokableEndpoints:  make([]EnhancedInvokableToolEndpoint, len(params.tools)),
		enhancedStreamableEndpoints: make([]EnhancedStreamableToolEndpoint, len(params.tools)),
		canonicalNames:              make([]string, len(params.tools)),
		toolInfos:                   make([]*schema.ToolInfo, len(params.tools)),
	}
	for idx, bt := range params.tools {
		tl, err := bt.Info(ctx)
		if err != nil {
			return nil, fmt.Errorf("(NewToolNode) failed to get tool info at idx= %d: %w", idx, err)
		}

		toolName := tl.Name
		var (
			st     tool.StreamableTool
			it     tool.InvokableTool
			eiTool tool.EnhancedInvokableTool
			esTool tool.EnhancedStreamableTool

			invokable          InvokableToolEndpoint
			streamable         StreamableToolEndpoint
			enhancedInvokable  EnhancedInvokableToolEndpoint
			enhancedStreamable EnhancedStreamableToolEndpoint

			ok   bool
			meta *executorMeta
		)

		meta = parseExecutorInfoFromComponent(components.ComponentOfTool, bt)

		if st, ok = bt.(tool.StreamableTool); ok {
			streamable = wrapStreamToolCall(st, params.middlewares.streamable, !meta.isComponentCallbackEnabled)
		}

		if it, ok = bt.(tool.InvokableTool); ok {
			invokable = wrapToolCall(it, params.middlewares.invokable, !meta.isComponentCallbackEnabled)
		}

		if eiTool, ok = bt.(tool.EnhancedInvokableTool); ok {
			enhancedInvokable = wrapEnhancedInvokableToolCall(eiTool, params.middlewares.enhancedInvokable, !meta.isComponentCallbackEnabled)
		}

		if esTool, ok = bt.(tool.EnhancedStreamableTool); ok {
			enhancedStreamable = wrapEnhancedStreamableToolCall(esTool, params.middlewares.enhancedStreamable, !meta.isComponentCallbackEnabled)
		}

		if st == nil && it == nil && eiTool == nil && esTool == nil {
			return nil, fmt.Errorf("tool %s is not invokable, streamable, enhanced invokable or enhanced streamable", toolName)
		}
		if streamable == nil && invokable != nil {
			streamable = invokableToStreamable(invokable)
		}
		if invokable == nil && streamable != nil {
			invokable = streamableToInvokable(streamable)
		}

		if enhancedStreamable == nil && enhancedInvokable != nil {
			enhancedStreamable = enhancedInvokableToEnhancedStreamable(enhancedInvokable)
		}
		if enhancedInvokable == nil && enhancedStreamable != nil {
			enhancedInvokable = enhancedStreamableToEnhancedInvokable(enhancedStreamable)
		}

		ret.indexes[toolName] = idx
		ret.meta[idx] = meta
		ret.endpoints[idx] = invokable
		ret.streamEndpoints[idx] = streamable
		ret.enhancedInvokableEndpoints[idx] = enhancedInvokable
		ret.enhancedStreamableEndpoints[idx] = enhancedStreamable
		ret.canonicalNames[idx] = toolName
		ret.toolInfos[idx] = tl
	}

	if len(params.aliasConfigs) > 0 {
		if err := ret.applyAliasConfigs(params.aliasConfigs); err != nil {
			return nil, err
		}
	}

	return ret, nil
}

func wrapToolCall(it tool.InvokableTool, middlewares []InvokableToolMiddleware, needCallback bool) InvokableToolEndpoint {
	middleware := func(next InvokableToolEndpoint) InvokableToolEndpoint {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
	if needCallback {
		it = &invokableToolWithCallback{it: it}
	}
	return middleware(func(ctx context.Context, input *ToolInput) (*ToolOutput, error) {
		result, err := it.InvokableRun(ctx, input.Arguments, input.CallOptions...)
		if err != nil {
			return nil, err
		}
		return &ToolOutput{Result: result}, nil
	})
}

func wrapStreamToolCall(st tool.StreamableTool, middlewares []StreamableToolMiddleware, needCallback bool) StreamableToolEndpoint {
	middleware := func(next StreamableToolEndpoint) StreamableToolEndpoint {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
	if needCallback {
		st = &streamableToolWithCallback{st: st}
	}
	return middleware(func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) {
		result, err := st.StreamableRun(ctx, input.Arguments, input.CallOptions...)
		if err != nil {
			return nil, err
		}
		return &StreamToolOutput{Result: result}, nil
	})
}

func wrapEnhancedInvokableToolCall(eiTool tool.EnhancedInvokableTool, middlewares []EnhancedInvokableToolMiddleware, needCallback bool) EnhancedInvokableToolEndpoint {
	middleware := func(next EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
	if needCallback {
		eiTool = &enhancedInvokableToolWithCallback{eiTool: eiTool}
	}
	return middleware(func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) {
		result, err := eiTool.InvokableRun(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...)
		if err != nil {
			return nil, err
		}
		return &EnhancedInvokableToolOutput{Result: result}, nil
	})
}

func wrapEnhancedStreamableToolCall(est tool.EnhancedStreamableTool, middlewares []EnhancedStreamableToolMiddleware, needCallback bool) EnhancedStreamableToolEndpoint {
	middleware := func(next EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
	if needCallback {
		est = &enhancedStreamableToolWithCallback{est: est}
	}
	return middleware(func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) {
		result, err := est.StreamableRun(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...)
		if err != nil {
			return nil, err
		}
		return &EnhancedStreamableToolOutput{Result: result}, nil
	})
}

type invokableToolWithCallback struct {
	it tool.InvokableTool
}

func (i *invokableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return i.it.Info(ctx)
}

func (i *invokableToolWithCallback) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	return invokeWithCallbacks(i.it.InvokableRun)(ctx, argumentsInJSON, opts...)
}

type streamableToolWithCallback struct {
	st tool.StreamableTool
}

func (s *streamableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return s.st.Info(ctx)
}

func (s *streamableToolWithCallback) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
	return streamWithCallbacks(s.st.StreamableRun)(ctx, argumentsInJSON, opts...)
}

type enhancedInvokableToolWithCallback struct {
	eiTool tool.EnhancedInvokableTool
}

func (e *enhancedInvokableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return e.eiTool.Info(ctx)
}

func (e *enhancedInvokableToolWithCallback) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
	return invokeEnhancedWithCallbacks(e.eiTool.InvokableRun)(ctx, toolArgument, opts...)
}

type enhancedStreamableToolWithCallback struct {
	est tool.EnhancedStreamableTool
}

func (e *enhancedStreamableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return e.est.Info(ctx)
}

func (e *enhancedStreamableToolWithCallback) StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
	return streamEnhancedWithCallbacks(e.est.StreamableRun)(ctx, toolArgument, opts...)
}

func streamableToInvokable(e StreamableToolEndpoint) InvokableToolEndpoint {
	return func(ctx context.Context, input *ToolInput) (*ToolOutput, error) {
		so, err := e(ctx, input)
		if err != nil {
			return nil, err
		}
		o, err := concatStreamReader(so.Result)
		if err != nil {
			return nil, fmt.Errorf("failed to concat StreamableTool output message stream: %w", err)
		}
		return &ToolOutput{Result: o}, nil
	}
}

func invokableToStreamable(e InvokableToolEndpoint) StreamableToolEndpoint {
	return func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) {
		o, err := e(ctx, input)
		if err != nil {
			return nil, err
		}
		return &StreamToolOutput{Result: schema.StreamReaderFromArray([]string{o.Result})}, nil
	}
}

func enhancedStreamableToEnhancedInvokable(e EnhancedStreamableToolEndpoint) EnhancedInvokableToolEndpoint {
	return func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) {
		so, err := e(ctx, input)
		if err != nil {
			return nil, err
		}
		o, err := concatStreamReader(so.Result)
		if err != nil {
			return nil, fmt.Errorf("failed to concat EnhancedStreamableTool output message stream: %w", err)
		}
		return &EnhancedInvokableToolOutput{Result: o}, nil
	}
}

func enhancedInvokableToEnhancedStreamable(e EnhancedInvokableToolEndpoint) EnhancedStreamableToolEndpoint {
	return func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) {
		o, err := e(ctx, input)
		if err != nil {
			return nil, err
		}
		return &EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{o.Result})}, nil
	}
}

func invokeEnhancedWithCallbacks(i func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error)) func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
	return runWithCallbacks(i, onStart[*schema.ToolArgument], onEnd[*schema.ToolResult], onError)
}

func streamEnhancedWithCallbacks(s func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error)) func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
	return runWithCallbacks(s, onStart[*schema.ToolArgument], onEndWithStreamOutput[*schema.ToolResult], onError)
}

type toolCallTask struct {
	// in
	endpoint                   InvokableToolEndpoint
	streamEndpoint             StreamableToolEndpoint
	enhancedInvokableEndpoint  EnhancedInvokableToolEndpoint
	enhancedStreamableEndpoint EnhancedStreamableToolEndpoint
	meta                       *executorMeta
	name                       string
	arg                        string
	callID                     string
	useEnhanced                bool

	// out
	executed        bool
	output          string
	sOutput         *schema.StreamReader[string]
	enhancedOutput  *schema.ToolResult
	enhancedSOutput *schema.StreamReader[*schema.ToolResult]
	err             error
}

func (tn *ToolsNode) genToolCallTasks(ctx context.Context, tuple *toolsTuple,
	input *schema.Message, executedTools map[string]string, executedEnhancedTools map[string]*schema.ToolResult, isStream bool) ([]toolCallTask, error) {

	if input.Role != schema.Assistant {
		return nil, fmt.Errorf("expected message role is Assistant, got %s", input.Role)
	}

	n := len(input.ToolCalls)
	if n == 0 {
		return nil, errors.New("no tool call found in input message")
	}

	toolCallTasks := make([]toolCallTask, n)

	for i := 0; i < n; i++ {
		toolCall := input.ToolCalls[i]
		if enhancedResult, executed := executedEnhancedTools[toolCall.ID]; executed {
			toolCallTasks[i].name = toolCall.Function.Name
			toolCallTasks[i].arg = toolCall.Function.Arguments
			toolCallTasks[i].callID = toolCall.ID
			toolCallTasks[i].executed = true
			toolCallTasks[i].useEnhanced = true
			if isStream {
				toolCallTasks[i].enhancedSOutput = schema.StreamReaderFromArray([]*schema.ToolResult{enhancedResult})
			} else {
				toolCallTasks[i].enhancedOutput = enhancedResult
			}
			continue
		}
		if result, executed := executedTools[toolCall.ID]; executed {
			toolCallTasks[i].name = toolCall.Function.Name
			toolCallTasks[i].arg = toolCall.Function.Arguments
			toolCallTasks[i].callID = toolCall.ID
			toolCallTasks[i].executed = true
			toolCallTasks[i].useEnhanced = false
			if isStream {
				toolCallTasks[i].sOutput = schema.StreamReaderFromArray([]string{result})
			} else {
				toolCallTasks[i].output = result
			}
			continue
		}
		index, ok := tuple.indexes[toolCall.Function.Name]
		if !ok {
			if tn.unknownToolHandler == nil {
				return nil, fmt.Errorf("tool %s not found in toolsNode indexes", toolCall.Function.Name)
			}
			toolCallTasks[i] = newUnknownToolTask(toolCall.Function.Name, toolCall.Function.Arguments, toolCall.ID, tn.unknownToolHandler)
		} else {
			toolCallTasks[i].meta = tuple.meta[index]
			toolCallTasks[i].name = toolCall.Function.Name
			toolCallTasks[i].callID = toolCall.ID

			if tuple.enhancedInvokableEndpoints[index] != nil && tuple.enhancedStreamableEndpoints[index] != nil {
				toolCallTasks[i].enhancedInvokableEndpoint = tuple.enhancedInvokableEndpoints[index]
				toolCallTasks[i].enhancedStreamableEndpoint = tuple.enhancedStreamableEndpoints[index]
				toolCallTasks[i].useEnhanced = true
			} else {
				toolCallTasks[i].endpoint = tuple.endpoints[index]
				toolCallTasks[i].streamEndpoint = tuple.streamEndpoints[index]
				toolCallTasks[i].useEnhanced = false
			}

			// Get canonical tool name for looking up argument aliases
			canonicalToolName := tuple.canonicalNames[index]

			// Process argument aliases remapping
			args := toolCall.Function.Arguments
			if aliasMap, hasAliases := tuple.argsAliasMap[canonicalToolName]; hasAliases {
				remappedArgs, err := remapArgs(args, aliasMap)
				if err != nil {
					return nil, fmt.Errorf("failed to remap args for tool[name:%s]: %w", canonicalToolName, err)
				}
				args = remappedArgs
			}

			if tn.toolArgumentsHandler != nil {
				arg, err := tn.toolArgumentsHandler(ctx, canonicalToolName, args)
				if err != nil {
					return nil, fmt.Errorf("failed to executed tool[name:%s arguments:%s] arguments handler: %w", toolCall.Function.Name, args, err)
				}
				toolCallTasks[i].arg = arg
			} else {
				toolCallTasks[i].arg = args
			}
		}
	}

	return toolCallTasks, nil
}

func newUnknownToolTask(name, arg, callID string, unknownToolHandler func(ctx context.Context, name, input string) (string, error)) toolCallTask {
	endpoint := func(ctx context.Context, input *ToolInput) (*ToolOutput, error) {
		result, err := unknownToolHandler(ctx, input.Name, input.Arguments)
		if err != nil {
			return nil, err
		}
		return &ToolOutput{
			Result: result,
		}, nil
	}
	return toolCallTask{
		endpoint:       endpoint,
		streamEndpoint: invokableToStreamable(endpoint),
		meta: &executorMeta{
			component:                  components.ComponentOfTool,
			isComponentCallbackEnabled: false,
			componentImplType:          "UnknownTool",
		},
		name:   name,
		arg:    arg,
		callID: callID,
	}
}

func runToolCallTaskByInvoke(ctx context.Context, task *toolCallTask, opts ...tool.Option) {
	if task.executed {
		return
	}
	ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{
		Name:      task.name,
		Type:      task.meta.componentImplType,
		Component: task.meta.component,
	})

	ctx = setToolCallInfo(ctx, &toolCallInfo{toolCallID: task.callID})
	ctx = appendToolAddressSegment(ctx, task.name, task.callID)

	if task.useEnhanced {
		enhancedOutput, err := task.enhancedInvokableEndpoint(ctx, &ToolInput{
			Name:        task.name,
			Arguments:   task.arg,
			CallID:      task.callID,
			CallOptions: opts,
		})
		if err != nil {
			task.err = err
		} else {
			task.enhancedOutput = enhancedOutput.Result
			task.executed = true
		}
	} else {
		output, err := task.endpoint(ctx, &ToolInput{
			Name:        task.name,
			Arguments:   task.arg,
			CallID:      task.callID,
			CallOptions: opts,
		})
		if err != nil {
			task.err = err
		} else {
			task.output = output.Result
			task.executed = true
		}
	}
}

func runToolCallTaskByStream(ctx context.Context, task *toolCallTask, opts ...tool.Option) {
	ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{
		Name:      task.name,
		Type:      task.meta.componentImplType,
		Component: task.meta.component,
	})

	ctx = setToolCallInfo(ctx, &toolCallInfo{toolCallID: task.callID})
	ctx = appendToolAddressSegment(ctx, task.name, task.callID)

	if task.useEnhanced {
		enhancedOutput, err := task.enhancedStreamableEndpoint(ctx, &ToolInput{
			Name:        task.name,
			Arguments:   task.arg,
			CallID:      task.callID,
			CallOptions: opts,
		})
		if err != nil {
			task.err = err
		} else {
			task.enhancedSOutput = enhancedOutput.Result
			task.executed = true
		}
	} else {
		output, err := task.streamEndpoint(ctx, &ToolInput{
			Name:        task.name,
			Arguments:   task.arg,
			CallID:      task.callID,
			CallOptions: opts,
		})
		if err != nil {
			task.err = err
		} else {
			task.sOutput = output.Result
			task.executed = true
		}
	}
}

func sequentialRunToolCall(ctx context.Context,
	run func(ctx2 context.Context, callTask *toolCallTask, opts ...tool.Option),
	tasks []toolCallTask, opts ...tool.Option) {

	for i := range tasks {
		if tasks[i].executed {
			continue
		}
		run(ctx, &tasks[i], opts...)
	}
}

func parallelRunToolCall(ctx context.Context,
	run func(ctx2 context.Context, callTask *toolCallTask, opts ...tool.Option),
	tasks []toolCallTask, opts ...tool.Option) {

	if len(tasks) == 1 {
		run(ctx, &tasks[0], opts...)
		return
	}

	var wg sync.WaitGroup
	for i := 1; i < len(tasks); i++ {
		if tasks[i].executed {
			continue
		}
		wg.Add(1)
		go func(ctx_ context.Context, t *toolCallTask, opts ...tool.Option) {
			defer wg.Done()
			defer func() {
				panicErr := recover()
				if panicErr != nil {
					t.err = safe.NewPanicErr(panicErr, debug.Stack())
				}
			}()
			run(ctx_, t, opts...)
		}(ctx, &tasks[i], opts...)
	}

	if !tasks[0].executed {
		run(ctx, &tasks[0], opts...)
	}

	wg.Wait()
}

// buildTupleFromOpts rebuilds a toolsTuple when call options override tools or aliases.
func (tn *ToolsNode) buildTupleFromOpts(ctx context.Context, opt *toolsNodeOptions) (*toolsTuple, error) {
	tools := opt.ToolList
	if tools == nil {
		tools = tn.tools
	}
	aliasConfigs := opt.ToolAliases
	if aliasConfigs == nil {
		aliasConfigs = tn.toolAliasConfigs
	}
	p := convToolsParams{
		tools:        tools,
		aliasConfigs: aliasConfigs,
	}
	p.middlewares.invokable = tn.toolCallMiddlewares
	p.middlewares.streamable = tn.streamToolCallMiddlewares
	p.middlewares.enhancedInvokable = tn.enhancedToolCallMiddlewares
	p.middlewares.enhancedStreamable = tn.enhancedStreamToolCallMiddlewares
	tuple, err := convTools(ctx, p)
	if err != nil {
		return nil, fmt.Errorf("failed to convert tool list from call option: %w", err)
	}
	return tuple, nil
}

// Invoke calls the tools and collects the results of invokable tools.
// it's parallel if there are multiple tool calls in the input message.
func (tn *ToolsNode) Invoke(ctx context.Context, input *schema.Message,
	opts ...ToolsNodeOption) ([]*schema.Message, error) {

	opt := getToolsNodeOptions(opts...)
	tuple := tn.tuple
	if opt.ToolList != nil || opt.ToolAliases != nil {
		var err error
		tuple, err = tn.buildTupleFromOpts(ctx, opt)
		if err != nil {
			return nil, err
		}
	}

	var executedTools map[string]string
	var executedEnhancedTools map[string]*schema.ToolResult
	if wasInterrupted, hasState, tnState := GetInterruptState[*toolsInterruptAndRerunState](ctx); wasInterrupted && hasState {
		input = tnState.Input
		if tnState.ExecutedTools != nil {
			executedTools = tnState.ExecutedTools
		}
		if tnState.ExecutedEnhancedTools != nil {
			executedEnhancedTools = tnState.ExecutedEnhancedTools
		}
	}

	tasks, err := tn.genToolCallTasks(ctx, tuple, input, executedTools, executedEnhancedTools, false)
	if err != nil {
		return nil, err
	}

	if tn.executeSequentially {
		sequentialRunToolCall(ctx, runToolCallTaskByInvoke, tasks, opt.ToolOptions...)
	} else {
		parallelRunToolCall(ctx, runToolCallTaskByInvoke, tasks, opt.ToolOptions...)
	}

	n := len(tasks)
	output := make([]*schema.Message, n)

	rerunExtra := &ToolsInterruptAndRerunExtra{
		ToolCalls:             input.ToolCalls,
		ExecutedTools:         make(map[string]string),
		ExecutedEnhancedTools: make(map[string]*schema.ToolResult),
		RerunExtraMap:         make(map[string]any),
	}
	rerunState := &toolsInterruptAndRerunState{
		Input:                 input,
		ExecutedTools:         make(map[string]string),
		ExecutedEnhancedTools: make(map[string]*schema.ToolResult),
	}

	var errs []error
	for i := 0; i < n; i++ {
		if tasks[i].err != nil {
			info, ok := IsInterruptRerunError(tasks[i].err)
			if !ok {
				return nil, fmt.Errorf("failed to invoke tool[name:%s id:%s]: %w", tasks[i].name, tasks[i].callID, tasks[i].err)
			}

			rerunExtra.RerunTools = append(rerunExtra.RerunTools, tasks[i].callID)
			rerunState.RerunTools = append(rerunState.RerunTools, tasks[i].callID)
			if info != nil {
				rerunExtra.RerunExtraMap[tasks[i].callID] = info
			}

			iErr := WrapInterruptAndRerunIfNeeded(ctx,
				AddressSegment{ID: tasks[i].callID, Type: AddressSegmentTool}, tasks[i].err)
			errs = append(errs, iErr)
			continue
		}
		if tasks[i].executed {
			if tasks[i].useEnhanced {
				rerunExtra.ExecutedEnhancedTools[tasks[i].callID] = tasks[i].enhancedOutput
				rerunState.ExecutedEnhancedTools[tasks[i].callID] = tasks[i].enhancedOutput
			} else {
				rerunExtra.ExecutedTools[tasks[i].callID] = tasks[i].output
				rerunState.ExecutedTools[tasks[i].callID] = tasks[i].output
			}
		}

		if len(errs) == 0 {
			if tasks[i].useEnhanced {
				output[i] = schema.ToolMessage("", tasks[i].callID, schema.WithToolName(tasks[i].name))
				output[i].UserInputMultiContent, err = tasks[i].enhancedOutput.ToMessageInputParts()
				if err != nil {
					return nil, err
				}
			} else {
				output[i] = schema.ToolMessage(tasks[i].output, tasks[i].callID, schema.WithToolName(tasks[i].name))
			}

		}
	}
	if len(errs) > 0 {
		return nil, CompositeInterrupt(ctx, rerunExtra, rerunState, errs...)
	}

	return output, nil
}

// Stream calls the tools and collects the results of stream readers.
// it's parallel if there are multiple tool calls in the input message.
func (tn *ToolsNode) Stream(ctx context.Context, input *schema.Message,
	opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error) {

	opt := getToolsNodeOptions(opts...)
	tuple := tn.tuple
	if opt.ToolList != nil || opt.ToolAliases != nil {
		var err error
		tuple, err = tn.buildTupleFromOpts(ctx, opt)
		if err != nil {
			return nil, err
		}
	}

	var executedTools map[string]string
	var executedEnhancedTools map[string]*schema.ToolResult
	if wasInterrupted, hasState, tnState := GetInterruptState[*toolsInterruptAndRerunState](ctx); wasInterrupted && hasState {
		input = tnState.Input
		if tnState.ExecutedTools != nil {
			executedTools = tnState.ExecutedTools
		}
		if tnState.ExecutedEnhancedTools != nil {
			executedEnhancedTools = tnState.ExecutedEnhancedTools
		}
	}

	tasks, err := tn.genToolCallTasks(ctx, tuple, input, executedTools, executedEnhancedTools, true)
	if err != nil {
		return nil, err
	}

	if tn.executeSequentially {
		sequentialRunToolCall(ctx, runToolCallTaskByStream, tasks, opt.ToolOptions...)
	} else {
		parallelRunToolCall(ctx, runToolCallTaskByStream, tasks, opt.ToolOptions...)
	}

	n := len(tasks)

	rerunExtra := &ToolsInterruptAndRerunExtra{
		ToolCalls:             input.ToolCalls,
		ExecutedTools:         make(map[string]string),
		ExecutedEnhancedTools: make(map[string]*schema.ToolResult),
		RerunExtraMap:         make(map[string]any),
	}
	rerunState := &toolsInterruptAndRerunState{
		Input:                 input,
		ExecutedTools:         make(map[string]string),
		ExecutedEnhancedTools: make(map[string]*schema.ToolResult),
	}
	var errs []error
	// check rerun
	for i := 0; i < n; i++ {
		if tasks[i].err != nil {
			info, ok := IsInterruptRerunError(tasks[i].err)
			if !ok {
				return nil, fmt.Errorf("failed to stream tool call %s: %w", tasks[i].callID, tasks[i].err)
			}

			rerunExtra.RerunTools = append(rerunExtra.RerunTools, tasks[i].callID)
			rerunState.RerunTools = append(rerunState.RerunTools, tasks[i].callID)
			if info != nil {
				rerunExtra.RerunExtraMap[tasks[i].callID] = info
			}
			iErr := WrapInterruptAndRerunIfNeeded(ctx,
				AddressSegment{ID: tasks[i].callID, Type: AddressSegmentTool}, tasks[i].err)
			errs = append(errs, iErr)
			continue
		}
	}

	if len(errs) > 0 {
		// concat and save tool output
		for _, t := range tasks {
			if t.executed {
				if t.useEnhanced {
					eo, err_ := concatStreamReader(t.enhancedSOutput)
					if err_ != nil {
						return nil, fmt.Errorf("failed to concat enhanced tool[name:%s id:%s]'s stream output: %w", t.name, t.callID, err_)
					}
					rerunExtra.ExecutedEnhancedTools[t.callID] = eo
					rerunState.ExecutedEnhancedTools[t.callID] = eo

				} else {
					o, err_ := concatStreamReader(t.sOutput)
					if err_ != nil {
						return nil, fmt.Errorf("failed to concat tool[name:%s id:%s]'s stream output: %w", t.name, t.callID, err_)
					}
					rerunExtra.ExecutedTools[t.callID] = o
					rerunState.ExecutedTools[t.callID] = o
				}
			}
		}
		return nil, CompositeInterrupt(ctx, rerunExtra, rerunState, errs...)
	}

	// common return
	sOutput := make([]*schema.StreamReader[[]*schema.Message], n)
	for i := 0; i < n; i++ {
		index := i
		callID := tasks[i].callID
		callName := tasks[i].name
		if tasks[i].useEnhanced {
			cvt := func(tr *schema.ToolResult) ([]*schema.Message, error) {
				ret := make([]*schema.Message, n)
				ret[index] = schema.ToolMessage("", callID, schema.WithToolName(callName))
				ret[index].UserInputMultiContent, err = tr.ToMessageInputParts()
				if err != nil {
					return nil, err
				}
				return ret, nil
			}
			sOutput[i] = schema.StreamReaderWithConvert(tasks[i].enhancedSOutput, cvt)
		} else {
			cvt := func(s string) ([]*schema.Message, error) {
				ret := make([]*schema.Message, n)
				ret[index] = schema.ToolMessage(s, callID, schema.WithToolName(callName))
				return ret, nil
			}
			sOutput[i] = schema.StreamReaderWithConvert(tasks[i].sOutput, cvt)
		}

	}
	return schema.MergeStreamReaders(sOutput), nil
}

// GetType returns the component type string for the Tools node.
func (tn *ToolsNode) GetType() string {
	return ""
}

func getToolsNodeOptions(opts ...ToolsNodeOption) *toolsNodeOptions {
	o := &toolsNodeOptions{
		ToolOptions: make([]tool.Option, 0),
	}
	for _, opt := range opts {
		opt(o)
	}
	return o
}

type toolCallInfoKey struct{}
type toolCallInfo struct {
	toolCallID string
}

func setToolCallInfo(ctx context.Context, toolCallInfo *toolCallInfo) context.Context {
	return context.WithValue(ctx, toolCallInfoKey{}, toolCallInfo)
}

// GetToolCallID gets the current tool call id from the context.
func GetToolCallID(ctx context.Context) string {
	v := ctx.Value(toolCallInfoKey{})
	if v == nil {
		return ""
	}

	info, ok := v.(*toolCallInfo)
	if !ok {
		return ""
	}

	return info.toolCallID
}
