package agent

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/mudler/LocalAGI/pkg/xlog"
	"github.com/mudler/cogito"

	"github.com/mudler/LocalAGI/core/action"
	"github.com/mudler/LocalAGI/core/types"
	"github.com/mudler/LocalAGI/pkg/llm"
	"github.com/robfig/cron/v3"
	"github.com/sashabaranov/go-openai"
)

const (
	UserRole      = "user"
	AssistantRole = "assistant"
	SystemRole    = "system"
)

type Agent struct {
	sync.Mutex
	options   *options
	Character Character
	client    *openai.Client
	jobQueue  chan *types.Job
	context   *types.ActionContext

	currentState *types.AgentInternalState

	selfEvaluationInProgress bool
	pause                    bool

	newConversations chan openai.ChatCompletionMessage

	mcpSessions []*mcp.ClientSession
	// only contains the MCP action definitions for observables
	mcpActionDefinitions types.Actions

	subscriberMutex        sync.Mutex
	newMessagesSubscribers []func(openai.ChatCompletionMessage)

	observer Observer

	llm         cogito.LLM
	sharedState *types.AgentSharedState
}

type RAGDB interface {
	Store(s string) error
	Reset() error
	Search(s string, similarEntries int) ([]string, error)
	Count() int
}

func New(opts ...Option) (*Agent, error) {
	options, err := newOptions(opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to set options: %v", err)
	}

	client := llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
	llmClient := cogito.NewOpenAILLM(options.LLMAPI.Model, options.LLMAPI.APIKey, options.LLMAPI.APIURL)
	c := context.Background()
	if options.context != nil {
		c = options.context
	}

	ctx, cancel := context.WithCancel(c)
	a := &Agent{
		jobQueue:               make(chan *types.Job),
		options:                options,
		client:                 client,
		Character:              options.character,
		currentState:           &types.AgentInternalState{},
		llm:                    llmClient,
		context:                types.NewActionContext(ctx, cancel),
		newConversations:       make(chan openai.ChatCompletionMessage),
		newMessagesSubscribers: options.newConversationsSubscribers,
		sharedState:            types.NewAgentSharedState(options.lastMessageDuration),
	}

	// Initialize observer if provided
	if options.observer != nil {
		a.observer = options.observer
	}

	if a.options.statefile != "" {
		if _, err := os.Stat(a.options.statefile); err == nil {
			if err = a.LoadState(a.options.statefile); err != nil {
				return a, fmt.Errorf("failed to load state: %v", err)
			}
		}
	}

	// var programLevel = new(xlog.LevelVar) // Info by default
	// h := xlog.NewTextHandler(os.Stdout, &xlog.HandlerOptions{Level: programLevel})
	// xlog = xlog.New(h)
	//programLevel.Set(a.options.logLevel)

	if err := a.prepareIdentity(); err != nil {
		return nil, fmt.Errorf("failed to prepare identity: %v", err)
	}

	xlog.Info("Populating actions from MCP Servers (if any)")
	a.initMCPActions()
	xlog.Info("Done populating actions from MCP Servers")

	xlog.Info(
		"Agent created",
		"agent", a.Character.Name,
		"character", a.Character.String(),
		"state", a.State().String(),
		"goal", a.options.permanentGoal,
		"model", a.options.LLMAPI.Model,
	)

	return a, nil
}

func (a *Agent) SharedState() *types.AgentSharedState {
	return a.sharedState
}

func (a *Agent) startNewConversationsConsumer() {
	go func() {
		for {
			select {
			case <-a.context.Done():
				return

			case msg := <-a.newConversations:
				xlog.Debug("New conversation", "agent", a.Character.Name, "message", msg.Content)
				a.subscriberMutex.Lock()
				subs := a.newMessagesSubscribers
				a.subscriberMutex.Unlock()
				for _, s := range subs {
					s(msg)
				}
			}
		}
	}()
}

func (a *Agent) AddSubscriber(f func(openai.ChatCompletionMessage)) {
	a.subscriberMutex.Lock()
	defer a.subscriberMutex.Unlock()
	a.newMessagesSubscribers = append(a.newMessagesSubscribers, f)
}

func (a *Agent) Context() context.Context {
	return a.context.Context
}

// Ask is a blocking call that returns the response as soon as it's ready.
// It discards any other computation.
func (a *Agent) Ask(opts ...types.JobOption) *types.JobResult {
	xlog.Debug("Agent Ask()", "agent", a.Character.Name, "model", a.options.LLMAPI.Model)
	defer func() {
		xlog.Debug("Agent has finished being asked", "agent", a.Character.Name)
	}()

	if a.observer != nil {
		obs := a.observer.NewObservable()
		obs.Name = "job"
		obs.Icon = "plug"
		a.observer.Update(*obs)
		opts = append(opts, types.WithObservable(obs))
	}

	return a.Execute(types.NewJob(
		append(
			opts,
			types.WithReasoningCallback(a.options.reasoningCallback),
			types.WithResultCallback(a.options.resultCallback),
		)...,
	))
}

