package agent

import (
	"context"
	"time"

	"go-micro.dev/v6/ai"
	"go-micro.dev/v6/broker"
	"go-micro.dev/v6/client"
	"go-micro.dev/v6/flow"
	"go-micro.dev/v6/registry"
	"go-micro.dev/v6/store"
	"go-micro.dev/v6/wrapper/x402"
	"go.opentelemetry.io/otel/trace"
)

// Option configures an Agent.
type Option func(*Options)

// ApproveFunc decides whether an agent may execute a tool call before it
// runs. Returning false blocks the call; the reason is shown to the
// model so it can adapt. Use it for human-in-the-loop approval or policy
// checks. It is called for actions (service tools and delegate), not for
// the internal plan tool.
type ApproveFunc func(tool string, input map[string]any) (approved bool, reason string)

// ToolFunc handles a custom tool call. Return the result as a string
// (often JSON); return an error to report failure back to the model.
type ToolFunc func(ctx context.Context, input map[string]any) (string, error)

// customTool is a developer-registered tool beyond the agent's services.
type customTool struct {
	def     ai.Tool
	handler ToolFunc
}

// Options holds agent configuration.
type Options struct {
	Name         string
	Services     []string
	Prompt       string
	Provider     string
	Model        string
	APIKey       string
	BaseURL      string
	Address      string
	Registry     registry.Registry
	Client       client.Client
	Broker       broker.Broker
	Store        store.Store
	HistoryLimit int

	// ModelTimeout bounds each provider Generate call (0 disables).
	ModelTimeout time.Duration
	// ModelMaxAttempts bounds provider Generate attempts including the first
	// call. Default 1 — retries are opt-in (enable with ModelRetry). A Generate
	// runs the whole tool-execution turn, so auto-retrying it would re-run
	// already-executed, possibly side-effecting tool calls; keep it explicit.
	ModelMaxAttempts int
	// ModelRetryBackoff is the base delay between transient provider failures
	// (grows exponentially per attempt when retries are enabled).
	ModelRetryBackoff time.Duration
	// ModelRetryJitter adds up to this random delay to each provider retry
	// backoff. Default 0 preserves deterministic timing unless explicitly set.
	ModelRetryJitter time.Duration
	// ToolTimeout bounds each tool execution (0 disables). The timeout is
	// applied before custom tools, delegate, and service RPC calls so context
	// deadlines propagate consistently through the agent loop.
	ToolTimeout time.Duration
	// ToolMaxAttempts bounds tool execution attempts including the first call.
	// Default 1; retries are opt-in because tools can have side effects.
	ToolMaxAttempts int
	// ToolRetryBackoff is the base delay between transient tool failures.
	ToolRetryBackoff time.Duration

	// Memory is the agent's conversation memory. Nil = the default
	// store-backed memory (durable across restarts).
	Memory Memory
	// MemoryRetrievalLimit enables retrieval-backed default memory without
	// compaction. The active conversation stays bounded to this many messages
	// while every turn is archived for deterministic recall.
	MemoryRetrievalLimit int
	// MemoryCompaction enables deterministic compaction/retrieval on the
	// default store-backed memory. Custom Memory implementations can expose
	// retrieval by implementing MemoryRecall.
	MemoryCompaction MemoryCompaction
	// MemoryRecallLimit bounds recalled archived turns injected into a model
	// request (0 disables recall injection).
	MemoryRecallLimit int
	// Checkpoint persists agent Ask runs so callers can resume by run id
	// after a restart without replaying a run that already completed.
	Checkpoint flow.Checkpoint

	// MaxSteps bounds the number of tool executions per Ask (0 =
	// unbounded). Once exceeded, further tool calls are refused and the
	// model is told to stop and summarize. A stopping condition.
	MaxSteps int
	// LoopLimit bounds how many times the agent may call the same tool
	// with the same arguments in one Ask before the call is refused as a
	// no-progress loop (0 = disabled). Catches the agent repeating an
	// identical action — which MaxSteps only bounds by total count.
	LoopLimit int
	// Approve gates each action before it runs. Nil = allow all.
	Approve ApproveFunc
	// MaxSpend bounds paid x402 tool spend per Ask in the asset's smallest
	// unit (0 = disabled). ToolSpend lists known paid tools and their prices.
	MaxSpend  int64
	ToolSpend map[string]int64
	// Payer lets the agent settle x402 Payment Required challenges from tools.
	// Budget bounds autonomous x402 payments per Ask (0 = unlimited).
	Payer  x402.Payer
	Budget int64

	// A2AAddress, if set, makes Run serve this agent over the A2A protocol
	// on that address directly (no separate gateway), e.g. ":4000".
	A2AAddress string

	// TraceProvider enables OpenTelemetry spans for agent runs, model calls,
	// and tool calls. Nil disables instrumentation.
	TraceProvider trace.TracerProvider

	// TraceInputs controls whether agent observability records include raw
	// user messages. It is false by default so spans and persisted run
	// timelines carry correlation and shape without leaking prompts.
	TraceInputs bool

	// tools are developer-registered custom tools (see WithTool).
	tools []customTool
	// wrappers are developer-registered tool-execution wrappers
	// (see WrapTool), applied outside the built-in guardrails.
	wrappers []ai.ToolWrapper
}

