// Package agent provides the Agent abstraction for Go Micro.
//
// An Agent is a service with an LLM inside it. It registers a Chat
// RPC endpoint, discovers its assigned services' tools, and
// orchestrates them intelligently.
//
//	agent := micro.NewAgent("task-mgr",
//	    micro.AgentServices("task"),
//	    micro.AgentPrompt("You manage tasks."),
//	    micro.AgentProvider("anthropic"),
//	)
//	agent.Run()
package agent

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	pb "go-micro.dev/v6/agent/proto"
	"go-micro.dev/v6/ai"
	"go-micro.dev/v6/flow"
	"go-micro.dev/v6/gateway/a2a"
	"go-micro.dev/v6/server"
	"go-micro.dev/v6/store"

	_ "go-micro.dev/v6/ai/anthropic"
	_ "go-micro.dev/v6/ai/atlascloud"
	_ "go-micro.dev/v6/ai/gemini"
	_ "go-micro.dev/v6/ai/groq"
	_ "go-micro.dev/v6/ai/minimax"
	_ "go-micro.dev/v6/ai/mistral"
	_ "go-micro.dev/v6/ai/ollama"
	_ "go-micro.dev/v6/ai/openai"
	_ "go-micro.dev/v6/ai/together"
)

// Agent is the interface for an AI agent that manages services.
type Agent interface {
	Name() string
	Init(...Option)
	Options() Options
	Ask(ctx context.Context, message string) (*Response, error)
	Stream(ctx context.Context, message string) (ai.Stream, error)
	Run() error
	Stop() error
	String() string
}

// Response is what an agent returns from Chat.
type Response struct {
	Reply     string
	ToolCalls []ai.ToolCall
	Agent     string

	// RunID correlates this Ask with tool calls, trace spans, and the
	// persisted run timeline. ParentID is set when this response belongs
	// to a delegated sub-agent run.
	RunID    string
	ParentID string
}

type agentImpl struct {
	opts   Options
	model  ai.Model
	tools  *ai.Tools
	mem    Memory
	server server.Server
	mu     sync.Mutex

	// ephemeral marks a short-lived sub-agent created by delegation.
	// Ephemeral agents run with an isolated context: they load and
	// persist no history, and have no built-in tools (so they cannot
	// plan or re-delegate).
	ephemeral bool

	// steps counts tool executions in the current Ask, for MaxSteps.
	steps int
	// spend counts reserved paid-tool spend in the current Ask, for MaxSpend.
	spend int64
	// calls counts identical tool calls (name+args) in the current Ask,
	// for LoopLimit.
	calls map[string]int

	// runID correlates the tool calls of the current Ask; parentRunID is
	// the run that delegated to this one (set on ephemeral sub-agents).
	// Both are surfaced to tool wrappers via ai.RunInfo on the context.
	runID       string
	parentRunID string

	// pause records a guardrail approval pause raised during the current
	// Ask. The model provider only sees a refused tool result; the agent
	// converts it into a durable paused run instead of completing the run.
	pause *approvalPause

	// currentRun points at the checkpoint record for the Ask currently
	// holding mu. Tool execution updates it so resumed runs can reuse
	// completed tool results without replaying side effects.
	currentRun *flow.Run

	// delegateCalls collapses concurrent equivalent delegate tool calls so a
	// provider replay cannot fan out duplicate delegated side effects before the
	// durable delegate-result cache is written.
	delegateMu    sync.Mutex
	delegateCalls map[string]*delegateCall

	// stopCh lets Stop unblock Run. Without this, tests and harnesses that
	// start agents in goroutines can leave Run parked forever after the RPC
	// server has been stopped.
	stopCh chan struct{}
}

// New creates a new Agent.
func New(opts ...Option) Agent {
	return &agentImpl{
		opts: newOptions(opts...),
	}
}