// Ask is a pre-emptive, blocking call that returns the response as soon as it's ready.
// It discards any other computation.
func (a *Agent) Execute(j *types.Job) *types.JobResult {
	xlog.Debug("Agent Execute()", "agent", a.Character.Name, "model", a.options.LLMAPI.Model)
	defer func() {
		xlog.Debug("Agent has finished", "agent", a.Character.Name)
	}()

	if j.Obs != nil {
		if len(j.ConversationHistory) > 0 {
			m := j.ConversationHistory[len(j.ConversationHistory)-1]
			j.Obs.Creation = &types.Creation{ChatCompletionMessage: &m}
			a.observer.Update(*j.Obs)
		}

		j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
			j.Obs.Completion = &types.Completion{
				Conversation: ccm,
			}

			if j.Result.Error != nil {
				j.Obs.Completion.Error = j.Result.Error.Error()
			}

			a.observer.Update(*j.Obs)
		})
	}

	a.Enqueue(j)
	return j.Result.WaitResult()
}

func (a *Agent) Enqueue(j *types.Job) {
	j.ReasoningCallback = a.options.reasoningCallback
	j.ResultCallback = a.options.resultCallback

	a.jobQueue <- j
}

func (a *Agent) Transcribe(ctx context.Context, file string) (string, error) {
	resp, err := a.client.CreateTranscription(ctx,
		openai.AudioRequest{
			Model:    a.options.LLMAPI.TranscriptionModel,
			Language: a.options.LLMAPI.TranscriptionLanguage,
			FilePath: file,
		},
	)
	if err != nil {
		return "", err
	}
	return resp.Text, nil
}

func (a *Agent) TTS(ctx context.Context, text string) ([]byte, error) {
	if a.options.LLMAPI.TTSModel == "" {
		return nil, fmt.Errorf("TTS model is not set")
	}
	resp, err := a.client.CreateSpeech(ctx,
		openai.CreateSpeechRequest{
			Model:          openai.SpeechModel(a.options.LLMAPI.TTSModel),
			Input:          text,
			ResponseFormat: openai.SpeechResponseFormatMp3,
		},
	)
	if err != nil {
		return nil, err
	}
	defer resp.Close()

	buf := bytes.NewBuffer(nil)
	io.Copy(buf, resp)

	return buf.Bytes(), nil
}

var ErrContextCanceled = fmt.Errorf("context canceled")

func (a *Agent) Stop() {
	a.Lock()
	defer a.Unlock()
	xlog.Debug("Stopping agent", "agent", a.Character.Name)
	a.closeMCPSTDIOServers()
	a.context.Cancel()
}

func (a *Agent) Pause() {
	a.Lock()
	defer a.Unlock()
	a.pause = true

}

func (a *Agent) Resume() {
	a.Lock()
	defer a.Unlock()
	a.pause = false
}

func (a *Agent) Paused() bool {
	a.Lock()
	defer a.Unlock()
	return a.pause
}

func (a *Agent) Memory() RAGDB {
	return a.options.ragdb
}

func (a *Agent) processPrompts(ctx context.Context, conversation Messages) Messages {
	// Add custom prompts
	for _, prompt := range a.options.prompts {
		message, err := prompt.Render(a)
		if err != nil {
			xlog.Error("Error rendering prompt", "error", err)
			continue
		}
		if message.Content == "" && message.ImageBase64 == "" {
			xlog.Debug("Prompt is empty, skipping", "agent", a.Character.Name)
			continue
		}

		content := message.Content

		if strings.Contains(content, "{{") {
			promptTemplate, err := templateBase("template", content)
			if err != nil {
				xlog.Error("Error rendering template", "error", err)
			}

			content, err = templateExecute(promptTemplate, struct{}{})
			if err != nil {
				xlog.Error("Error executing template", "error", err)
				content = message.Content
			}
		}

		if message.ImageBase64 != "" {
			// iF model support both images and text, process it as a single multicontent message and return
			if !a.options.SeparatedMultimodalModel() {
				conversation = append([]openai.ChatCompletionMessage{
					{
						Role: prompt.Role(),
						MultiContent: []openai.ChatMessagePart{
							{
								Type: openai.ChatMessagePartTypeText,
								Text: content,
							},
							{
								Type: openai.ChatMessagePartTypeImageURL,
								ImageURL: &openai.ChatMessageImageURL{
									URL: message.ImageBase64,
								},
							},
						},
					}}, conversation...)

			} else {
				// We need to describe the image first, and we will process the text separately (we do not return here)
				imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, message.ImageBase64)
				if err != nil {
					xlog.Error("Error describing image", "error", err)
				} else {
					conversation = append([]openai.ChatCompletionMessage{
						{
							Role:    prompt.Role(),
							Content: fmt.Sprintf("%s\n\nImage description: %s", content, imageDescription),
						}}, conversation...)
				}
			}
		} else {
			conversation = append([]openai.ChatCompletionMessage{
				{
					Role:    prompt.Role(),
					Content: content,
				}}, conversation...)
		}
	}

	// TODO: move to a Promptblock?
	if a.options.systemPrompt != "" {
		content := a.options.systemPrompt

		if strings.Contains(content, "{{") {
			promptTemplate, err := templateBase("template", a.options.systemPrompt)
			if err != nil {
				xlog.Error("Error rendering template", "error", err)
			}

			content, err = templateExecute(promptTemplate, struct{}{})
			if err != nil {
				xlog.Error("Error executing template", "error", err)
				content = a.options.systemPrompt
			}
		}

		if !conversation.Exist(content) {
			conversation = append([]openai.ChatCompletionMessage{
				{
					Role:    "system",
					Content: content,
				}}, conversation...)
		}
	}

	return conversation
}