func newOptions(opts ...Option) Options {
	o := Options{
		Registry:          registry.DefaultRegistry,
		Client:            client.DefaultClient,
		Store:             store.DefaultStore,
		HistoryLimit:      50,
		ModelTimeout:      30 * time.Second,
		ModelMaxAttempts:  1, // retries opt-in via ModelRetry (see field doc)
		ModelRetryBackoff: 100 * time.Millisecond,
		ToolTimeout:       30 * time.Second,
		ToolMaxAttempts:   1,
		ToolRetryBackoff:  100 * time.Millisecond,
		// On by default and lenient: identical repeated calls are a
		// no-progress loop, never useful. Set LoopLimit(0) to disable.
		LoopLimit: 3,
	}
	for _, opt := range opts {
		opt(&o)
	}
	return o
}

// Name sets the agent name.
func Name(n string) Option {
	return func(o *Options) { o.Name = n }
}

// Services sets which services this agent manages.
func Services(names ...string) Option {
	return func(o *Options) { o.Services = names }
}

// Prompt sets the system prompt.
func Prompt(p string) Option {
	return func(o *Options) { o.Prompt = p }
}

// Provider sets the LLM provider.
func Provider(p string) Option {
	return func(o *Options) { o.Provider = p }
}

// Model sets the LLM model name.
func Model(m string) Option {
	return func(o *Options) { o.Model = m }
}

// APIKey sets the API key for the LLM provider.
func APIKey(k string) Option {
	return func(o *Options) { o.APIKey = k }
}

// BaseURL sets the base URL for the LLM provider. Use this to point
// the provider at a non-default endpoint (e.g., local Ollama, a proxy).
func BaseURL(url string) Option {
	return func(o *Options) { o.BaseURL = url }
}

// Address sets the network address for the agent's service endpoint.
// Use "127.0.0.1:0" in local harnesses/tests to bind an ephemeral loopback
// port and avoid advertising the default service address.
func Address(addr string) Option {
	return func(o *Options) { o.Address = addr }
}

// WithRegistry sets the service registry.
func WithRegistry(r registry.Registry) Option {
	return func(o *Options) { o.Registry = r }
}

// WithClient sets the RPC client.
func WithClient(c client.Client) Option {
	return func(o *Options) { o.Client = c }
}

// WithBroker sets the broker used by the agent service endpoint. Use an
// in-memory broker in local harnesses/tests to avoid sharing the package-wide
// default broker listener across concurrently running examples.
func WithBroker(b broker.Broker) Option {
	return func(o *Options) { o.Broker = b }
}

// WithStore sets the store for agent memory.
func WithStore(s store.Store) Option {
	return func(o *Options) { o.Store = s }
}

// HistoryLimit sets the max conversation messages to retain.
func HistoryLimit(n int) Option {
	return func(o *Options) { o.HistoryLimit = n }
}