// newEphemeral creates a short-lived sub-agent for a delegated subtask.
// It shares the parent's provider, model, and infrastructure but runs
// with an isolated context: it loads and persists no history and has no
// built-in tools (so it can neither plan nor re-delegate). Returns the
// concrete type because ephemeral is an internal construction detail,
// not a public option.
func newEphemeral(opts ...Option) *agentImpl {
	return &agentImpl{
		opts:      newOptions(opts...),
		ephemeral: true,
	}
}

func (a *agentImpl) Name() string {
	return a.opts.Name
}

func (a *agentImpl) Init(opts ...Option) {
	for _, o := range opts {
		o(&a.opts)
	}
	a.setup()
}

func (a *agentImpl) Options() Options {
	return a.opts
}

func (a *agentImpl) String() string {
	return "agent"
}

func (a *agentImpl) setup() {
	a.setupWithToolHandler(nil)
}

func (a *agentImpl) setupWithToolHandler(handler ai.ToolHandler) {
	var modelOpts []ai.Option
	modelOpts = append(modelOpts, ai.WithAPIKey(a.opts.APIKey))
	if a.opts.Model != "" {
		modelOpts = append(modelOpts, ai.WithModel(a.opts.Model))
	}
	if a.opts.BaseURL != "" {
		modelOpts = append(modelOpts, ai.WithBaseURL(a.opts.BaseURL))
	}

	// Reuse the existing tools instance: its name map is populated by
	// discoverTools, and rebuilding it here would orphan a base handler that
	// already captured the old instance (breaking StreamAsk tool resolution).
	if a.tools == nil {
		a.tools = ai.NewTools(a.opts.Registry, ai.ToolClient(a.opts.Client))
	}
	if handler == nil {
		handler = a.toolHandler()
	}
	modelOpts = append(modelOpts, ai.WithToolHandler(handler))
	a.model = ai.New(a.opts.Provider, modelOpts...)
	if a.model != nil {
		a.model = a.tracedModel(a.model)
	}

	if a.mem != nil {
		return
	}

	// Memory is pluggable. Use the configured one, otherwise the default
	// store-backed memory — except ephemeral sub-agents, which keep an
	// isolated, non-persistent context.
	switch {
	case a.opts.Memory != nil:
		a.mem = a.opts.Memory
	case a.ephemeral:
		a.mem = NewInMemory(a.opts.HistoryLimit)
	case a.opts.MemoryCompaction.MaxMessages > 0:
		a.mem = NewCompactingMemoryWithOptions(a.stateStore(), "history", a.opts.MemoryCompaction)
	case a.opts.MemoryRetrievalLimit > 0:
		a.mem = NewRetrievalMemory(a.stateStore(), "history", a.opts.MemoryRetrievalLimit)
	default:
		a.mem = NewMemory(a.stateStore(), "history", a.opts.HistoryLimit)
	}
}

// stateStore returns the agent's own state store, scoped to its name so
// memory and plan live in their own table ("agent/{name}") rather than a
// shared global one. The scoped handle injects the database/table per
// operation without mutating the underlying store.
func (a *agentImpl) stateStore() store.Store {
	s := a.opts.Store
	if s == nil {
		s = store.DefaultStore
	}
	return store.Scope(s, "agent", a.opts.Name)
}

// Ask sends a message and returns the agent's response.
// This is the programmatic API for direct use.
func (a *agentImpl) Ask(ctx context.Context, message string) (*Response, error) {
	return a.ask(ctx, message, a.parentRunID)
}

// Stream sends a message and returns a streaming model response. Tool-calling
// agent runs still use Ask; Stream is for chat turns where immediate token
// delivery is more important than tool orchestration.
func (a *agentImpl) Stream(ctx context.Context, message string) (ai.Stream, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if a.model == nil {
		a.setup()
	}
	toolList, err := a.discoverTools()
	if err != nil {
		return nil, fmt.Errorf("discover tools: %w", err)
	}
	runID := uuid.New().String()
	ctx = ai.WithRunInfo(ctx, ai.RunInfo{
		RunID:    runID,
		ParentID: a.parentRunID,
		Agent:    a.opts.Name,
	})
	messages := append([]ai.Message(nil), a.mem.Messages()...)
	messages = append(messages, ai.Message{Role: "user", Content: message})
	stream, err := a.model.Stream(ctx, &ai.Request{
		Prompt:       message,
		SystemPrompt: a.buildPrompt(),
		Tools:        toolList,
		Messages:     messages,
	})
	if err != nil {
		return nil, err
	}
	if err := ctx.Err(); err != nil {
		_ = stream.Close()
		return nil, err
	}
	a.mem.Add("user", message)
	return &memoryRecordingStream{stream: stream, memory: a.mem}, nil
}