func (a *Agent) describeImage(ctx context.Context, model, imageURL string) (string, error) {
	xlog.Debug("Describing image", "model", model)
	resp, err := a.client.CreateChatCompletion(ctx,
		openai.ChatCompletionRequest{
			Model: model,
			Messages: []openai.ChatCompletionMessage{
				{

					Role: "user",
					MultiContent: []openai.ChatMessagePart{
						{
							Type: openai.ChatMessagePartTypeText,
							Text: "What is in the image?",
						},
						{
							Type: openai.ChatMessagePartTypeImageURL,
							ImageURL: &openai.ChatMessageImageURL{

								URL: imageURL,
							},
						},
					},
				},
			}})
	if err != nil {
		return "", err
	}
	if len(resp.Choices) == 0 {
		return "", fmt.Errorf("no choices")
	}

	xlog.Debug("Described image", "description", resp.Choices[0].Message.Content)
	return resp.Choices[0].Message.Content, nil
}

// extractAllImageContent extracts all images from a message
func extractAllImageContent(message openai.ChatCompletionMessage) (images []string, text string, e error) {
	e = fmt.Errorf("no image found")
	if message.MultiContent != nil {
		for _, content := range message.MultiContent {
			if content.Type == openai.ChatMessagePartTypeImageURL {
				images = append(images, content.ImageURL.URL)
				e = nil
			}
			if content.Type == openai.ChatMessagePartTypeText {
				text = content.Text
				e = nil
			}
		}
	}
	return
}

func (a *Agent) processUserInputs(conv Messages) Messages {

	// walk conversation history, and check if any message contains images.
	// If they do, we need to describe the images first with a model that supports image understanding (if the current model doesn't support it)
	// and add them to the conversation context
	if !a.options.SeparatedMultimodalModel() {
		return conv
	}

	xlog.Debug("Processing user inputs", "agent", a.Character.Name, "conversation", conv)

	// Process all messages in the conversation to extract and describe images
	var processedMessages Messages
	var messagesToRemove []int

	for i, message := range conv {
		images, text, err := extractAllImageContent(message)
		if err == nil && len(images) > 0 {
			xlog.Debug("Found images in message", "messageIndex", i, "imageCount", len(images), "role", message.Role)

			// Mark this message for removal
			messagesToRemove = append(messagesToRemove, i)

			// Process each image in the message
			var imageDescriptions []string
			for j, image := range images {
				imageDescription, err := a.describeImage(a.context.Context, a.options.LLMAPI.MultimodalModel, image)
				if err != nil {
					xlog.Error("Error describing image", "error", err, "messageIndex", i, "imageIndex", j)
					imageDescriptions = append(imageDescriptions, fmt.Sprintf("Image %d: [Error describing image: %v]", j+1, err))
				} else {
					imageDescriptions = append(imageDescriptions, fmt.Sprintf("Image %d: %s", j+1, imageDescription))
				}
			}

			// Add the text content as a new message with the same role first
			if text != "" {
				textMessage := openai.ChatCompletionMessage{
					Role:    message.Role,
					Content: text,
				}
				processedMessages = append(processedMessages, textMessage)

				// Add the image descriptions as a system message after the text
				explainerMessage := openai.ChatCompletionMessage{
					Role: "system",
					Content: fmt.Sprintf("The above message also contains %d image(s) which can be described as: %s",
						len(images), strings.Join(imageDescriptions, "; ")),
				}
				processedMessages = append(processedMessages, explainerMessage)
			} else {
				// If there's no text, just add the image descriptions as a system message
				explainerMessage := openai.ChatCompletionMessage{
					Role: "system",
					Content: fmt.Sprintf("Message contains %d image(s) which can be described as: %s",
						len(images), strings.Join(imageDescriptions, "; ")),
				}
				processedMessages = append(processedMessages, explainerMessage)
			}
		} else {
			// No image found, keep the original message
			processedMessages = append(processedMessages, message)
		}
	}

	// If we found and processed any images, replace the conversation
	if len(messagesToRemove) > 0 {
		xlog.Info("Processed images in conversation", "messagesWithImages", len(messagesToRemove), "agent", a.Character.Name)
		return processedMessages
	}

	return conv
}

