/*
 * Copyright 2025 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 planexecute implements a plan–execute–replan style agent.
package planexecute

import (
	"context"
	"encoding/json"
	"fmt"
	"runtime/debug"
	"strings"

	"github.com/bytedance/sonic"

	"github.com/cloudwego/eino/adk"
	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/components/prompt"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/internal/safe"
	"github.com/cloudwego/eino/schema"
)

func init() {
	schema.RegisterName[*defaultPlan]("_eino_adk_plan_execute_default_plan")
	schema.RegisterName[ExecutedStep]("_eino_adk_plan_execute_executed_step")
	schema.RegisterName[[]ExecutedStep]("_eino_adk_plan_execute_executed_steps")
}

// Plan represents an execution plan with a sequence of actionable steps.
// It supports JSON serialization and deserialization while providing access to the first step.
type Plan interface {
	// FirstStep returns the first step to be executed in the plan.
	FirstStep() string

	// Marshaler serializes the Plan into JSON.
	// The resulting JSON can be used in prompt templates.
	json.Marshaler
	// Unmarshaler deserializes JSON content into the Plan.
	// This processes output from structured chat models or tool calls into the Plan structure.
	json.Unmarshaler
}

// NewPlan is a function type that creates a new Plan instance.
type NewPlan func(ctx context.Context) Plan

// defaultPlan is the default implementation of the Plan interface.
//
// JSON Schema:
//
//	{
//	  "type": "object",
//	  "properties": {
//	    "steps": {
//	      "type": "array",
//	      "items": {
//	        "type": "string"
//	      },
//	      "description": "Ordered list of actions to be taken. Each step should be clear, actionable, and arranged in a logical sequence."
//	    }
//	  },
//	  "required": ["steps"]
//	}
type defaultPlan struct {
	// Steps contains the ordered list of actions to be taken.
	// Each step should be clear, actionable, and arranged in a logical sequence.
	Steps []string `json:"steps"`
}

// FirstStep returns the first step in the plan or an empty string if no steps exist.
func (p *defaultPlan) FirstStep() string {
	if len(p.Steps) == 0 {
		return ""
	}
	return p.Steps[0]
}

func (p *defaultPlan) MarshalJSON() ([]byte, error) {
	type planTyp defaultPlan
	return sonic.Marshal((*planTyp)(p))
}

func (p *defaultPlan) UnmarshalJSON(bytes []byte) error {
	type planTyp defaultPlan
	return sonic.Unmarshal(bytes, (*planTyp)(p))
}

// Response represents the final response to the user.
// This struct is used for JSON serialization/deserialization of the final response
// generated by the model.
type Response struct {
	// Response is the complete response to provide to the user.
	// This field is required.
	Response string `json:"response"`
}

var (
	// PlanToolInfo defines the schema for the Plan tool that can be used with ToolCallingChatModel.
	// This schema instructs the model to generate a structured plan with ordered steps.
	PlanToolInfo = schema.ToolInfo{
		Name: "plan",
		Desc: "Plan with a list of steps to execute in order. Each step should be clear, actionable, and arranged in a logical sequence. The output will be used to guide the execution process.",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"steps": {
					Type:     schema.Array,
					ElemInfo: &schema.ParameterInfo{Type: schema.String},
					Desc:     "different steps to follow, should be in sorted order",
					Required: true,
				},
			},
		),
	}

	// RespondToolInfo defines the schema for the response tool that can be used with ToolCallingChatModel.
	// This schema instructs the model to generate a direct response to the user.
	RespondToolInfo = schema.ToolInfo{
		Name: "respond",
		Desc: "Generate a direct response to the user. Use this tool when you have all the information needed to provide a final answer.",
		ParamsOneOf: schema.NewParamsOneOfByParams(
			map[string]*schema.ParameterInfo{
				"response": {
					Type:     schema.String,
					Desc:     "The complete response to provide to the user",
					Required: true,
				},
			},
		),
	}

	// PlannerPrompt is the prompt template for the planner.
	// It provides context and guidance to the planner on how to generate the Plan.
	PlannerPrompt = prompt.FromMessages(schema.FString,
		schema.SystemMessage(`You are an expert planning agent. Given an objective, create a comprehensive step-by-step plan to achieve the objective.

## YOUR TASK
Analyze the objective and generate a strategic plan that breaks down the goal into manageable, executable steps.

## PLANNING REQUIREMENTS
Each step in your plan must be:
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
- **Self-contained**: Include all necessary context, parameters, and requirements
- **Independently executable**: Can be performed by an agent without dependencies on other steps
- **Logically sequenced**: Arranged in optimal order for efficient execution
- **Objective-focused**: Directly contribute to achieving the main goal

## PLANNING GUIDELINES
- Eliminate redundant or unnecessary steps
- Include relevant constraints, parameters, and success criteria for each step
- Ensure the final step produces a complete answer or deliverable
- Anticipate potential challenges and include mitigation strategies
- Structure steps to build upon each other logically
- Provide sufficient detail for successful execution

## QUALITY CRITERIA
- Plan completeness: Does it address all aspects of the objective?
- Step clarity: Can each step be understood and executed independently?
- Logical flow: Do steps follow a sensible progression?
- Efficiency: Is this the most direct path to the objective?
- Adaptability: Can the plan handle unexpected results or changes?`),
		schema.MessagesPlaceholder("input", false),
	)

	// ExecutorPrompt is the prompt template for the executor.
	// It provides context and guidance to the executor on how to execute the Task.
	ExecutorPrompt = prompt.FromMessages(schema.FString,
		schema.SystemMessage(`You are a diligent and meticulous executor agent. Follow the given plan and execute your tasks carefully and thoroughly.`),
		schema.UserMessage(`## OBJECTIVE
{input}
## Given the following plan:
{plan}
## COMPLETED STEPS & RESULTS
{executed_steps}
## Your task is to execute the first step, which is: 
{step}`))

	// ReplannerPrompt is the prompt template for the replanner.
	// It provides context and guidance to the replanner on how to regenerate the Plan.
	ReplannerPrompt = prompt.FromMessages(schema.FString,
		schema.SystemMessage(
			`You are going to review the progress toward an objective. Analyze the current state and determine the optimal next action.

## YOUR TASK
Based on the progress above, you MUST choose exactly ONE action:

### Option 1: COMPLETE (if objective is fully achieved)
Call '{respond_tool}' with:
- A comprehensive final answer
- Clear conclusion summarizing how the objective was met
- Key insights from the execution process

### Option 2: CONTINUE (if more work is needed)
Call '{plan_tool}' with a revised plan that:
- Contains ONLY remaining steps (exclude completed ones)
- Incorporates lessons learned from executed steps
- Addresses any gaps or issues discovered
- Maintains logical step sequence

## PLANNING REQUIREMENTS
Each step in your plan must be:
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
- **Self-contained**: Include all necessary context, parameters, and requirements
- **Independently executable**: Can be performed by an agent without dependencies on other steps
- **Logically sequenced**: Arranged in optimal order for efficient execution
- **Objective-focused**: Directly contribute to achieving the main goal

## PLANNING GUIDELINES
- Eliminate redundant or unnecessary steps
- Adapt strategy based on new information
- Include relevant constraints, parameters, and success criteria for each step

## DECISION CRITERIA
- Has the original objective been completely satisfied?
- Are there any remaining requirements or sub-goals?
- Do the results suggest a need for strategy adjustment?
- What specific actions are still required?`),
		schema.UserMessage(`## OBJECTIVE
{input}

## ORIGINAL PLAN
{plan}

## COMPLETED STEPS & RESULTS
{executed_steps}`),
	)
)

const (
	// UserInputSessionKey is the session key for the user input.
	UserInputSessionKey = "UserInput"

	// PlanSessionKey is the session key for the plan.
	PlanSessionKey = "Plan"

	// ExecutedStepSessionKey is the session key for the execute result.
	ExecutedStepSessionKey = "ExecutedStep"

	// ExecutedStepsSessionKey is the session key for the execute results.
	ExecutedStepsSessionKey = "ExecutedSteps"
)

// PlannerConfig provides configuration options for creating a planner agent.
// There are two ways to configure the planner to generate structured Plan output:
//  1. Use ChatModelWithFormattedOutput: A model pre-configured to output in the Plan format
//  2. Use ToolCallingChatModel + ToolInfo: A model that uses tool calling to generate
//     the Plan structure
type PlannerConfig struct {
	// ChatModelWithFormattedOutput is a model pre-configured to output in the Plan format.
	// Create this by configuring a model to output structured data directly.
	// See example: https://github.com/cloudwego/eino-ext/blob/main/components/model/openai/examples/structured/structured.go
	ChatModelWithFormattedOutput model.BaseChatModel

	// ToolCallingChatModel is a model that supports tool calling capabilities.
	// When provided with ToolInfo, it will use tool calling to generate the Plan structure.
	ToolCallingChatModel model.ToolCallingChatModel

	// ToolInfo defines the schema for the Plan structure when using tool calling.
	// Optional. If not provided, PlanToolInfo will be used as the default.
	ToolInfo *schema.ToolInfo

	// GenInputFn is a function that generates the input messages for the planner.
	// Optional. If not provided, defaultGenPlannerInputFn will be used.
	GenInputFn GenPlannerModelInputFn

	// NewPlan creates a new Plan instance for JSON.
	// The returned Plan will be used to unmarshal the model-generated JSON output.
	// Optional. If not provided, defaultNewPlan will be used.
	NewPlan NewPlan
}

// GenPlannerModelInputFn is a function type that generates input messages for the planner.
type GenPlannerModelInputFn func(ctx context.Context, userInput []adk.Message) ([]adk.Message, error)

func defaultNewPlan(ctx context.Context) Plan {
	return &defaultPlan{}
}

func defaultGenPlannerInputFn(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) {
	msgs, err := PlannerPrompt.Format(ctx, map[string]any{
		"input": userInput,
	})
	if err != nil {
		return nil, err
	}
	return msgs, nil
}

type planner struct {
	toolCall   bool
	chatModel  model.BaseChatModel
	genInputFn GenPlannerModelInputFn
	newPlan    NewPlan
}

func (p *planner) Name(_ context.Context) string {
	return "planner"
}

func (p *planner) Description(_ context.Context) string {
	return "a planner agent"
}

func argToContent(msg adk.Message) (adk.Message, error) {
	if len(msg.ToolCalls) == 0 {
		return nil, schema.ErrNoValue
	}

	return schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil), nil
}

func (p *planner) Run(ctx context.Context, input *adk.AgentInput,
	_ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {

	iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()

	adk.AddSessionValue(ctx, UserInputSessionKey, input.Messages)

	go func() {
		defer func() {
			panicErr := recover()
			if panicErr != nil {
				e := safe.NewPanicErr(panicErr, debug.Stack())
				generator.Send(&adk.AgentEvent{Err: e})
			}

			generator.Close()
		}()

		c := compose.NewChain[*adk.AgentInput, Plan]().
			AppendLambda(
				compose.InvokableLambda(func(ctx context.Context, input *adk.AgentInput) (output []adk.Message, err error) {
					return p.genInputFn(ctx, input.Messages)
				}),
			).
			AppendChatModel(p.chatModel).
			AppendLambda(
				compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
					if input.EnableStreaming {
						ss := sr.Copy(2)
						var sOutput *schema.StreamReader[*schema.Message]
						if p.toolCall {
							sOutput = schema.StreamReaderWithConvert(ss[0], argToContent)
						} else {
							sOutput = ss[0]
						}

						generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))

						return schema.ConcatMessageStream(ss[1])
					}

					msg, err := schema.ConcatMessageStream(sr)
					if err != nil {
						return nil, err
					}

					var output adk.Message
					if p.toolCall {
						if len(msg.ToolCalls) == 0 {
							return nil, fmt.Errorf("no tool call")
						}
						output = schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
					} else {
						output = msg
					}

					generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))

					return msg, nil
				}),
			).
			AppendLambda(
				compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (plan Plan, err error) {
					var planJSON string
					if p.toolCall {
						if len(msg.ToolCalls) == 0 {
							return nil, fmt.Errorf("no tool call")
						}
						planJSON = msg.ToolCalls[0].Function.Arguments
					} else {
						planJSON = msg.Content
					}

					plan = p.newPlan(ctx)
					err = plan.UnmarshalJSON([]byte(planJSON))
					if err != nil {
						return nil, fmt.Errorf("unmarshal plan error: %w", err)
					}

					adk.AddSessionValue(ctx, PlanSessionKey, plan)

					return plan, nil
				}),
			)

		var opts []compose.Option
		if p.toolCall {
			opts = append(opts, compose.WithChatModelOption(model.WithToolChoice(schema.ToolChoiceForced)))
		}

		r, err := c.Compile(ctx, compose.WithGraphName(p.Name(ctx)))
		if err != nil { // unexpected
			generator.Send(&adk.AgentEvent{Err: err})
			return
		}

		_, err = r.Stream(ctx, input, opts...)
		if err != nil {
			generator.Send(&adk.AgentEvent{Err: err})
			return
		}
	}()

	return iterator
}

// NewPlanner creates a new planner agent based on the provided configuration.
// The planner agent uses either ChatModelWithFormattedOutput or ToolCallingChatModel+ToolInfo
// to generate structured Plan output.
//
// If ChatModelWithFormattedOutput is provided, it will be used directly.
// If ToolCallingChatModel is provided, it will be configured with ToolInfo (or PlanToolInfo by default)
// to generate structured Plan output.
func NewPlanner(_ context.Context, cfg *PlannerConfig) (adk.Agent, error) {
	var chatModel model.BaseChatModel
	var toolCall bool
	if cfg.ChatModelWithFormattedOutput != nil {
		chatModel = cfg.ChatModelWithFormattedOutput
	} else {
		toolCall = true
		toolInfo := cfg.ToolInfo
		if toolInfo == nil {
			toolInfo = &PlanToolInfo
		}

		var err error
		chatModel, err = cfg.ToolCallingChatModel.WithTools([]*schema.ToolInfo{toolInfo})
		if err != nil {
			return nil, err
		}
	}

	inputFn := cfg.GenInputFn
	if inputFn == nil {
		inputFn = defaultGenPlannerInputFn
	}

	planParser := cfg.NewPlan
	if planParser == nil {
		planParser = defaultNewPlan
	}

	return &planner{
		toolCall:   toolCall,
		chatModel:  chatModel,
		genInputFn: inputFn,
		newPlan:    planParser,
	}, nil
}

// ExecutionContext is the input information for the executor and the planner.
type ExecutionContext struct {
	UserInput     []adk.Message
	Plan          Plan
	ExecutedSteps []ExecutedStep
}

// GenModelInputFn is a function that generates the input messages for the executor and the planner.
type GenModelInputFn func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error)

// ExecutorConfig provides configuration options for creating an executor agent.
type ExecutorConfig struct {
	// Model is the chat model used by the executor.
	// If the executor uses any tools, this model must support the model.WithTools call option,
	// as that's how the executor configures the model with tool information.
	Model model.BaseChatModel

	// ToolsConfig specifies the tools available to the executor.
	ToolsConfig adk.ToolsConfig

	// MaxIterations defines the upper limit of ChatModel generation cycles.
	// The agent will terminate with an error if this limit is exceeded.
	// Optional. Defaults to 20.
	MaxIterations int

	// GenInputFn generates the input messages for the Executor.
	// Optional. If not provided, defaultGenExecutorInputFn will be used.
	GenInputFn GenModelInputFn
}

type ExecutedStep struct {
	Step   string
	Result string
}

// NewExecutor creates a new executor agent.
func NewExecutor(ctx context.Context, cfg *ExecutorConfig) (adk.Agent, error) {

	genInputFn := cfg.GenInputFn
	if genInputFn == nil {
		genInputFn = defaultGenExecutorInputFn
	}
	genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) {

		plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
		if !ok {
			panic("impossible: plan not found")
		}
		plan_ := plan.(Plan)

		userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
		if !ok {
			panic("impossible: user input not found")
		}
		userInput_ := userInput.([]adk.Message)

		var executedSteps_ []ExecutedStep
		executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
		if ok {
			executedSteps_ = executedStep.([]ExecutedStep)
		}

		in := &ExecutionContext{
			UserInput:     userInput_,
			Plan:          plan_,
			ExecutedSteps: executedSteps_,
		}

		msgs, err := genInputFn(ctx, in)
		if err != nil {
			return nil, err
		}

		return msgs, nil
	}

	agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:          "executor",
		Description:   "an executor agent",
		Model:         cfg.Model,
		ToolsConfig:   cfg.ToolsConfig,
		GenModelInput: genInput,
		MaxIterations: cfg.MaxIterations,
		OutputKey:     ExecutedStepSessionKey,
	})
	if err != nil {
		return nil, err
	}

	return agent, nil
}

func defaultGenExecutorInputFn(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {

	planContent, err := in.Plan.MarshalJSON()
	if err != nil {
		return nil, err
	}

	userMsgs, err := ExecutorPrompt.Format(ctx, map[string]any{
		"input":          formatInput(in.UserInput),
		"plan":           string(planContent),
		"executed_steps": formatExecutedSteps(in.ExecutedSteps),
		"step":           in.Plan.FirstStep(),
	})
	if err != nil {
		return nil, err
	}

	return userMsgs, nil
}

type replanner struct {
	chatModel   model.ToolCallingChatModel
	planTool    *schema.ToolInfo
	respondTool *schema.ToolInfo

	genInputFn GenModelInputFn
	newPlan    NewPlan
}

type ReplannerConfig struct {
	// ChatModel is the model that supports tool calling capabilities.
	// It will be configured with PlanTool and RespondTool to generate updated plans or responses.
	ChatModel model.ToolCallingChatModel

	// PlanTool defines the schema for the Plan tool that can be used with ToolCallingChatModel.
	// Optional. If not provided, the default PlanToolInfo will be used.
	PlanTool *schema.ToolInfo

	// RespondTool defines the schema for the response tool that can be used with ToolCallingChatModel.
	// Optional. If not provided, the default RespondToolInfo will be used.
	RespondTool *schema.ToolInfo

	// GenInputFn generates the input messages for the Replanner.
	// Optional. If not provided, buildGenReplannerInputFn will be used.
	GenInputFn GenModelInputFn

	// NewPlan creates a new Plan instance.
	// The returned Plan will be used to unmarshal the model-generated JSON output from PlanTool.
	// Optional. If not provided, defaultNewPlan will be used.
	NewPlan NewPlan
}

// formatInput formats the input messages into a string.
func formatInput(input []adk.Message) string {
	var sb strings.Builder
	for _, msg := range input {
		sb.WriteString(msg.Content)
		sb.WriteString("\n")
	}

	return sb.String()
}

func formatExecutedSteps(results []ExecutedStep) string {
	var sb strings.Builder
	for _, result := range results {
		sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result))
	}

	return sb.String()
}

func (r *replanner) Name(_ context.Context) string {
	return "replanner"
}

func (r *replanner) Description(_ context.Context) string {
	return "a replanner agent"
}

func (r *replanner) genInput(ctx context.Context) ([]adk.Message, error) {

	executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepSessionKey)
	if !ok {
		panic("impossible: execute result not found")
	}
	executedStep_ := executedStep.(string)

	plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
	if !ok {
		panic("impossible: plan not found")
	}
	plan_ := plan.(Plan)
	step := plan_.FirstStep()

	var executedSteps_ []ExecutedStep
	executedSteps, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
	if ok {
		executedSteps_ = executedSteps.([]ExecutedStep)
	}

	executedSteps_ = append(executedSteps_, ExecutedStep{
		Step:   step,
		Result: executedStep_,
	})
	adk.AddSessionValue(ctx, ExecutedStepsSessionKey, executedSteps_)

	userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
	if !ok {
		panic("impossible: user input not found")
	}
	userInput_ := userInput.([]adk.Message)

	in := &ExecutionContext{
		UserInput:     userInput_,
		Plan:          plan_,
		ExecutedSteps: executedSteps_,
	}
	genInputFn := r.genInputFn
	if genInputFn == nil {
		genInputFn = buildGenReplannerInputFn(r.planTool.Name, r.respondTool.Name)
	}
	msgs, err := genInputFn(ctx, in)
	if err != nil {
		return nil, err
	}

	return msgs, nil
}

func (r *replanner) Run(ctx context.Context, input *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
	iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()

	go func() {
		defer func() {
			panicErr := recover()
			if panicErr != nil {
				e := safe.NewPanicErr(panicErr, debug.Stack())
				generator.Send(&adk.AgentEvent{Err: e})
			}

			generator.Close()
		}()

		callOpt := model.WithToolChoice(schema.ToolChoiceForced)

		c := compose.NewChain[struct{}, any]().
			AppendLambda(
				compose.InvokableLambda(func(ctx context.Context, input struct{}) (output []adk.Message, err error) {
					return r.genInput(ctx)
				}),
			).
			AppendChatModel(r.chatModel).
			AppendLambda(
				compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
					if input.EnableStreaming {
						ss := sr.Copy(2)
						sOutput := schema.StreamReaderWithConvert(ss[0], argToContent)
						generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))
						return schema.ConcatMessageStream(ss[1])
					}

					msg, err := schema.ConcatMessageStream(sr)
					if err != nil {
						return nil, err
					}
					if len(msg.ToolCalls) > 0 {
						output := schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
						generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))
					}
					return msg, nil
				}),
			).
			AppendLambda(
				compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (msgOrPlan any, err error) {
					if len(msg.ToolCalls) == 0 {
						return nil, fmt.Errorf("no tool call")
					}

					// exit
					if msg.ToolCalls[0].Function.Name == r.respondTool.Name {
						action := adk.NewBreakLoopAction(r.Name(ctx))
						generator.Send(&adk.AgentEvent{Action: action})
						return msg, nil
					}

					// replan
					if msg.ToolCalls[0].Function.Name != r.planTool.Name {
						return nil, fmt.Errorf("unexpected tool call: %s", msg.ToolCalls[0].Function.Name)
					}

					plan := r.newPlan(ctx)
					if err = plan.UnmarshalJSON([]byte(msg.ToolCalls[0].Function.Arguments)); err != nil {
						return nil, fmt.Errorf("unmarshal plan error: %w", err)
					}

					adk.AddSessionValue(ctx, PlanSessionKey, plan)

					return plan, nil
				}),
			)

		runnable, err := c.Compile(ctx, compose.WithGraphName(r.Name(ctx)))
		if err != nil {
			generator.Send(&adk.AgentEvent{Err: err})
			return
		}

		_, err = runnable.Stream(ctx, struct{}{}, compose.WithChatModelOption(callOpt))
		if err != nil {
			generator.Send(&adk.AgentEvent{Err: err})
			return
		}
	}()

	return iterator
}

func buildGenReplannerInputFn(planToolName, respondToolName string) GenModelInputFn {
	return func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {
		planContent, err := in.Plan.MarshalJSON()
		if err != nil {
			return nil, err
		}
		msgs, err := ReplannerPrompt.Format(ctx, map[string]any{
			"plan":           string(planContent),
			"input":          formatInput(in.UserInput),
			"executed_steps": formatExecutedSteps(in.ExecutedSteps),
			"plan_tool":      planToolName,
			"respond_tool":   respondToolName,
		})
		if err != nil {
			return nil, err
		}

		return msgs, nil
	}
}

// NewReplanner creates a plan-execute-replan agent wired with plan and respond tools.
// It configures the provided ToolCallingChatModel with the tools and returns an Agent.
func NewReplanner(_ context.Context, cfg *ReplannerConfig) (adk.Agent, error) {
	planTool := cfg.PlanTool
	if planTool == nil {
		planTool = &PlanToolInfo
	}

	respondTool := cfg.RespondTool
	if respondTool == nil {
		respondTool = &RespondToolInfo
	}

	chatModel, err := cfg.ChatModel.WithTools([]*schema.ToolInfo{planTool, respondTool})
	if err != nil {
		return nil, err
	}

	planParser := cfg.NewPlan
	if planParser == nil {
		planParser = defaultNewPlan
	}

	return &replanner{
		chatModel:   chatModel,
		planTool:    planTool,
		respondTool: respondTool,
		genInputFn:  cfg.GenInputFn,
		newPlan:     planParser,
	}, nil
}

// Config provides configuration options for creating a plan-execute-replan agent.
type Config struct {
	// Planner specifies the agent that generates the plan.
	// You can use provided NewPlanner to create a planner agent.
	Planner adk.Agent

	// Executor specifies the agent that executes the plan generated by planner or replanner.
	// You can use provided NewExecutor to create an executor agent.
	Executor adk.Agent

	// Replanner specifies the agent that replans the plan.
	// You can use provided NewReplanner to create a replanner agent.
	Replanner adk.Agent

	// MaxIterations defines the maximum number of loops for 'execute-replan'.
	// Optional. If not provided, 10 will be used as the default.
	MaxIterations int
}

// New creates a new plan-execute-replan agent with the given configuration.
// The plan-execute-replan pattern works in three phases:
// 1. Planning: Generate a structured plan with clear, actionable steps
// 2. Execution: Execute the first step of the plan
// 3. Replanning: Evaluate progress and either complete the task or revise the plan
// This approach enables complex problem-solving through iterative refinement.
func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) {
	maxIterations := cfg.MaxIterations
	if maxIterations <= 0 {
		maxIterations = 10
	}
	loop, err := adk.NewLoopAgent(ctx, &adk.LoopAgentConfig{
		Name:          "execute_replan",
		SubAgents:     []adk.Agent{cfg.Executor, cfg.Replanner},
		MaxIterations: maxIterations,
	})
	if err != nil {
		return nil, err
	}

	return adk.NewSequentialAgent(ctx, &adk.SequentialAgentConfig{
		Name:      "plan_execute_replan",
		SubAgents: []adk.Agent{cfg.Planner, loop},
	})
}