// StreamChat serves the Agent.StreamChat RPC endpoint by forwarding stream-capable
// remote clients to the agent streaming path. If the model cannot stream, the
// underlying error is returned so callers can fall back to Agent.Chat.
func (a *agentImpl) StreamChat(ctx context.Context, stream pb.Agent_StreamChatStream) error {
	req, err := stream.Recv()
	if err != nil {
		return err
	}
	aiStream, err := a.streamAskAI(ctx, req.Message)
	if err != nil {
		return err
	}
	defer aiStream.Close()
	for {
		chunk, err := aiStream.Recv()
		if errors.Is(err, io.EOF) {
			return nil
		}
		if err != nil {
			return err
		}
		if chunk == nil || chunk.Reply == "" {
			continue
		}
		if err := stream.Send(&pb.ChatResponse{Reply: chunk.Reply, Agent: a.opts.Name}); err != nil {
			return err
		}
	}
}

// Pending returns checkpointed agent runs that have not completed. It mirrors
// flow.Pending for startup recovery loops that drain durable agent work.
func Pending(ctx context.Context, ag Agent) ([]flow.Run, error) {
	a, ok := ag.(*agentImpl)
	if !ok {
		return nil, fmt.Errorf("agent pending: unsupported agent implementation %T", ag)
	}
	return a.pending(ctx)
}

// ResumePending resumes every checkpointed agent run that has not completed
// yet, in the same oldest-first order returned by Pending.
//
// It is a convenience for service startup and recovery loops: after recreating
// an agent with the same checkpoint store, call ResumePending to drain the
// durable backlog without listing and resuming each run manually. If any run
// fails again, ResumePending stops and returns that run id with the error so
// callers can log, alert, or retry later without hiding the failing run.
func ResumePending(ctx context.Context, ag Agent) (string, error) {
	a, ok := ag.(*agentImpl)
	if !ok {
		return "", fmt.Errorf("agent resume pending: unsupported agent implementation %T", ag)
	}
	runs, err := a.pending(ctx)
	if err != nil {
		return "", err
	}
	for _, run := range runs {
		if _, err := a.resume(ctx, run.ID); err != nil {
			return run.ID, err
		}
	}
	return "", nil
}

func (a *agentImpl) ask(ctx context.Context, message, parentRunID string) (*Response, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	if a.model == nil {
		a.setup()
	}

	return a.askLocked(ctx, uuid.New().String(), message, parentRunID, nil, true)
}