// MaxSteps bounds tool executions per Ask (0 = unbounded). A stopping
// condition: beyond the limit, tool calls are refused and the model is
// told to stop and summarize.
func MaxSteps(n int) Option {
	return func(o *Options) { o.MaxSteps = n }
}

// ApproveTool sets a human-in-the-loop / policy hook called before each
// action (service tools and delegate). Returning false blocks the call.
func ApproveTool(fn ApproveFunc) Option {
	return func(o *Options) { o.Approve = fn }
}

// MaxSpend bounds paid x402 tool spend per Ask, in the asset's smallest unit
// (0 = disabled). A paid tool that would exceed the cap is refused before the
// tool handler runs or any payment can be made.
func MaxSpend(amount int64) Option {
	return func(o *Options) { o.MaxSpend = amount }
}

// ToolSpend records the x402 price for a tool, in the asset's smallest unit,
// so MaxSpend can reserve budget before execution. Non-positive amounts are
// treated as free.
func ToolSpend(tool string, amount int64) Option {
	return func(o *Options) {
		if o.ToolSpend == nil {
			o.ToolSpend = map[string]int64{}
		}
		o.ToolSpend[tool] = amount
	}
}

// Payer configures the wallet/signing hook used to settle x402-paid tools.
// Without a payer, payment-required tool results are returned as clear errors.
func Payer(p x402.Payer) Option {
	return func(o *Options) { o.Payer = p }
}

// Budget bounds autonomous x402 payments per Ask, in the asset's smallest
// unit (0 = unlimited). The budget is enforced by wrapper/x402.Client.
func Budget(amount int64) Option {
	return func(o *Options) { o.Budget = amount }
}

// LoopLimit sets how many times the agent may repeat the same tool call
// (same name and arguments) in one Ask before it is refused as a
// no-progress loop. 0 disables loop detection.
func LoopLimit(n int) Option {
	return func(o *Options) { o.LoopLimit = n }
}

// ModelCallTimeout sets the timeout for each provider Generate call.
func ModelCallTimeout(d time.Duration) Option {
	return func(o *Options) { o.ModelTimeout = d }
}

// ToolCallTimeout sets the timeout for each tool execution. It bounds custom
// tools, built-in delegate calls, and service RPC tools with the same context
// deadline so mid-run cancellation and slow tools produce safe error results
// instead of unbounded agent runs. Set 0 to disable.
func ToolCallTimeout(d time.Duration) Option {
	return func(o *Options) { o.ToolTimeout = d }
}

// ModelRetry sets the provider retry budget and backoff for transient failures.
func ModelRetry(maxAttempts int, backoff time.Duration) Option {
	return func(o *Options) {
		o.ModelMaxAttempts = maxAttempts
		o.ModelRetryBackoff = backoff
	}
}

// ModelRetryJitter adds bounded random jitter to provider retry backoff.
// Set 0 to disable.
func ModelRetryJitter(d time.Duration) Option {
	return func(o *Options) { o.ModelRetryJitter = d }
}

// ToolRetry sets the tool retry budget and backoff for transient failures.
// Attempts include the first call. Retries are opt-in because tools may have
// side effects; keep handlers idempotent before enabling this.
func ToolRetry(maxAttempts int, backoff time.Duration) Option {
	return func(o *Options) {
		o.ToolMaxAttempts = maxAttempts
		o.ToolRetryBackoff = backoff
	}
}

// WithA2A makes Run serve the agent over the A2A protocol on addr (e.g.
// ":4000"), so other agents can reach it directly by URL without a
// separate gateway. The agent stays a normal go-micro service as well;
// this adds a second, A2A-native HTTP endpoint that calls it in-process.
func WithA2A(addr string) Option {
	return func(o *Options) { o.A2AAddress = addr }
}

// WithMemory sets the agent's conversation memory. The default is
// store-backed memory keyed by agent name; supply your own to use an
// in-process, database, or semantic store.
func WithMemory(m Memory) Option {
	return func(o *Options) { o.Memory = m }
}