func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
	hasTriggers := false
	triggeredBy := ""
	failedBy := ""

	if job.DoneFilter {
		return true, nil
	}
	job.DoneFilter = true

	if len(a.options.jobFilters) < 1 {
		xlog.Debug("No filters")
		return true, nil
	}

	for _, filter := range a.options.jobFilters {
		name := filter.Name()
		if triggeredBy != "" && filter.IsTrigger() {
			continue
		}

		ok, err = filter.Apply(job)
		if err != nil {
			xlog.Error("Error in job filter", "filter", name, "error", err)
			failedBy = name
			break
		}

		if filter.IsTrigger() {
			hasTriggers = true
			if ok {
				triggeredBy = name
				xlog.Info("Job triggered by filter", "filter", name)
			}
		} else if !ok {
			failedBy = name
			xlog.Info("Job failed filter", "filter", name)
			break
		} else {
			xlog.Debug("Job passed filter", "filter", name)
		}
	}

	if a.Observer() != nil {
		obs := a.Observer().NewObservable()
		obs.Name = "filter"
		obs.Icon = "shield"
		obs.ParentID = job.Obs.ID
		if err == nil {
			obs.Completion = &types.Completion{
				FilterResult: &types.FilterResult{
					HasTriggers: hasTriggers,
					TriggeredBy: triggeredBy,
					FailedBy:    failedBy,
				},
			}
		} else {
			obs.Completion = &types.Completion{
				Error: err.Error(),
			}
		}
		a.Observer().Update(*obs)
	}

	return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
}

// replyWithToolCall handles user-defined actions by recording the action state without setting Response
func (a *Agent) replyWithToolCall(job *types.Job, conv []openai.ChatCompletionMessage, params types.ActionParams, chosenAction types.Action, reasoning string) {
	// Record the action state so the webui can detect this is a user-defined action
	stateResult := types.ActionState{
		ActionCurrentState: types.ActionCurrentState{
			Job:       job,
			Action:    chosenAction,
			Params:    params,
			Reasoning: reasoning,
		},
		ActionResult: types.ActionResult{
			Result: reasoning, // The reasoning/message to show to user
		},
	}

	// Add the action state to the job result
	job.Result.SetResult(stateResult)

	// Used by the observer
	conv = append(conv, openai.ChatCompletionMessage{
		Role: "assistant",
		ToolCalls: []openai.ToolCall{
			{
				Type: openai.ToolTypeFunction,
				Function: openai.FunctionCall{
					Name:      chosenAction.Definition().ToFunctionDefinition().Name,
					Arguments: params.String(),
				},
			},
		},
	})

	// Set conversation but leave Response empty
	// The webui will detect the user-defined action and generate the proper tool call response
	job.Result.Conversation = conv
	// job.Result.Response remains empty - this signals to webui that it should check State
	job.Result.Finish(nil)
}

// validateBuiltinTools checks that builtin tools specified by the user can be matched to available actions
func (a *Agent) validateBuiltinTools(job *types.Job) {
	builtinTools := job.GetBuiltinTools()
	if len(builtinTools) == 0 {
		return
	}

	// Get available actions
	availableActions := a.availableActions()

	for _, tool := range builtinTools {
		functionName := tool.Name

		// Check if this is a web search builtin tool
		if strings.HasPrefix(string(functionName), "web_search_") {
			// Look for a search action
			searchAction := availableActions.Find("search")
			if searchAction == nil {
				xlog.Warn("Web search builtin tool specified but no 'search' action available",
					"function_name", functionName,
					"agent", a.Character.Name)
			} else {
				xlog.Debug("Web search builtin tool matched to search action",
					"function_name", functionName,
					"agent", a.Character.Name)
			}
		} else {
			// For future builtin tools, add more matching logic here
			xlog.Warn("Unknown builtin tool specified",
				"function_name", functionName,
				"agent", a.Character.Name)
		}
	}
}

