package assistant

import (
	"context"
	"fmt"

	"github.com/fatih/color"
	"github.com/google/uuid"
	"github.com/yaoapp/gou/runtime/v8/bridge"
	"github.com/yaoapp/kun/log"
	chatctx "github.com/yaoapp/yao/neo/context"
	chatMessage "github.com/yaoapp/yao/neo/message"
	"rogchap.com/v8go"
)

// objectCall is the object for the call function
type objectCall struct{}

// OptionsCall is the options for the call function
type OptionsCall struct {
	Retry   OptionsCallRetry       `json:"retry,omitempty"`   // Retry options
	Options map[string]interface{} `json:"options,omitempty"` // LLM API options
	Silent  bool                   `json:"silent,omitempty"`  // Silent mode, default is true
}

// OptionsCallRetry is the retry options for the call function
type OptionsCallRetry struct {
	Times    int    `json:"times,omitempty"`     // Retry times, default is 3
	Delay    int    `json:"delay,omitempty"`     // Retry delay, default is 200
	DelayMax int    `json:"delay_max,omitempty"` // Retry delay max, default is 5000
	Prompt   string `json:"prompt,omitempty"`    // Retry prompt, default is "Please fix the error. \n {{ error }}"
}

// allowedEvents is the allowed events for the call function
var allowedEvents = map[string]bool{
	"done":    true,
	"retry":   true,
	"message": true,
}

var callProps = []string{
	"assistant_id",
	"input",
	"options",
	"retry_times",
}

// jsNewPlan create a plan object and return it
func jsCall(info *v8go.FunctionCallbackInfo) *v8go.Value {

	args := info.Args()
	if len(args) < 2 {
		return bridge.JsException(info.Context(), "Run requires at least two arguments")
	}

	options := v8go.Undefined(info.Context().Isolate())
	if len(args) > 2 {
		options = args[2]
	}

	// Export the object
	obj := &objectCall{}
	objectTmpl := obj.ExportObject(info)
	this, err := objectTmpl.NewInstance(info.Context())
	if err != nil {
		return bridge.JsException(info.Context(), err.Error())
	}

	// Copy global properties
	global := info.This()
	for _, prop := range objectProperties {
		if !global.Has(prop) {
			continue
		}
		value, err := global.Get(prop)
		if err != nil {
			return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get property %s: %s", prop, err.Error()))
		}
		this.Set(prop, value)
	}

	this.Set("assistant_id", args[0])
	this.Set("input", args[1])
	this.Set("options", options)
	this.Set("retry_times", int32(1))
	return this.Value
}

// ExportObject Export as a FS Object
func (obj *objectCall) ExportObject(info *v8go.FunctionCallbackInfo) *v8go.ObjectTemplate {
	tmpl := v8go.NewObjectTemplate(info.Context().Isolate())
	tmpl.Set("On", v8go.NewFunctionTemplate(info.Context().Isolate(), obj.on))   // On the call
	tmpl.Set("Run", v8go.NewFunctionTemplate(info.Context().Isolate(), obj.run)) // Run the call
	return tmpl
}

// on bind the callback to the call object
func (obj *objectCall) on(info *v8go.FunctionCallbackInfo) *v8go.Value {

	args := info.Args()
	if len(args) < 2 {
		return bridge.JsException(info.Context(), "On requires at least one argument")
	}

	if !args[0].IsString() {
		return bridge.JsException(info.Context(), "The first argument should be a string")
	}

	name := args[0].String()
	if !allowedEvents[name] {
		return bridge.JsException(info.Context(), fmt.Sprintf("Invalid event %s", name))
	}

	cb := args[1]
	if !cb.IsFunction() {
		return bridge.JsException(info.Context(), fmt.Sprintf("The second argument should be a function for event %s", name))
	}

	this := info.This()
	this.Set(fmt.Sprintf("on_%s", name), cb)
	return this.Value
}

