// Package flow provides event-driven workflows for go-micro services.
//
// A Flow is a workflow in the sense of Anthropic's "Building Effective
// Agents": LLMs and tools orchestrated through a predefined path. It
// subscribes to a broker topic and, for each event, runs one augmented
// LLM step — the registered services as tools, a fixed prompt — and
// lets the model decide which RPCs to call. Use a Flow when the task is
// well-defined and you want a deterministic trigger; use an Agent (see
// the agent package) when the work needs to direct itself dynamically.
//
// Usage:
//
//	f := flow.New("onboard-user",
//	    flow.Trigger("events.user.created"),
//	    flow.Prompt("New user created: {{.Data}}. Send welcome email and create workspace."),
//	    flow.Provider("anthropic"),
//	    flow.APIKey(key),
//	)
//	f.Register(service)
//	service.Run()
package flow

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"sync"
	"text/template"
	"time"

	"github.com/google/uuid"
	"go-micro.dev/v6/ai"
	"go-micro.dev/v6/broker"
	"go-micro.dev/v6/client"
	codecbytes "go-micro.dev/v6/codec/bytes"
	"go-micro.dev/v6/logger"
	"go-micro.dev/v6/registry"

	// Register default providers.
	_ "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/mistral"
	_ "go-micro.dev/v6/ai/openai"
	_ "go-micro.dev/v6/ai/together"
)

// Flow is an event-driven LLM orchestration unit. It subscribes to
// a broker topic, discovers services as tools, and feeds each event
// into an LLM that decides which RPCs to call.
type Flow struct {
	name         string
	opts         Options
	model        ai.Model
	toolSet      *ai.Tools
	client       client.Client
	tmpl         *template.Template
	log          logger.Logger
	checkpoint   Checkpoint
	reg          registry.Registry
	sub          broker.Subscriber
	registration *registry.Service
	mu           sync.Mutex
	results      []Result
}

// Result records one flow execution.
type Result struct {
	FlowName  string    `json:"flow"`
	Trigger   string    `json:"trigger"`
	Prompt    string    `json:"prompt"`
	Reply     string    `json:"reply,omitempty"`
	Answer    string    `json:"answer,omitempty"`
	ToolCalls []string  `json:"tool_calls,omitempty"`
	Error     string    `json:"error,omitempty"`
	ErrorKind string    `json:"error_kind,omitempty"`
	Timestamp time.Time `json:"timestamp"`
	Duration  float64   `json:"duration_seconds"`
}

// New creates a Flow with the given name and options.
func New(name string, opts ...Option) *Flow {
	o := Options{
		Provider:     "openai",
		SystemPrompt: "You are a service orchestrator. Use the available tools to fulfill the request. Explain what you do.",
		HistoryLimit: 20,
	}
	for _, opt := range opts {
		opt(&o)
	}

	var tmpl *template.Template
	if o.Prompt != "" {
		var err error
		tmpl, err = template.New(name).Parse(o.Prompt)
		if err != nil {
			tmpl = template.Must(template.New(name).Parse("{{.Data}}"))
		}
	}

	return &Flow{
		name:       name,
		opts:       o,
		tmpl:       tmpl,
		log:        logger.DefaultLogger,
		checkpoint: defaultCheckpoint(name, o),
	}
}

// Register wires the flow into a running service. It sets up the
// model, discovers tools from the registry, and subscribes to the
// trigger topic on the broker. Call this before service.Run().
func (f *Flow) Register(reg registry.Registry, br broker.Broker, cl client.Client) error {
	f.client = cl
	f.reg = reg
	f.toolSet = ai.NewTools(reg, ai.ToolClient(cl))

	// A flow that dispatches to an agent doesn't run its own model — the
	// agent is the engine. Otherwise, set up the augmented LLM.
	if f.opts.Agent == "" {
		var modelOpts []ai.Option
		if f.opts.APIKey != "" {
			modelOpts = append(modelOpts, ai.WithAPIKey(f.opts.APIKey))
		}
		if f.opts.Model != "" {
			modelOpts = append(modelOpts, ai.WithModel(f.opts.Model))
		}
		if f.opts.BaseURL != "" {
			modelOpts = append(modelOpts, ai.WithBaseURL(f.opts.BaseURL))
		}
		modelOpts = append(modelOpts, ai.WithTools(f.toolSet))

		f.model = ai.New(f.opts.Provider, modelOpts...)
		if f.model == nil {
			return fmt.Errorf("unknown provider: %s", f.opts.Provider)
		}
	}

	if f.opts.TriggerTopic != "" {
		sub, err := br.Subscribe(f.opts.TriggerTopic, func(p broker.Event) error {
			data := string(p.Message().Body)
			ctx := ai.WithRunInfo(context.Background(), ai.RunInfo{Dispatch: "broker", Trigger: f.opts.TriggerTopic})
			if err := f.Execute(ctx, data); err != nil {
				f.log.Logf(logger.ErrorLevel, "Flow %s failed: %v", f.name, err)
			}
			return nil
		})
		if err != nil {
			return fmt.Errorf("subscribe to %s: %w", f.opts.TriggerTopic, err)
		}
		f.sub = sub
		f.log.Logf(logger.InfoLevel, "Flow %s subscribed to %s", f.name, f.opts.TriggerTopic)

		// Announce the flow in the registry so it's discoverable like a
		// service or agent (e.g. `micro flow list`). This is liveness only:
		// Stop deregisters it. Durable run history lives in the store.
		f.registration = &registry.Service{
			Name:    f.name,
			Version: "latest",
			Metadata: map[string]string{
				"type":    "flow",
				"trigger": f.opts.TriggerTopic,
				"steps":   strconv.Itoa(len(f.opts.Steps)),
			},
			Nodes: []*registry.Node{{
				Id:       f.name + "-" + uuid.New().String()[:8],
				Address:  "flow://" + f.name,
				Metadata: map[string]string{"type": "flow"},
			}},
		}
		if err := reg.Register(f.registration); err != nil {
			f.log.Logf(logger.ErrorLevel, "Flow %s registry register: %v", f.name, err)
			f.registration = nil
		}
	}

	return nil
}