func (a *Agent) addFunctionResultToConversation(ctx context.Context, chosenAction types.Action, actionParams types.ActionParams, result types.ActionResult, conv Messages) Messages {
	// calling the function
	conv = append(conv, openai.ChatCompletionMessage{
		Role: "assistant",
		ToolCalls: []openai.ToolCall{
			{
				Type: openai.ToolTypeFunction,
				Function: openai.FunctionCall{
					Name:      chosenAction.Definition().Name.String(),
					Arguments: actionParams.String(),
				},
			},
		},
	})

	// result of calling the function

	// If it contains an image, we need to put it in the conversation (if supported by the model)
	if result.ImageBase64Result != "" {
		// iF model support both images and text, process it as a single multicontent message and return
		if !a.options.SeparatedMultimodalModel() {
			conv = append(conv, openai.ChatCompletionMessage{
				Role: openai.ChatMessageRoleTool,
				MultiContent: []openai.ChatMessagePart{
					{
						Type: openai.ChatMessagePartTypeText,
						Text: result.Result,
					},
					{
						Type: openai.ChatMessagePartTypeImageURL,
						ImageURL: &openai.ChatMessageImageURL{
							URL: result.ImageBase64Result,
						},
					},
				},
				Name:       chosenAction.Definition().Name.String(),
				ToolCallID: chosenAction.Definition().Name.String(),
			})

			return conv
		} else {
			// We need to describe the image first, and we will process the text separately (we do not return here)
			imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, result.ImageBase64Result)
			if err != nil {
				xlog.Error("Error describing image", "error", err)
			} else {
				conv = append(conv, openai.ChatCompletionMessage{
					Role:       openai.ChatMessageRoleTool,
					Content:    fmt.Sprintf("Tool generated an image, the description of the image is: %s", imageDescription),
					Name:       chosenAction.Definition().Name.String(),
					ToolCallID: chosenAction.Definition().Name.String(),
				})
				if result.Result != "" {
					conv = append(conv, openai.ChatCompletionMessage{
						Role:       openai.ChatMessageRoleTool,
						Content:    result.Result,
						Name:       chosenAction.Definition().Name.String(),
						ToolCallID: chosenAction.Definition().Name.String(),
					})
				}
			}
		}
	} else {
		conv = append(conv, openai.ChatCompletionMessage{
			Role:       openai.ChatMessageRoleTool,
			Content:    result.Result,
			Name:       chosenAction.Definition().Name.String(),
			ToolCallID: chosenAction.Definition().Name.String(),
		})
	}

	return conv
}