// run run the call
func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value {

	this := info.This()
	args := info.Args()

	global, err := getGlobal(info.Context(), this)
	if err != nil {
		return bridge.JsException(info.Context(), err.Error())
	}

	goArgs := []interface{}{}
	jsArgs := []v8go.Valuer{}
	if len(args) > 0 {
		for _, arg := range args {
			v, err := bridge.GoValue(arg, info.Context())
			if err != nil {
				return bridge.JsException(info.Context(), err.Error())
			}
			goArgs = append(goArgs, v)
			jsArgs = append(jsArgs, arg)
		}
	}

	// Get the assistant id
	jsAssistantID, err := this.Get("assistant_id")
	if err != nil {
		return bridge.JsException(info.Context(), err.Error())
	}

	assistantID := jsAssistantID.String()

	// Get the input
	jsInput, err := this.Get("input")
	if err != nil {
		return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the input: %s", err.Error()))
	}

	input, err := bridge.GoValue(jsInput, info.Context())
	if err != nil {
		return bridge.JsException(info.Context(), fmt.Sprintf("Failed to unmarshal the input: %s", err.Error()))
	}

	// Get the retry input
	if this.Has("retry_input") {

		jsRetryInput, err := this.Get("retry_input")
		if err != nil {
			return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the retry input: %s", err.Error()))
		}

		input, err = bridge.GoValue(jsRetryInput, info.Context())
		if err != nil {
			return bridge.JsException(info.Context(), fmt.Sprintf("Failed to unmarshal the retry input: %s", err.Error()))
		}
	}

	// Options
	options := OptionsCall{
		// Retry: OptionsCallRetry{
		// 	Times:    3,
		// 	Delay:    200,
		// 	DelayMax: 1000,
		// 	Prompt:   "{{ input }}\n**Answer is not correct, please try again.**\nError:\n{{ error }} \nAssistant's last answer:\n{{ output }}",
		// },
		Silent:  true,
		Options: map[string]interface{}{}, // LLM API options
	}

	// Get the options
	if this.Has("options") {
		jsOptions, err := this.Get("options")
		if err != nil {
			return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the options: %s", err.Error()))
		}

		// Check if the options is undefined
		if !jsOptions.IsUndefined() {
			err = bridge.Unmarshal(jsOptions, &options)
			if err != nil {
				return bridge.JsException(info.Context(), fmt.Sprintf("Failed to unmarshal the options: %s", err.Error()))
			}
		}
	}

	// Get the assistant
	newAst, err := Get(assistantID)
	if err != nil {
		return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the assistant: %s", err.Error()))
	}

	// Get the message event ( it will be used for the message event )
	eventMessage := ""
	goCallProps := map[string]interface{}{}
	if this.Has("on_message") {
		jsEventMessage, err := this.Get("on_message")
		if err != nil {
			return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the message: %s", err.Error()))
		}
		eventMessage = jsEventMessage.String()

		for _, prop := range callProps {
			if this.Has(prop) {
				value, err := this.Get(prop)
				if err != nil {
					return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the %s property: %s", prop, err.Error()))
				}
				goValue, err := bridge.GoValue(value, info.Context())
				if err != nil {
					return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the %s property: %s", prop, err.Error()))
				}
				goCallProps[prop] = goValue
			}
		}
	}

	// Update the chat context
	var chatCtx chatctx.Context = global.ChatContext
	chatCtx.AssistantID = assistantID
	chatCtx.ChatID = fmt.Sprintf("call_%s", uuid.New().String()) // New chat id
	chatCtx.Silent = options.Silent
	chatCtx.ClientType = chatctx.ClientTypeAgent // Set the client type to agent
	chatCtx.Args = goArgs                        // Arguments for call

	// Define the callback function
	var cb func(msg *chatMessage.Message) = nil
	var output = []chatMessage.Message{}
	cb = func(msg *chatMessage.Message) {
		output = append(output, *msg)
		if eventMessage != "" {
			err := obj.triggerAnonymous(chatCtx, global, goCallProps, eventMessage, goArgs, msg)
			if err != nil {
				color.Red("Failed to trigger the message event: %s", err.Error())
				log.Error("Failed to trigger the message event: %s", err.Error())
				return
			}
		}
	}

	// Execute the assistant
	result, err := newAst.Execute(global.GinContext, chatCtx, input, options.Options, cb) // Execute the assistant
	if err != nil {
		// result, err = obj.retry(jsArgs, err, input, output, info, options)
		// if err != nil {
		// 	return bridge.JsException(info.Context(), err.Error())
		// }
		return bridge.JsException(info.Context(), err.Error())
	}

	// Copy props
	for name, value := range goCallProps {
		info.Context().Global().Set(name, value)
	}

	// Trigger the done event
	doneResult, err := obj.trigger(info, "done", jsArgs...)
	if err != nil {
		// result, err = obj.retry(jsArgs, err, input, output, info, options)
		// if err != nil {
		// 	return bridge.JsException(info.Context(), err.Error())
		// }
		return bridge.JsException(info.Context(), err.Error())
	}

	// Return the done result
	if doneResult != nil && !doneResult.IsUndefined() {
		return doneResult
	}

	// Return Value
	switch v := result.(type) {
	case *v8go.Value:
		return v
	case error:
		return bridge.JsException(info.Context(), v.Error())
	}

	// Return Value
	jsResult, err := bridge.JsValue(info.Context(), result)
	if err != nil {
		return bridge.JsException(info.Context(), fmt.Sprintf("Failed to get the result: %s", err.Error()))
	}
	return jsResult
}