func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID string, existing *flow.Run, addUserMessage bool) (*Response, error) {
	toolList, err := a.discoverTools()
	if err != nil {
		return nil, fmt.Errorf("discover tools: %w", err)
	}

	if addUserMessage {
		a.mem.Add("user", message)
	}
	a.steps = 0
	a.spend = 0
	a.calls = map[string]int{}
	a.pause = nil

	// Correlate this run's tool calls and surface lineage to wrappers.
	a.runID = runID
	ctx = ai.WithRunInfo(ctx, ai.RunInfo{
		RunID:    a.runID,
		ParentID: parentRunID,
		Agent:    a.opts.Name,
	})
	run := a.newCheckpointRun(runID, message, parentRunID, existing)
	a.currentRun = &run
	defer func() { a.currentRun = nil }()
	if err := a.saveRun(ctx, run); err != nil {
		return nil, err
	}
	ctx, endRun := a.startRun(ctx, message)
	if existing != nil {
		a.recordTimelineEvent(ctx, RunEvent{Time: time.Now(), RunID: runID, ParentID: parentRunID, Agent: a.opts.Name, Kind: "resume", Name: run.State.Stage})
	}
	defer func() { endRun(err) }()

	messages := a.mem.Messages()
	if recall, ok := a.mem.(MemoryRecall); ok && a.opts.MemoryRecallLimit > 0 {
		if recalled := recall.Recall(message, a.opts.MemoryRecallLimit); len(recalled) > 0 {
			messages = append([]ai.Message{{
				Role:    "system",
				Content: "Relevant recalled memory follows; use it as durable prior context without assuming the whole conversation was replayed.",
			}}, append(recalled, messages...)...)
		}
	}

	// Some providers satisfy a saved plan one outstanding item per turn,
	// especially when the final item delegates to another agent. Allow enough
	// continuations for the services → agents → workflows harness to complete
	// every planned side effect without weakening the final unfinished-plan guard.
	const maxPlanCompletionTurns = 6
	var resp *ai.Response
	for planCompletionTurn := 0; ; planCompletionTurn++ {
		resp, err = ai.GenerateWithRetry(ctx, a.model, &ai.Request{
			Prompt:       message,
			SystemPrompt: a.buildPrompt(),
			Tools:        toolList,
			Messages:     messages,
		}, ai.GeneratePolicy{
			Timeout:     a.opts.ModelTimeout,
			MaxAttempts: a.opts.ModelMaxAttempts,
			Backoff:     a.opts.ModelRetryBackoff,
			Jitter:      a.opts.ModelRetryJitter,
		})
		if err != nil {
			run.Status = agentRunFailureStatus(err)
			failureKind := ai.ClassifyError(err)
			attempts := agentRunFailureAttempts(err)
			err = agentOperationalError(err)
			if a.currentRun != nil {
				run.Steps = a.currentRun.Steps
			}
			if len(run.Steps) == 0 {
				run.Steps = []flow.StepRecord{{Name: agentAskStep}}
			}
			run.Steps[0].Status = run.Status
			run.Steps[0].Attempts = attempts
			run.Steps[0].Error = err.Error()
			run.Steps[0].ErrorKind = string(failureKind)
			_ = a.saveRun(ctx, run)
			return nil, err
		}
		if a.pause != nil && a.opts.Checkpoint != nil {
			run.Status = "paused"
			run.State.Stage = agentApprovalStep
			run.State.Data = []byte(message)
			if a.pause.Tool == toolHumanInput {
				run.State.Stage = agentInputStep
				_ = run.State.Set(inputPause{OriginalMessage: message, Prompt: a.pause.Message})
			}
			run.Steps[0].Status = "paused"
			run.Steps[0].Error = a.pause.Message
			run.Steps[0].Result = a.pause.Tool
			if err := a.saveRun(ctx, run); err != nil {
				return nil, err
			}
			return nil, fmt.Errorf("agent run %s paused for approval: %s", run.ID, a.pause.Message)
		}

		if len(resp.ToolCalls) == 0 {
			if calls, answer, ok := a.executeTextToolCalls(ctx, resp.Reply, toolList); ok {
				resp.ToolCalls = calls
				if resp.Answer == "" {
					resp.Answer = answer
				}
				trimmedReply := strings.TrimSpace(resp.Reply)
				if strings.HasPrefix(trimmedReply, "{") || strings.HasPrefix(trimmedReply, "[") || strings.HasPrefix(trimmedReply, "```") {
					resp.Reply = ""
				}
			}
		} else if calls, answer, ok := a.executeAdditionalTextToolCalls(ctx, resp.Reply, toolList, resp.ToolCalls); ok {
			resp.ToolCalls = append(resp.ToolCalls, calls...)
			if answer != "" {
				if resp.Answer == "" {
					resp.Answer = answer
				} else {
					resp.Answer += "\n" + answer
				}
			}
		}

		if a.opts.Checkpoint != nil {
			if unfinished := a.unfinishedPlanSteps(); len(unfinished) > 0 && planCompletionTurn < maxPlanCompletionTurns {
				if resp.Reply != "" {
					a.mem.Add("assistant", resp.Reply)
				}
				if resp.Answer != "" {
					a.mem.Add("assistant", resp.Answer)
				}
				message = fmt.Sprintf("Continue the same run by calling the required tool(s) for the unfinished plan steps below. Do not repeat completed work, do not provide a final answer yet, and complete at least one unfinished step this turn if a matching tool is available. Unfinished plan steps: %s", strings.Join(unfinished, ", "))
				a.mem.Add("user", message)
				messages = a.mem.Messages()
				continue
			}
		}
		if toolName := partialTextToolCallName(resp.Reply, toolList); len(resp.ToolCalls) == 0 && toolName != "" && planCompletionTurn < maxPlanCompletionTurns {
			if resp.Reply != "" {
				a.mem.Add("assistant", resp.Reply)
			}
			message = fmt.Sprintf("Your previous response started a %q tool call but did not finish valid tool-call markup or JSON arguments, so no tool was executed. Retry the same step now by emitting one complete valid tool call for %q. Do not describe the action in prose, and do not claim completion until the tool call succeeds.", toolName, toolName)
			a.mem.Add("user", message)
			messages = a.mem.Messages()
			continue
		}
		break
	}

	if resp.Reply != "" {
		a.mem.Add("assistant", resp.Reply)
	}
	if resp.Answer != "" {
		a.mem.Add("assistant", resp.Answer)
	}

	reply := resp.Reply
	if resp.Answer != "" {
		if reply != "" {
			reply += "\n\n"
		}
		reply += resp.Answer
	}

	completedToolCalls := checkpointToolCalls(run.Steps)
	if a.currentRun != nil {
		completedToolCalls = checkpointToolCalls(a.currentRun.Steps)
	}
	res := &Response{
		Reply:     reply,
		ToolCalls: mergeCheckpointToolCalls(completedToolCalls, resp.ToolCalls),
		Agent:     a.opts.Name,
		RunID:     a.runID,
		ParentID:  parentRunID,
	}
	if a.opts.Checkpoint != nil {
		if unfinished := a.unfinishedPlanSteps(); len(unfinished) > 0 {
			err = fmt.Errorf("agent run %s has unfinished plan steps: %s", run.ID, strings.Join(unfinished, ", "))
			run.Status = "failed"
			run.State.Stage = agentAskStep
			run.State.Data = []byte(message)
			if a.currentRun != nil {
				run.Steps = a.currentRun.Steps
			}
			if len(run.Steps) == 0 {
				run.Steps = []flow.StepRecord{{Name: agentAskStep}}
			}
			run.Steps[0].Status = "failed"
			run.Steps[0].Error = err.Error()
			_ = a.saveRun(ctx, run)
			return nil, err
		}
	}
	run.Status = "done"
	run.State.Stage = ""
	if b, marshalErr := json.Marshal(res); marshalErr == nil {
		run.State.Data = b
	}
	if a.currentRun != nil {
		run.Steps = a.currentRun.Steps
	}
	if len(run.Steps) == 0 {
		run.Steps = []flow.StepRecord{{Name: agentAskStep}}
	}
	run.Steps[0].Status = "done"
	run.Steps[0].Attempts++
	run.Steps[0].Result = reply
	if err := a.saveRun(ctx, run); err != nil {
		return nil, err
	}
	return res, nil
}