func (a *Agent) consumeJob(job *types.Job, role string) {
	if err := job.GetContext().Err(); err != nil {
		job.Result.Finish(fmt.Errorf("expired"))
		return
	}

	a.Lock()
	paused := a.pause
	a.Unlock()

	if paused {
		xlog.Info("Agent is paused, skipping job", "agent", a.Character.Name)
		job.Result.Finish(fmt.Errorf("agent is paused"))
		return
	}

	// We are self evaluating if we consume the job as a system role
	selfEvaluation := role == SystemRole

	conv := job.ConversationHistory

	a.Lock()
	a.selfEvaluationInProgress = selfEvaluation
	a.Unlock()
	defer job.Cancel()

	if selfEvaluation {
		defer func() {
			a.Lock()
			a.selfEvaluationInProgress = false
			a.Unlock()
		}()
	}

	conv = a.processPrompts(job.GetContext(), conv)
	if ok, err := a.filterJob(job); !ok || err != nil {
		if err != nil {
			job.Result.Finish(fmt.Errorf("Error in job filter: %w", err))
		} else {
			job.Result.Finish(nil)
		}
		return
	}
	conv = a.processUserInputs(conv)

	// RAG
	conv = a.knowledgeBaseLookup(job, conv)

	// Validate builtin tools against available actions
	a.validateBuiltinTools(job)

	fragment := cogito.NewFragment(conv...)

	if selfEvaluation {
		fragment = fragment.AddStartMessage("system", pickSelfTemplate)
	}

	if a.options.enableHUD {
		prompt, err := renderTemplate(hudTemplate, a.prepareHUD(), a.availableActions(), "")
		if err != nil {
			job.Result.Finish(fmt.Errorf("error renderTemplate: %w", err))
			return
		}
		fragment = fragment.AddStartMessage("system", prompt)
	}

	availableActions := a.getAvailableActionsForJob(job)
	cogitoTools := availableActions.ToCogitoTools(job.GetContext(), a.sharedState)
	allActions := append(availableActions, a.mcpActionDefinitions...)

	obs := job.Obs
	if obs == nil && a.observer != nil && job.Obs != nil {
		obs = a.observer.NewObservable()
		obs.Name = "decision"
		obs.Icon = "brain"
		obs.ParentID = job.Obs.ID
		obs.Creation = &types.Creation{
			ChatCompletionRequest: &openai.ChatCompletionRequest{
				Model:    a.options.LLMAPI.Model,
				Messages: conv,
			},
		}
	}

	defer func() {
		if obs != nil && a.observer != nil {
			obs.MakeLastProgressCompletion()
			a.observer.Update(*obs)
		}
	}()

	var err error
	var userTool bool

	var observables = make(map[string]*types.Observable)

	cogitoOpts := []cogito.Option{
		cogito.WithMCPs(a.mcpSessions...),
		cogito.WithReasoningCallback(func(s string) {
			xlog.Debug("Cogito reasoning callback", "status", s)

			if a.observer != nil && job.Obs != nil {
				job.Obs.AddProgress(
					types.Progress{
						ChatCompletionResponse: &openai.ChatCompletionResponse{
							Choices: []openai.ChatCompletionChoice{
								{
									Message: openai.ChatCompletionMessage{
										Role:    "assistant",
										Content: s,
									},
								},
							},
						},
					})
				a.observer.Update(*job.Obs)
			}
		}),
		cogito.WithTools(
			cogitoTools...,
		),
		cogito.WithToolCallResultCallback(func(t cogito.ToolStatus) {
			if a.observer != nil && obs != nil {
				obs := observables[t.ToolArguments.ID]
				obs.Progress = append(obs.Progress, types.Progress{
					ActionResult: t.Result,
				})
				obs.Name = "action"
				obs.Icon = "bolt"
				obs.MakeLastProgressCompletion()
				a.observer.Update(*obs)
			}

			aa := allActions.Find(t.Name)
			state := types.ActionState{
				ActionCurrentState: types.ActionCurrentState{
					Job:       job,
					Action:    aa,
					Params:    types.ActionParams(t.ToolArguments.Arguments),
					Reasoning: t.ToolArguments.Reasoning,
				},
				ActionResult: types.ActionResult{Result: t.Result},
			}
			job.Result.SetResult(state)
			job.CallbackWithResult(state)
			conv = a.addFunctionResultToConversation(job.GetContext(), aa, types.ActionParams(t.ToolArguments.Arguments), types.ActionResult{Result: t.Result}, conv)
		}),
		cogito.WithToolCallBack(
			func(tc *cogito.ToolChoice) bool {

				xlog.Debug("Tool call back", "tool_call", tc)

				// Check if this is a user-defined action
				chosenAction := allActions.Find(tc.Name)

				xlog.Debug("Action found", "action", chosenAction)

				if chosenAction != nil && types.IsActionUserDefined(chosenAction) {
					xlog.Debug("User-defined action chosen, returning tool call", "action", chosenAction.Definition().Name)
					a.replyWithToolCall(job, conv, tc.Arguments, chosenAction, tc.Reasoning)
					userTool = true
					return false
				}

				if a.observer != nil && job.Obs != nil {
					obs := a.observer.NewObservable()
					obs.Name = "decision"
					obs.ParentID = job.Obs.ID
					obs.Icon = "brain"
					obs.Creation = &types.Creation{
						ChatCompletionRequest: &openai.ChatCompletionRequest{
							Model:    a.options.LLMAPI.Model,
							Messages: conv,
						},
						FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(),
						FunctionParams:     types.ActionParams(tc.Arguments),
					}

					a.observer.Update(*obs)
					observables[tc.ID] = obs
				}

				switch tc.Name {
				case action.StopActionName:
					return false
				case action.ConversationActionName:
					message := action.ConversationActionResponse{}
					toolArgs, _ := json.Marshal(tc.Arguments)
					if err := json.Unmarshal([]byte(toolArgs), &message); err != nil {
						xlog.Error("Error unmarshalling conversation response", "error", err)
						job.Result.Finish(fmt.Errorf("error unmarshalling conversation response: %w", err))
						return false
					}

					msg := openai.ChatCompletionMessage{
						Role:    "assistant",
						Content: message.Message,
					}

					go func(agent *Agent) {
						xlog.Info("Sending new conversation to channel", "agent", agent.Character.Name, "message", msg.Content)
						agent.newConversations <- msg
					}(a)

					job.Result.Conversation = []openai.ChatCompletionMessage{
						msg,
					}
					job.Result.SetResponse("decided to initiate a new conversation")
					job.Result.Finish(nil)
					return true
				case action.StateActionName:
					// We need to store the result in the state
					state := types.AgentInternalState{}
					dat, _ := json.Marshal(tc.Arguments)
					err = json.Unmarshal(dat, &state)
					if err != nil {
						werr := fmt.Errorf("error unmarshalling state of the agent: %w", err)
						if obs != nil && a.observer != nil {
							obs.Completion = &types.Completion{
								Error: werr.Error(),
							}
							a.observer.Update(*obs)
						}
						return false
					}
					// update the current state with the one we just got from the action
					a.currentState = &state
					if obs != nil && a.observer != nil {
						obs.Progress = append(obs.Progress, types.Progress{
							AgentState: &state,
						})
						a.observer.Update(*obs)
					}

					// update the state file
					if a.options.statefile != "" {
						if err := a.SaveState(a.options.statefile); err != nil {
							if obs != nil && a.observer != nil {
								obs.Completion = &types.Completion{
									Error: err.Error(),
								}
								a.observer.Update(*obs)
							}

							return false
						}
					}

				}

				cont := job.Callback(types.ActionCurrentState{
					Job:       job,
					Action:    chosenAction,
					Params:    types.ActionParams(tc.Arguments),
					Reasoning: tc.Reasoning})

				if !cont {
					job.Result.SetResult(
						types.ActionState{
							ActionCurrentState: types.ActionCurrentState{
								Job:       job,
								Action:    chosenAction,
								Params:    types.ActionParams(tc.Arguments),
								Reasoning: tc.Reasoning,
							},
							ActionResult: types.ActionResult{Result: "stopped by callback"},
						})

					job.Result.Conversation = conv
					job.Result.Finish(nil)

				}
				return cont
			},
		),
	}

	if a.options.canPlan {
		cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlan)
		if a.options.enableEvaluation {
			cogitoOpts = append(cogitoOpts, cogito.EnableAutoPlanReEvaluator)
		}
	}

	if a.options.forceReasoning {
		cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning())
	}

	if a.options.maxEvaluationLoops > 0 {
		cogitoOpts = append(cogitoOpts,
			cogito.WithMaxAttempts(a.options.maxEvaluationLoops),
			cogito.WithIterations(a.options.maxEvaluationLoops),
		)
	}

	fragment, err = cogito.ExecuteTools(
		a.llm, fragment,
		cogitoOpts...,
	)

	if err != nil && !errors.Is(err, cogito.ErrNoToolSelected) && !errors.Is(err, cogito.ErrGoalNotAchieved) && !userTool {
		if obs != nil {
			obs.Completion = &types.Completion{
				Error: err.Error(),
			}
			a.observer.Update(*obs)
		}
		xlog.Error("Error executing cogito", "error", err)
		job.Result.Finish(err)
		return
	}

	if userTool {
		return
	}

	if len(fragment.Messages) > 0 &&
		fragment.LastMessage().Role == "tool" {
		toolToCall := fragment.Messages[len(fragment.Messages)-2].ToolCalls[0].Function.Name
		switch toolToCall {
		case action.StopActionName:
			job.Result.Finish(nil)
			return
		}
	}

	if len(fragment.Messages) == 0 {
		job.Result.Finish(fmt.Errorf("no messages in fragment"))
		return
	}

	responseFragment, err := a.llm.Ask(job.GetContext(), fragment)
	if err != nil {
		job.Result.Finish(err)
		return
	}

	result := a.cleanupLLMResponse(responseFragment.LastMessage().Content)

	conv = append(fragment.Messages, openai.ChatCompletionMessage{
		Role:    "assistant",
		Content: result,
	})

	job.Result.Plans = fragment.Status.Plans
	job.Result.Conversation = conv
	job.ConversationHistory = conv
	job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
		a.saveCurrentConversation(conv)
	})
	job.Result.SetResponse(result)
	job.Result.Finish(nil)
}