// Deprecated this function is not used anymore
// Use the hook retry instead
// func (obj *objectCall) retry(jsArgs []v8go.Valuer, err error, input interface{}, output []chatMessage.Message, info *v8go.FunctionCallbackInfo, options OptionsCall) (*v8go.Value, error) {

// 	// Retry times, if not set, return the error
// 	if options.Retry.Times <= 0 {
// 		return nil, err
// 	}

// 	this := info.This()
// 	errmsg := exception.Trim(err)

// 	// Get current retry times
// 	jsTimes, retryErr := this.Get("retry_times")
// 	if retryErr != nil {
// 		return nil, fmt.Errorf("%s occurred but failed to get the retry times: %s", errmsg, retryErr.Error())
// 	}

// 	times := int(jsTimes.Int32())
// 	if times > options.Retry.Times {
// 		return nil, fmt.Errorf("%s occurred, max retry times reached", errmsg)
// 	}

// 	// Update the retry times
// 	times = times + 1
// 	this.Set("retry_times", int32(times))

// 	// Message content
// 	content := ""
// 	for _, msg := range output {
// 		if msg.Type == "text" && msg.IsDelta {
// 			content += msg.Text
// 		}
// 	}

// 	// Delay
// 	delay := options.Retry.Delay * int(times)
// 	if delay > options.Retry.DelayMax {
// 		delay = options.Retry.DelayMax
// 	}

// 	// Wait for the delay
// 	if delay > 0 {
// 		time.Sleep(time.Duration(delay) * time.Millisecond)
// 	}

// 	// Format the input
// 	var lastUserMessage *chatMessage.Message = nil
// 	var inputMessages []*chatMessage.Message = nil
// 	var lastUserMessageIndex int = 0
// 	switch v := input.(type) {
// 	case string:
// 		lastUserMessage = &chatMessage.Message{Type: "text", Text: v, Role: "user"}
// 		inputMessages = []*chatMessage.Message{lastUserMessage}

// 	case []interface{}:
// 		// Get the last user message
// 		raw, parseErr := jsoniter.Marshal(v)
// 		if parseErr != nil {
// 			return nil, fmt.Errorf("%s occurred but failed to marshal the input: %s", errmsg, parseErr.Error())
// 		}

// 		parseErr = jsoniter.Unmarshal(raw, &inputMessages)
// 		if parseErr != nil {
// 			return nil, fmt.Errorf("%s occurred but failed to unmarshal the input: %s", errmsg, parseErr.Error())
// 		}

// 		// Get the last user message
// 		for i := len(inputMessages) - 1; i >= 0; i-- {
// 			if inputMessages[i].Type == "text" && inputMessages[i].Role == "user" {
// 				lastUserMessage = inputMessages[i]
// 				lastUserMessageIndex = i
// 				break
// 			}
// 		}

// 	case *chatMessage.Message:
// 		lastUserMessage = v
// 		inputMessages = []*chatMessage.Message{lastUserMessage}

// 	case map[string]interface{}:
// 		text, ok := v["text"].(string)
// 		if !ok {
// 			return nil, fmt.Errorf("%s occurred but failed to get the text", errmsg)
// 		}

// 		if v["role"] != "user" {
// 			return nil, fmt.Errorf("%s occurred but the role is not user", errmsg)
// 		}

// 		lastUserMessage = &chatMessage.Message{Type: "text", Text: text, Role: "user"}
// 		inputMessages = []*chatMessage.Message{lastUserMessage}
// 	}

// 	// Get the prompt from the options
// 	promptTmpl := options.Retry.Prompt
// 	data := sui.Data{
// 		"error":  errmsg,
// 		"output": strings.TrimSpace(content),
// 		"input":  lastUserMessage.Text,
// 	}
// 	prompt, _ := data.Replace(promptTmpl)