// Chat implements the proto AgentHandler interface for RPC.
// @example {"message": "What tasks are overdue?"}
func (a *agentImpl) Chat(ctx context.Context, req *pb.ChatRequest, rsp *pb.ChatResponse) error {
	resp, err := a.ask(ctx, req.Message, req.ParentId)
	if err != nil {
		return err
	}
	rsp.Reply = resp.Reply
	rsp.Agent = resp.Agent
	rsp.RunId = resp.RunID
	rsp.ParentId = resp.ParentID
	for _, tc := range resp.ToolCalls {
		input, _ := json.Marshal(tc.Input)
		rsp.ToolCalls = append(rsp.ToolCalls, &pb.ToolCall{
			Id:     tc.ID,
			Name:   tc.Name,
			Input:  string(input),
			Result: tc.Result,
		})
	}
	return nil
}

// Run starts the agent as a service with a Chat RPC endpoint.
func (a *agentImpl) Run() error {
	if a.model == nil {
		a.setup()
	}

	serverOpts := []server.Option{
		server.Name(a.opts.Name),
		server.Address(a.opts.Address),
		server.Registry(a.opts.Registry),
		server.Metadata(map[string]string{
			"type":     "agent",
			"services": strings.Join(a.opts.Services, ","),
		}),
	}
	if a.opts.Broker != nil {
		serverOpts = append(serverOpts, server.Broker(a.opts.Broker))
	}
	a.server = server.NewServer(serverOpts...)

	_ = pb.RegisterAgentHandler(a.server, a)

	if err := a.server.Start(); err != nil {
		return fmt.Errorf("failed to start agent: %w", err)
	}

	stopCh := make(chan struct{})
	a.mu.Lock()
	a.stopCh = stopCh
	a.mu.Unlock()

	fmt.Printf("Agent %s registered (manages: %s)\n", a.opts.Name, strings.Join(a.opts.Services, ", "))

	// Optionally serve the agent directly over the A2A protocol, calling
	// Ask in-process — no separate gateway needed to be queried by URL.
	if a.opts.A2AAddress != "" {
		card := a2a.Card(a.opts.Name, "http://localhost"+a.opts.A2AAddress, "", a.opts.Services)
		handler := a2a.NewAgentStreamHandler(card, func(ctx context.Context, text string) (string, error) {
			resp, err := a.Ask(ctx, text)
			if err != nil {
				return "", err
			}
			return resp.Reply, nil
		}, a.streamAskAI)
		go func() {
			if err := http.ListenAndServe(a.opts.A2AAddress, handler); err != nil {
				fmt.Printf("agent %s A2A server: %v\n", a.opts.Name, err)
			}
		}()
		fmt.Printf("Agent %s serving A2A on %s\n", a.opts.Name, a.opts.A2AAddress)
	}

	<-stopCh
	return nil
}