// Stop unsubscribes the flow from its trigger and deregisters it from the
// registry. In-flight and past runs remain in the store; Stop only ends
// the flow's liveness, mirroring how a service leaves the registry when
// it shuts down.
func (f *Flow) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
	if ctx == nil {
		ctx = context.Background()
	}
	if f.opts.Timeout <= 0 {
		return ctx, func() {}
	}
	if _, ok := ctx.Deadline(); ok {
		return ctx, func() {}
	}
	return context.WithTimeout(ctx, f.opts.Timeout)
}

func (f *Flow) Stop() error {
	if f.sub != nil {
		_ = f.sub.Unsubscribe()
		f.sub = nil
	}
	if f.registration != nil && f.reg != nil {
		err := f.reg.Deregister(f.registration)
		f.registration = nil
		return err
	}
	return nil
}

// Execute runs the flow once with the given input data. This is
// called automatically on each broker event, but can also be
// invoked directly for testing or one-shot use.
func (f *Flow) Execute(ctx context.Context, data string) error {
	ctx, cancel := f.withTimeout(ctx)
	defer cancel()

	// Stepped flows run the ordered, checkpointed step loop.
	if len(f.opts.Steps) > 0 {
		_, err := f.startRun(ctx, data)
		return err
	}

	runID := uuid.New().String()
	info, _ := ai.RunInfoFrom(ctx)
	info.RunID = runID
	info.Flow = f.name
	ctx = ai.WithRunInfo(ctx, info)

	start := time.Now()

	prompt := data
	if f.tmpl != nil {
		var buf bytes.Buffer
		_ = f.tmpl.Execute(&buf, map[string]string{"Data": data})
		prompt = buf.String()
	}

	result := Result{
		FlowName:  f.name,
		Trigger:   f.opts.TriggerTopic,
		Prompt:    prompt,
		Timestamp: start,
	}

	// Flow triggers, Agent reasons: hand the event to the named agent.
	if f.opts.Agent != "" {
		reply, err := f.callAgent(ctx, f.opts.Agent, prompt)
		result.Duration = time.Since(start).Seconds()
		if err != nil {
			result.Error = err.Error()
			result.ErrorKind = string(ai.ClassifyError(err))
			f.record(result)
			return err
		}
		result.Reply = reply
		f.record(result)
		f.log.Logf(logger.InfoLevel, "Flow %s dispatched to agent %s in %.1fs",
			f.name, f.opts.Agent, result.Duration)
		return nil
	}

	// Otherwise run a single augmented-LLM step with the services as tools.
	discovered, err := f.toolSet.Discover()
	if err != nil {
		result.Duration = time.Since(start).Seconds()
		result.Error = err.Error()
		result.ErrorKind = string(ai.ClassifyError(err))
		f.record(result)
		return fmt.Errorf("discover tools: %w", err)
	}

	resp, err := f.model.Generate(ctx, &ai.Request{
		Prompt:       prompt,
		SystemPrompt: f.opts.SystemPrompt,
		Tools:        discovered,
	})
	result.Duration = time.Since(start).Seconds()

	if err != nil {
		result.Error = err.Error()
		result.ErrorKind = string(ai.ClassifyError(err))
		f.record(result)
		return err
	}

	result.Reply = resp.Reply
	result.Answer = resp.Answer
	for _, tc := range resp.ToolCalls {
		args, _ := json.Marshal(tc.Input)
		result.ToolCalls = append(result.ToolCalls, fmt.Sprintf("%s(%s)", tc.Name, args))
	}

	f.record(result)

	f.log.Logf(logger.InfoLevel, "Flow %s completed in %.1fs: %d tool calls",
		f.name, result.Duration, len(result.ToolCalls))

	return nil
}

// callAgent hands the rendered prompt to a registered agent's Agent.Chat
// endpoint over RPC and returns its reply.
func (f *Flow) callAgent(ctx context.Context, name, message string) (string, error) {
	info, _ := ai.RunInfoFrom(ctx)
	body, _ := json.Marshal(map[string]string{"message": message, "parent_id": info.RunID})
	req := f.client.NewRequest(name, "Agent.Chat", &codecbytes.Frame{Data: body})
	var rsp codecbytes.Frame
	if err := f.client.Call(ctx, req, &rsp); err != nil {
		return "", err
	}
	var out struct {
		Reply string `json:"reply"`
	}
	if err := json.Unmarshal(rsp.Data, &out); err != nil {
		return "", err
	}
	return out.Reply, nil
}

// Results returns a copy of all recorded execution results.
func (f *Flow) Results() []Result {
	f.mu.Lock()
	defer f.mu.Unlock()
	out := make([]Result, len(f.results))
	copy(out, f.results)
	return out
}

// Name returns the flow name.
func (f *Flow) Name() string {
	return f.name
}

func (f *Flow) record(r Result) {
	f.mu.Lock()
	f.results = append(f.results, r)
	f.mu.Unlock()

	if f.opts.OnResult != nil {
		f.opts.OnResult(r)
	}
}