func stripThinkingTags(content string) string {
	// Remove content between <thinking> and </thinking> (including multi-line)
	content = regexp.MustCompile(`(?s)<thinking>.*?</thinking>`).ReplaceAllString(content, "")
	// Remove content between <think> and </think> (including multi-line)
	content = regexp.MustCompile(`(?s)<think>.*?</think>`).ReplaceAllString(content, "")
	// Clean up any extra whitespace
	content = strings.TrimSpace(content)
	return content
}

func (a *Agent) cleanupLLMResponse(content string) string {
	if a.options.stripThinkingTags {
		content = stripThinkingTags(content)
	}
	// Future post-processing options can be added here
	return content
}

// This is running in the background.
func (a *Agent) periodicallyRun(timer *time.Timer) {
	// Remember always to reset the timer - if we don't the agent will stop..
	defer timer.Reset(a.options.periodicRuns)

	xlog.Debug("Agent is running periodically", "agent", a.Character.Name)

	// Check for reminders that need to be triggered
	now := time.Now()
	var triggeredReminders []types.ReminderActionResponse
	var remainingReminders []types.ReminderActionResponse

	for _, reminder := range a.sharedState.Reminders {
		xlog.Debug("Checking reminder", "reminder", reminder)
		if now.After(reminder.NextRun) {
			triggeredReminders = append(triggeredReminders, reminder)
			xlog.Debug("Reminder triggered", "reminder", reminder)
			// Calculate next run time for recurring reminders
			if reminder.IsRecurring {
				xlog.Debug("Reminder is recurring", "reminder", reminder)
				parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
				schedule, err := parser.Parse(reminder.CronExpr)
				if err == nil {
					nextRun := schedule.Next(now)
					xlog.Debug("Next run time", "reminder", reminder, "nextRun", nextRun)
					reminder.LastRun = now
					reminder.NextRun = nextRun
					remainingReminders = append(remainingReminders, reminder)
				}
			}
		} else {
			xlog.Debug("Reminder not triggered", "reminder", reminder)
			remainingReminders = append(remainingReminders, reminder)
		}
	}

	// Update the reminders list
	a.sharedState.Reminders = remainingReminders

	// Handle triggered reminders
	for _, reminder := range triggeredReminders {
		xlog.Info("Processing triggered reminder", "agent", a.Character.Name, "message", reminder.Message)

		// Create a more natural conversation flow for the reminder
		reminderJob := types.NewJob(
			types.WithText(fmt.Sprintf("I have a reminder for you: %s", reminder.Message)),
			types.WithReasoningCallback(a.options.reasoningCallback),
			types.WithResultCallback(a.options.resultCallback),
		)

		// Add the reminder message to the job's metadata
		reminderJob.Metadata = map[string]interface{}{
			"message":     reminder.Message,
			"is_reminder": true,
		}

		// Process the reminder as a normal conversation
		a.consumeJob(reminderJob, UserRole)

		// After the reminder job is complete, ensure the user is notified
		if reminderJob.Result != nil && reminderJob.Result.Conversation != nil {
			// Get the last assistant message from the conversation
			var lastAssistantMsg *openai.ChatCompletionMessage
			for i := len(reminderJob.Result.Conversation) - 1; i >= 0; i-- {
				if reminderJob.Result.Conversation[i].Role == AssistantRole {
					lastAssistantMsg = &reminderJob.Result.Conversation[i]
					break
				}
			}

			if lastAssistantMsg != nil && lastAssistantMsg.Content != "" {
				// Send the reminder response to the user
				msg := openai.ChatCompletionMessage{
					Role:    "assistant",
					Content: fmt.Sprintf("Reminder Update: %s\n\n%s", reminder.Message, lastAssistantMsg.Content),
				}

				go func(agent *Agent) {
					xlog.Info("Sending reminder response to user", "agent", agent.Character.Name, "message", msg.Content)
					agent.newConversations <- msg
				}(a)
			}
		}
	}

	if !a.options.standaloneJob {
		return
	}
	xlog.Info("Periodically running", "agent", a.Character.Name)

	// Here we go in a loop of
	// - asking the agent to do something
	// - evaluating the result
	// - asking the agent to do something else based on the result

	whatNext := types.NewJob(
		types.WithText(innerMonologueTemplate),
		types.WithReasoningCallback(a.options.reasoningCallback),
		types.WithResultCallback(a.options.resultCallback),
	)
	a.consumeJob(whatNext, SystemRole)

	xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
}