func (a *agentImpl) Stop() error {
	a.mu.Lock()
	if a.stopCh != nil {
		close(a.stopCh)
		a.stopCh = nil
	}
	a.mu.Unlock()
	if a.server != nil {
		return a.server.Stop()
	}
	return nil
}

func (a *agentImpl) discoverTools() ([]ai.Tool, error) {
	all, err := a.tools.Discover()
	if err != nil {
		return nil, err
	}

	var scoped []ai.Tool
	for _, t := range all {
		if strings.HasPrefix(t.OriginalName, a.opts.Name+".") {
			continue
		}
		if len(a.opts.Services) == 0 {
			scoped = append(scoped, t)
			continue
		}
		for _, svc := range a.opts.Services {
			if strings.HasPrefix(t.OriginalName, svc+".") {
				scoped = append(scoped, t)
				break
			}
		}
	}

	// Developer-registered custom tools (WithTool).
	for i := range a.opts.tools {
		scoped = append(scoped, a.opts.tools[i].def)
	}

	// Expose the agent's own capabilities (plan, delegate) as tools.
	// Ephemeral sub-agents don't get them.
	if !a.ephemeral {
		scoped = append(scoped, builtinTools()...)
	}
	return scoped, nil
}

func (a *agentImpl) buildPrompt() string {
	var base string
	switch {
	case a.opts.Prompt != "":
		base = a.opts.Prompt
	case len(a.opts.Services) > 0:
		base = fmt.Sprintf("You are the %s agent. You manage these services: %s. Use the available tools to fulfill requests.",
			a.opts.Name, strings.Join(a.opts.Services, ", "))
	default:
		base = fmt.Sprintf("You are the %s agent. Use the available tools to fulfill requests.", a.opts.Name)
	}

	// Keep the agent oriented: surface its saved plan, if any.
	if !a.ephemeral {
		if plan := a.loadPlan(); plan != "" {
			base += "\n\nYour current plan (update it with the plan tool as you make progress):\n" + plan
		}
	}
	return base
}
