// Agent Ollama — a self-contained agent powered by Ollama Cloud.
//
// This example demonstrates the full harness loop — service tools, custom
// tools, agent memory, guardrails, and streaming — using the Ollama
// provider with gpt-oss:120b on Ollama Cloud.
//
// It creates a "knowledge" service with two endpoints (Add, Search) that
// the agent discovers as tools, plus a custom "current_time" tool. The
// agent answers natural-language questions by calling those tools.
//
// Run (Ollama Cloud — default):
//
//	OLLAMA_API_KEY=your-key go run main.go
//
// Run (local Ollama):
//
//	OLLAMA_BASE_URL=http://localhost:11434 \
//	OLLAMA_MODEL=llama3.2 \
//	go run main.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

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

// ---------------------------------------------------------------------------
// knowledge service — a tiny in-memory knowledge base
// ---------------------------------------------------------------------------

type KnowledgeEntry struct {
	ID      string `json:"id" description:"Unique entry identifier"`
	Topic   string `json:"topic" description:"Topic or category"`
	Content string `json:"content" description:"The knowledge content"`
}

type AddKnowledgeRequest struct {
	Topic   string `json:"topic" description:"Topic or category (required)"`
	Content string `json:"content" description:"The knowledge content (required)"`
}

type AddKnowledgeResponse struct {
	Entry *KnowledgeEntry `json:"entry" description:"The added entry"`
}

type SearchKnowledgeRequest struct {
	Topic   string `json:"topic,omitempty" description:"Filter by topic (optional)"`
	Keyword string `json:"keyword,omitempty" description:"Search keyword in content (optional)"`
}

type SearchKnowledgeResponse struct {
	Entries []*KnowledgeEntry `json:"entries" description:"Matching entries"`
}

type KnowledgeService struct {
	mu      sync.RWMutex
	entries []*KnowledgeEntry
	nextID  int
}

// Add stores a new knowledge entry.
//
// @example {"topic": "go", "content": "Go interfaces are implicit."}
func (s *KnowledgeService) Add(ctx context.Context, req *AddKnowledgeRequest, rsp *AddKnowledgeResponse) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.nextID++
	e := &KnowledgeEntry{
		ID:      fmt.Sprintf("kb-%d", s.nextID),
		Topic:   req.Topic,
		Content: req.Content,
	}
	s.entries = append(s.entries, e)
	rsp.Entry = e
	return nil
}

// Search finds knowledge entries by topic or keyword.
//
// @example {"topic": "go"}
// @example {"keyword": "interface"}
func (s *KnowledgeService) Search(ctx context.Context, req *SearchKnowledgeRequest, rsp *SearchKnowledgeResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, e := range s.entries {
		if req.Topic != "" && !strings.EqualFold(e.Topic, req.Topic) {
			continue
		}
		if req.Keyword != "" && !strings.Contains(strings.ToLower(e.Content), strings.ToLower(req.Keyword)) {
			continue
		}
		rsp.Entries = append(rsp.Entries, e)
	}
	return nil
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

func main() {
	// Ollama Cloud is the default. Override with env vars for local Ollama.
	baseURL := os.Getenv("OLLAMA_BASE_URL")
	if baseURL == "" {
		baseURL = "https://ollama.com/v1"
	}
	model := os.Getenv("OLLAMA_MODEL")
	if model == "" {
		model = "gpt-oss:120b"
	}
	apiKey := os.Getenv("OLLAMA_API_KEY")

	fmt.Println("╔══════════════════════════════════════════╗")
	fmt.Println("║       Ollama-Powered Go Micro Agent      ║")
	fmt.Println("╚══════════════════════════════════════════╝")
	fmt.Println()
	fmt.Printf("  Ollama URL:  %s\n", baseURL)
	fmt.Printf("  Model:       %s\n", model)
	if apiKey != "" {
		fmt.Printf("  API Key:     (set)\n")
	} else {
		fmt.Printf("  API Key:     (none — set OLLAMA_API_KEY)\n")
	}
	fmt.Println()

	// 1. Start the knowledge service. Its handlers become agent tools.
	svc := micro.NewService("knowledge")
	svc.Handle(new(KnowledgeService))
	go svc.Run()

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

	// 2. Create the agent. It discovers the knowledge service endpoints
	//    as tools automatically, plus gets a custom "current_time" tool.
	ag := micro.NewAgent("ollama-assistant",
		micro.AgentServices("knowledge"),
		micro.AgentPrompt(
			"You are a helpful knowledge assistant. You can search and add to "+
				"a knowledge base using the knowledge service tools. "+
				"When asked about the current time, use the current_time tool. "+
				"Be concise and factual.",
		),
		micro.AgentProvider("ollama"),
		micro.AgentModel(model),
		micro.AgentAPIKey(apiKey),
		micro.AgentBaseURL(baseURL),
		micro.AgentMaxSteps(10),
		micro.AgentLoopLimit(3),
		// Custom tool — any function, not tied to a service.
		agent.WithTool(
			"current_time",
			"Get the current date and time in a human-readable format",
			map[string]any{
				"timezone": map[string]any{
					"type":        "string",
					"description": "Optional timezone (defaults to local)",
				},
			},
			func(ctx context.Context, input map[string]any) (string, error) {
				tz, _ := input["timezone"].(string)
				if tz == "" {
					return time.Now().Format("2006-01-02 15:04:05 MST"), nil
				}
				loc, err := time.LoadLocation(tz)
				if err != nil {
					return "", fmt.Errorf("unknown timezone: %s", tz)
				}
				return time.Now().In(loc).Format("2006-01-02 15:04:05 MST"), nil
			},
		),
	)

	// 3. Seed initial knowledge via the agent's first question.
	questions := []string{
		"What time is it now?",
		"Add a new knowledge entry: topic 'go', content 'Go interfaces are implicit — a type implements an interface by having the required methods.'",
		"Add another entry: topic 'go', content 'Go is a statically typed, compiled language designed at Google.'",
		"Add another entry: topic 'ai', content 'Large language models generate text by predicting the next token in a sequence.'",
		"Search the knowledge base for entries about Go.",
		"Search for everything in the knowledge base.",
	}

	fmt.Println("─── Agent Demo ───")
	fmt.Println()

	for i, q := range questions {
		fmt.Printf("Q%d: %s\n", i+1, q)
		fmt.Print("A: ")

		resp, err := ag.Ask(context.Background(), q)
		if err != nil {
			fmt.Printf("error: %v\n", err)
			fmt.Println()
			continue
		}

		// Show tool calls the agent made.
		if len(resp.ToolCalls) > 0 {
			for _, tc := range resp.ToolCalls {
				args, _ := json.Marshal(tc.Input)
				fmt.Printf("  [tool] %s(%s)\n", tc.Name, string(args))
			}
		}

		fmt.Println(resp.Reply)
		if resp.Reply == "" && len(resp.ToolCalls) == 0 {
			fmt.Println("(no response)")
		}
		fmt.Println()
	}

	// 4. Streaming demonstration.
	fmt.Println("─── Streaming Demo ───")
	fmt.Println()
	streamQ := "Explain what Go Micro is in two sentences."
	fmt.Printf("Q: %s\n", streamQ)
	fmt.Print("A: ")

	stream, err := ag.Stream(context.Background(), streamQ)
	if err != nil {
		fmt.Printf("stream error: %v\n", err)
	} else {
		for {
			chunk, err := stream.Recv()
			if err != nil {
				break
			}
			if chunk.Reply != "" {
				fmt.Print(chunk.Reply)
			}
		}
		fmt.Println()
	}

	fmt.Println()
	fmt.Println("Done.")
}