// 	// Custom retry prompt by hooking the retry event
// 	if this.Has("on_retry") {
// 		info.Context().Global().Set("error", errmsg) // Set error
// 		jsDelay, _ := bridge.JsValue(info.Context(), delay)
// 		jsPrompt, _ := bridge.JsValue(info.Context(), prompt)
// 		newPrompt, retryErr := obj.trigger(info, "retry", jsTimes, jsDelay, jsPrompt)
// 		if retryErr != nil {
// 			return nil, fmt.Errorf("%s occurred but failed to trigger the retry event: %s", errmsg, retryErr.Error())
// 		}
// 		// Update the prompt
// 		if newPrompt.IsString() {
// 			prompt = newPrompt.String()
// 		}
// 	}

// 	// Generate the new input with the prompt
// 	// Update the input
// 	inputMessages[lastUserMessageIndex].Text = prompt
// 	jsInput, inputErr := bridge.JsValue(info.Context(), inputMessages)
// 	if inputErr != nil {
// 		return nil, fmt.Errorf("%s occurred but failed to update the input: %s", errmsg, inputErr.Error())
// 	}
// 	// Update the input
// 	this.Set("retry_input", jsInput)

// 	// Call the run function
// 	run, funcErr := this.Get("Run")
// 	if funcErr != nil {
// 		return nil, fmt.Errorf("%s occurred but failed to get the run function: %s", errmsg, funcErr.Error())
// 	}

// 	if !run.IsFunction() {
// 		return nil, fmt.Errorf("%s occurred but the run function is not a function", errmsg)
// 	}

// 	fn, fnErr := run.AsFunction()
// 	if fnErr != nil {
// 		return nil, fmt.Errorf("%s occurred but failed to get the run function: %s", errmsg, fnErr.Error())
// 	}

// 	result, resErr := fn.Call(this, jsArgs...)
// 	if resErr != nil {
// 		return nil, fmt.Errorf("%s (%d)", exception.Trim(resErr), times-1)
// 	}

// 	return result, nil
// }

func (obj *objectCall) triggerAnonymous(chatCtx chatctx.Context, global *GlobalVariables, goCallProps map[string]interface{}, source string, bindArgs []interface{}, fnArgs ...interface{}) error {

	ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
	if err != nil {
		return err
	}
	defer ctx.Close()

	// Update Context
	global.Assistant.InitObject(ctx, global.GinContext, chatCtx, global.Contents)

	// Copy props
	for k, v := range goCallProps {
		ctx.WithGlobal(k, v)
	}

	// Add the args
	ctx.WithGlobal("args", bindArgs)
	_, err = ctx.CallAnonymousWith(context.Background(), source, fnArgs...)
	if err != nil {
		return err
	}
	return nil

}

// trigger trigger the callback
func (obj *objectCall) trigger(info *v8go.FunctionCallbackInfo, name string, fnArgs ...v8go.Valuer) (*v8go.Value, error) {
	// Try to get the callback
	this := info.This()
	if this.Has(fmt.Sprintf("on_%s", name)) {
		event, err := this.Get(fmt.Sprintf("on_%s", name))
		if err != nil {
			return nil, err
		}

		if event.IsFunction() {

			cb, err := event.AsFunction()
			if err != nil {
				return nil, err
			}

			result, err := cb.Call(this, fnArgs...)
			if err != nil {
				return nil, err
			}
			return result, nil
		}
	}

	return nil, nil
}

// jsCallBackup is the backup function for the call function
// func jsCallBackup(info *v8go.FunctionCallbackInfo) *v8go.Value {

// 	// Get the args
// 	args := info.Args()
// 	if len(args) < 2 {
// 		return bridge.JsException(info.Context(), "Run requires at least two arguments")
// 	}

// 	// Get the assistant id
// 	assistantID := args[0].String()

// 	// Get the assistant
// 	newAst, err := Get(assistantID)
// 	if err != nil {
// 		return bridge.JsException(info.Context(), err.Error())
// 	}

// 	// Get the input
// 	input := args[1].String()

// 	// Get the global variables
// 	global, err := global(info)
// 	if err != nil {
// 		return bridge.JsException(info.Context(), err.Error())
// 	}

// 	// Update Context
// 	chatContext := global.ChatContext
// 	chatContext.AssistantID = assistantID
// 	chatContext.ChatID = fmt.Sprintf("chat_%s", uuid.New().String()) // New chat id
// 	chatContext.Silent = true                                        // Silent mode

// 	var cb func(msg *chatMessage.Message) = nil
// 	if len(args) > 2 {

