// Agent Tool Wrappers — middleware around tool execution
//
// Every tool call an agent makes runs through ai.ToolHandler. WrapTool
// wraps that handler the same way client.CallWrapper and
// server.HandlerWrapper wrap RPCs: a wrapper takes the next handler and
// returns a new one, so code before the next(...) call runs before the
// tool and code after runs after.
//
// This example registers two wrappers on one agent:
//
//   - observe: times every call and records a count per tool, keyed so
//     you can correlate by call ID. Pure "lifecycle hook" — it observes,
//     it doesn't change behavior.
//   - retry:   re-runs a call whose result comes back as an error, up to
//     a few attempts. The "weather" service fails the first time it is
//     hit and succeeds after, so the retry wrapper turns a transient
//     failure into a success without the model ever seeing it.
//
// Wrappers compose outermost-first, so observe (registered first) wraps
// retry: it sees one logical call even though retry may run it twice.
//
// Run (needs an LLM provider key):
//
//	MICRO_AI_PROVIDER=anthropic MICRO_AI_API_KEY=sk-ant-... go run main.go
package main

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"go-micro.dev/v6"
	"go-micro.dev/v6/ai"
)

// ---------------------------------------------------------------------------
// weather service — flaky on purpose
// ---------------------------------------------------------------------------

type ForecastRequest struct {
	City string `json:"city" description:"City to get the forecast for (required)"`
}

type ForecastResponse struct {
	City     string `json:"city" description:"The city"`
	Forecast string `json:"forecast" description:"Human-readable forecast"`
}

type WeatherService struct {
	mu    sync.Mutex
	calls int
}

// Forecast returns the weather forecast for a city. It fails on the very
// first call (to simulate a transient dependency error) and succeeds
// afterwards, so the retry wrapper has something to recover from.
//
// @example {"city": "London"}
func (s *WeatherService) Forecast(ctx context.Context, req *ForecastRequest, rsp *ForecastResponse) error {
	s.mu.Lock()
	s.calls++
	n := s.calls
	s.mu.Unlock()

	if n == 1 {
		return fmt.Errorf("weather upstream temporarily unavailable")
	}
	rsp.City = req.City
	rsp.Forecast = "18°C and clear"
	return nil
}

// ---------------------------------------------------------------------------
// wrappers
// ---------------------------------------------------------------------------

// metrics is collected by the observe wrapper. A real deployment would
// emit these to OpenTelemetry or Prometheus; here we just print them.
type metrics struct {
	mu     sync.Mutex
	counts map[string]int
	took   map[string]time.Duration
}

func newMetrics() *metrics {
	return &metrics{counts: map[string]int{}, took: map[string]time.Duration{}}
}

// observe times each tool call and records a per-tool count. It mirrors a
// service-side metrics wrapper: measure around next(...), record, return
// the result untouched.
func (m *metrics) observe(next ai.ToolHandler) ai.ToolHandler {
	return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
		start := time.Now()
		res := next(ctx, call)
		took := time.Since(start)

		m.mu.Lock()
		m.counts[call.Name]++
		m.took[call.Name] += took
		m.mu.Unlock()

		fmt.Printf("  [observe] id=%s tool=%s took=%s\n", shortID(call.ID), call.Name, took.Round(time.Millisecond))
		return res
	}
}

// retry re-runs a call whose result comes back as an error, up to
// attempts times. Because it sits inside observe, the outer wrapper still
// sees one logical call even though retry may run next more than once.
//
// Developer wrappers run outside the built-in guardrails, so next here is
// the full guardrail stack: each retry is also seen by loop detection.
// Keep LoopLimit at or above your retry count (the default 3 covers the
// 2 attempts this example makes), or disable it with AgentLoopLimit(0)
// when a wrapper is responsible for repetition.
func retry(attempts int) ai.ToolWrapper {
	return func(next ai.ToolHandler) ai.ToolHandler {
		return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
			var res ai.ToolResult
			for i := 1; i <= attempts; i++ {
				res = next(ctx, call)
				if !isError(res) {
					return res
				}
				if i < attempts {
					fmt.Printf("  [retry]   tool=%s attempt %d failed, retrying\n", call.Name, i)
				}
			}
			return res
		}
	}
}

// isError reports whether a tool result is an error. The RPC handler
// encodes failures as a JSON object with an "error" field in Content.
func isError(res ai.ToolResult) bool {
	return strings.Contains(res.Content, `"error"`)
}

func shortID(id string) string {
	if len(id) > 8 {
		return id[:8]
	}
	if id == "" {
		return "-"
	}
	return id
}

func main() {
	provider, apiKey := detectProvider()
	if apiKey == "" {
		fmt.Println("No LLM key found. Set a provider key and run again, e.g.:")
		fmt.Println("  export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY, GEMINI_API_KEY, ...")
		fmt.Println("  go run main.go")
		return
	}
	fmt.Printf("Using provider %q\n", provider)

	weather := micro.NewService("weather")
	weather.Handle(new(WeatherService))
	go weather.Run()

	m := newMetrics()

	agent := micro.NewAgent("forecaster",
		micro.AgentServices("weather"),
		micro.AgentPrompt("You report the weather. Use the weather service to answer."),
		micro.AgentProvider(provider),
		micro.AgentAPIKey(apiKey),
		// observe is registered first, so it is the outer wrapper and
		// retry is the inner one.
		micro.AgentWrapTool(m.observe, retry(3)),
	)

	// Give the service a moment to register.
	time.Sleep(2 * time.Second)

	resp, err := agent.Ask(context.Background(), "What's the weather in London?")
	if err != nil {
		fmt.Println("error:", err)
		os.Exit(1)
	}

	fmt.Println("\n--- reply ---")
	fmt.Println(resp.Reply)

	fmt.Println("\n--- tool metrics ---")
	m.mu.Lock()
	for name, n := range m.counts {
		fmt.Printf("  %s: %d call(s), total %s\n", name, n, m.took[name].Round(time.Millisecond))
	}
	m.mu.Unlock()
}

// detectProvider picks an LLM provider and key from the environment.
// MICRO_AI_PROVIDER / MICRO_AI_API_KEY win if set; otherwise it falls
// back to the first provider-specific key it finds.
func detectProvider() (provider, apiKey string) {
	provider = os.Getenv("MICRO_AI_PROVIDER")
	apiKey = os.Getenv("MICRO_AI_API_KEY")
	if apiKey != "" {
		if provider == "" {
			provider = "anthropic"
		}
		return provider, apiKey
	}

	for _, p := range []struct{ name, env string }{
		{"anthropic", "ANTHROPIC_API_KEY"},
		{"openai", "OPENAI_API_KEY"},
		{"gemini", "GEMINI_API_KEY"},
		{"groq", "GROQ_API_KEY"},
		{"mistral", "MISTRAL_API_KEY"},
		{"together", "TOGETHER_API_KEY"},
		{"atlascloud", "ATLASCLOUD_API_KEY"},
	} {
		if v := os.Getenv(p.env); v != "" {
			return p.name, v
		}
	}
	return "", ""
}