// RetrievalMemory enables deterministic, store-backed retrieval memory for
// the default agent memory without compaction. Active context is capped at
// activeLimit messages while every turn is archived in the store for Recall.
func RetrievalMemory(activeLimit int) Option {
	return func(o *Options) {
		o.MemoryRetrievalLimit = activeLimit
		if o.MemoryRecallLimit == 0 {
			o.MemoryRecallLimit = 5
		}
	}
}

// CompactMemory enables deterministic, store-backed memory compaction for the
// default agent memory. Older turns are summarized once active context exceeds
// maxMessages, keepRecent newest turns remain verbatim, and recalled archived
// turns are injected into matching future asks.
func CompactMemory(maxMessages, keepRecent int) Option {
	return func(o *Options) {
		o.MemoryCompaction.MaxMessages = maxMessages
		o.MemoryCompaction.KeepRecent = keepRecent
		if o.MemoryRecallLimit == 0 {
			o.MemoryRecallLimit = 5
		}
	}
}

// MemorySummarizer sets the deterministic summarization hook used by the
// default compacting memory. It is optional; without it, compacted memory uses
// a provider-neutral text summary. The hook receives the older messages being
// removed from active context and returns the replacement summary message.
func MemorySummarizer(fn MemorySummaryFunc) Option {
	return func(o *Options) { o.MemoryCompaction.Summarize = fn }
}

// MemoryRecallLimit sets how many archived turns a memory backend may inject
// into a model request for the current Ask. Use 0 to disable retrieval.
func MemoryRecallLimit(n int) Option {
	return func(o *Options) { o.MemoryRecallLimit = n }
}

// WithCheckpoint sets the durability backend for agent Ask runs. The
// Checkpoint interface is shared with flow so services, agents, and workflows
// can use one execution history backend. When set, each Ask is saved as a
// single-step run keyed by run id; Resume returns a completed run's persisted
// response instead of calling the model again.
func WithCheckpoint(c flow.Checkpoint) Option {
	return func(o *Options) { o.Checkpoint = c }
}

// WrapTool registers a tool-execution wrapper, the tool-side analog of
// a client/server middleware wrapper. Each wrapper takes the next handler
// and returns a new one; code before the next(...) call runs before the
// tool executes, code after runs after. Use it for logging, metrics,
// retries, or custom policy. Wrappers run outside the built-in guardrails
// (MaxSteps, LoopLimit, ApproveTool), so they observe every call and its
// result, including refusals. Multiple wrappers compose outermost-first.
//
//	micro.NewAgent("worker", micro.AgentWrapTool(
//	    func(next ai.ToolHandler) ai.ToolHandler {
//	        return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
//	            res := next(ctx, call)
//	            log.Printf("id=%s tool=%s", call.ID, call.Name)
//	            return res
//	        }
//	    }))
func WrapTool(w ...ai.ToolWrapper) Option {
	return func(o *Options) {
		o.wrappers = append(o.wrappers, w...)
	}
}

// WithTool registers a custom tool the agent can call, beyond the
// services it discovers — a local function, an external API, anything.
// properties is the JSON-schema map for the tool's parameters.
func WithTool(name, description string, properties map[string]any, handler ToolFunc) Option {
	return func(o *Options) {
		o.tools = append(o.tools, customTool{
			def: ai.Tool{
				Name:         name,
				OriginalName: name,
				Description:  description,
				Properties:   properties,
			},
			handler: handler,
		})
	}
}

// TraceProvider enables OpenTelemetry tracing for agent runs. The persisted
// run timeline is recorded even when TraceProvider is nil; trace/span IDs are
// added only when a provider is configured.
func TraceProvider(tp trace.TracerProvider) Option {
	return func(o *Options) { o.TraceProvider = tp }
}

// TraceInputs opts in to recording raw user messages on agent run events.
// By default inputs are redacted from OpenTelemetry spans and persisted run
// timelines; use this only when the observability backend is approved to store
// prompt content.
func TraceInputs(enabled bool) Option {
	return func(o *Options) { o.TraceInputs = enabled }
}