// 		// Rest args
// 		var jsArgs *v8go.Value
// 		goArgs := []interface{}{}
// 		if len(args) > 3 {
// 			jsArgs = args[3]
// 			if jsArgs != nil {
// 				if jsArgs.IsArray() {
// 					v, err := bridge.GoValue(jsArgs, info.Context())
// 					if err != nil {
// 						return bridge.JsException(info.Context(), err.Error())
// 					}
// 					arr, ok := v.([]interface{})
// 					if !ok {
// 						return bridge.JsException(info.Context(), "Invalid arguments")
// 					}
// 					goArgs = arr
// 				} else {
// 					v, err := bridge.GoValue(jsArgs, info.Context())
// 					if err != nil {
// 						return bridge.JsException(info.Context(), err.Error())
// 					}
// 					goArgs = []interface{}{v}
// 				}
// 			}
// 		}

// 		// Parse the callback
// 		funcType := "method"
// 		name := ""
// 		userArgs := []interface{}{}
// 		if args[2].IsFunction() {
// 			funcType = "anonymous"
// 		} else {
// 			goValue, err := bridge.GoValue(args[2], info.Context())
// 			if err != nil {
// 				return bridge.JsException(info.Context(), err.Error())
// 			}
// 			switch v := goValue.(type) {
// 			case string:
// 				name = v
// 			case map[string]interface{}:
// 				if fname, ok := v["name"].(string); ok {
// 					name = fname
// 				}
// 				if args, ok := v["args"].([]interface{}); ok {
// 					userArgs = args
// 				}
// 			}

// 			if strings.Contains(name, ".") {
// 				funcType = "process"
// 			}
// 		}

// 		switch funcType {
// 		case "anonymous":
// 			source := args[2].String()
// 			cb = func(msg *chatMessage.Message) {
// 				cbArgs := []interface{}{msg}
// 				cbArgs = append(cbArgs, goArgs...)
// 				ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
// 				if err != nil {
// 					fmt.Println("Failed to create context", err.Error())
// 					return
// 				}
// 				defer ctx.Close()

// 				global.Assistant.InitObject(ctx, global.GinContext, chatContext, global.Contents)
// 				_, err = ctx.CallAnonymousWith(context.Background(), source, cbArgs...)
// 				if err != nil {
// 					log.Error("Failed to call the method: %s", err.Error())
// 					color.Red("Failed to call the method: %s", err.Error())
// 					return
// 				}
// 			}
// 			break

// 		case "process":

// 			cb = func(msg *chatMessage.Message) {
// 				cbArgs := []interface{}{}
// 				cbArgs = append(cbArgs, msg)
// 				cbArgs = append(cbArgs, userArgs...)
// 				p, err := process.Of(name, cbArgs...)
// 				if err != nil {
// 					log.Error("Failed to get the process: %s", err.Error())
// 					color.Red("Failed to get the process: %s", err.Error())
// 					return
// 				}
// 				err = p.Execute()
// 				if err != nil {
// 					log.Error("Failed to execute the process: %s", err.Error())
// 					color.Red("Failed to execute the process: %s", err.Error())
// 					return
// 				}
// 				defer p.Release()
// 			}

// 		case "method":

// 			cb = func(msg *chatMessage.Message) {
// 				cbArgs := []interface{}{}
// 				cbArgs = append(cbArgs, msg)
// 				cbArgs = append(cbArgs, userArgs...)
// 				ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
// 				if err != nil {
// 					return
// 				}
// 				defer ctx.Close()

// 				global.Assistant.InitObject(ctx, global.GinContext, global.ChatContext, global.Contents)
// 				_, err = ctx.CallWith(context.Background(), name, cbArgs...)
// 				if err != nil {
// 					log.Error("Failed to call the method: %s", err.Error())
// 					color.Red("Failed to call the method: %s", err.Error())
// 					return
// 				}
// 			}
// 		}

// 	}

// 	// Parse the options
// 	options := map[string]interface{}{}
// 	if len(args) > 4 {
// 		optionsRaw, err := bridge.GoValue(args[4], info.Context())
// 		if err != nil {
// 			return bridge.JsException(info.Context(), err.Error())
// 		}

// 		// Parse the options
// 		if optionsRaw != nil {
// 			switch v := optionsRaw.(type) {
// 			case string:
// 				err := jsoniter.UnmarshalFromString(v, &options)
// 				if err != nil {
// 					return bridge.JsException(info.Context(), err.Error())
// 				}
// 			case map[string]interface{}:
// 				options = v
// 			default:
// 				return bridge.JsException(info.Context(), "Invalid options")
// 			}
// 		}
// 	}

// 	err = newAst.Execute(global.GinContext, chatContext, input, options, cb) // Execute the assistant
// 	if err != nil {
// 		return bridge.JsException(info.Context(), err.Error())
// 	}
// 	return nil
// }