func (a *Agent) Run() error {
	a.startNewConversationsConsumer()
	xlog.Debug("Agent is now running", "agent", a.Character.Name)
	// The agent run does two things:
	// picks up requests from a queue
	// and generates a response/perform actions

	// It is also preemptive.
	// That is, it can interrupt the current action
	// if another one comes in.

	// If there is no action, periodically evaluate if it has to do something on its own.

	// Expose a REST API to interact with the agent to ask it things

	timer := time.NewTimer(a.options.periodicRuns)

	// we fire the periodicalRunner only once.
	go a.periodicalRunRunner(timer)
	var errs []error
	var muErr sync.Mutex
	var wg sync.WaitGroup

	parallelJobs := a.options.parallelJobs
	if a.options.parallelJobs == 0 {
		parallelJobs = 1
	}

	for i := 0; i < parallelJobs; i++ {
		xlog.Debug("Starting agent worker", "worker", i)
		wg.Add(1)
		go func() {
			e := a.run(timer)
			muErr.Lock()
			errs = append(errs, e)
			muErr.Unlock()
			wg.Done()
		}()
	}

	wg.Wait()

	return errors.Join(errs...)
}

func (a *Agent) run(timer *time.Timer) error {
	for {
		xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name)
		select {
		case job := <-a.jobQueue:
			if !timer.Stop() {
				<-timer.C
			}
			xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
			a.consumeJob(job, UserRole)
			timer.Reset(a.options.periodicRuns)
		case <-a.context.Done():
			// Agent has been canceled, return error
			xlog.Warn("Agent has been canceled", "agent", a.Character.Name)
			return ErrContextCanceled
		}
	}
}

func (a *Agent) periodicalRunRunner(timer *time.Timer) {
	for {
		select {
		case <-a.context.Done():
			// Agent has been canceled, return error
			xlog.Warn("periodicalRunner has been canceled", "agent", a.Character.Name)
			return
		case <-timer.C:
			a.periodicallyRun(timer)
		}
	}
}

func (a *Agent) Observer() Observer {
	return a.observer
}
