package goja_bindings

import (
	"encoding/json"
	"fmt"
	"seanime/internal/events"
	"seanime/internal/extension"
	"strings"

	"github.com/dop251/goja"
	"github.com/rs/zerolog"
	"github.com/samber/mo"
)

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Console
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

type console struct {
	logger         *zerolog.Logger
	vm             *goja.Runtime
	wsEventManager mo.Option[events.WSEventManagerInterface]
	ext            *extension.Extension
}

// BindConsole binds the console to the VM
func BindConsole(vm *goja.Runtime, logger *zerolog.Logger) error {
	return BindConsoleWithWS(nil, vm, logger, nil)
}

// BindConsoleWithWS binds the console to the VM and sends logs messages to the websocket manager
// in order to be printed in the browser console
func BindConsoleWithWS(ext *extension.Extension, vm *goja.Runtime, logger *zerolog.Logger, wsEventManager events.WSEventManagerInterface) error {
	c := &console{
		logger:         logger,
		vm:             vm,
		wsEventManager: mo.None[events.WSEventManagerInterface](),
		ext:            ext,
	}
	if wsEventManager != nil {
		c.wsEventManager = mo.Some(wsEventManager)
	}

	consoleObj := vm.NewObject()
	consoleObj.Set("log", c.logFunc("log"))
	consoleObj.Set("error", c.logFunc("error"))
	consoleObj.Set("warn", c.logFunc("warn"))
	consoleObj.Set("info", c.logFunc("info"))
	consoleObj.Set("debug", c.logFunc("debug"))

	vm.Set("console", consoleObj)

	return nil
}

func (c *console) logFunc(t string) (ret func(c goja.FunctionCall) goja.Value) {
	defer func() {
		if r := recover(); r != nil {
			c.logger.Error().Msgf("extension: Panic from console: %v", r)
			ret = func(call goja.FunctionCall) goja.Value {
				return goja.Undefined()
			}
		}
	}()

	return func(call goja.FunctionCall) goja.Value {
		var ret []string
		for _, arg := range call.Arguments {
			// Check if the argument is a goja.Object
			if obj, ok := arg.(*goja.Object); ok {
				// First check if it's a Go error
				if _, ok := obj.Export().(error); ok {
					ret = append(ret, fmt.Sprintf("%+v", obj.Export()))
					continue
				}

				// Then check if it's a JavaScript Error object by checking its constructor name
				constructor := obj.Get("constructor")
				if constructor != nil && !goja.IsUndefined(constructor) && !goja.IsNull(constructor) {
					if constructorObj, ok := constructor.(*goja.Object); ok {
						if name := constructorObj.Get("name"); name != nil && !goja.IsUndefined(name) && !goja.IsNull(name) {
							if name.String() == "Error" || strings.HasSuffix(name.String(), "Error") {
								message := obj.Get("message")
								stack := obj.Get("stack")
								errStr := name.String()
								if message != nil && !goja.IsUndefined(message) && !goja.IsNull(message) {
									errStr += ": " + fmt.Sprintf("%+v", message.Export())
								}
								if stack != nil && !goja.IsUndefined(stack) && !goja.IsNull(stack) {
									errStr += "\nStack: " + fmt.Sprintf("%+v", stack.Export())
								}
								ret = append(ret, errStr)
								continue
							}
						}
					}
				}

				// Fallback for other objects: Try calling toString() if available
				if hasOwnPropFn, ok := goja.AssertFunction(obj.Get("hasOwnProperty")); ok {
					if retVal, err := hasOwnPropFn(obj, c.vm.ToValue("toString")); err == nil && retVal.ToBoolean() {
						tsVal := obj.Get("toString")
						if fn, ok := goja.AssertFunction(tsVal); ok {
							strVal, err := fn(obj)
							if err == nil {
								// Avoid double logging if toString() is just "[object Object]"
								if strVal.String() != "[object Object]" {
									ret = append(ret, strVal.String())
									continue // Skip default handling if toString() worked
								}
							}
						}
					}
				}
			}

			// Original default handling
			switch v := arg.Export().(type) {
			case nil:
				ret = append(ret, "undefined")
			case bool:
				ret = append(ret, fmt.Sprintf("%t", v))
			case int64, float64:
				ret = append(ret, fmt.Sprintf("%v", v))
			case string:
				if v == "" {
					ret = append(ret, fmt.Sprintf("%q", v))
					break
				}
				ret = append(ret, fmt.Sprintf("%s", v))
			case []byte:
				ret = append(ret, fmt.Sprintf("Uint8Array %s", fmt.Sprint(v)))
			case map[string]interface{}:
				// Try to marshal the value to JSON
				bs, err := json.Marshal(v)
				if err != nil {
					ret = append(ret, fmt.Sprintf("%+v", v))
				} else {
					ret = append(ret, fmt.Sprintf("%s", string(bs)))
				}
			default:
				// Try to marshal the value to JSON
				bs, err := json.Marshal(v)
				if err != nil {
					ret = append(ret, fmt.Sprintf("%+v", v))
				} else {
					ret = append(ret, fmt.Sprintf("%s", string(bs)))
				}
			}
		}
		switch t {
		case "log", "warn", "info", "debug":
			c.logger.Debug().Msgf("extension: (console.%s): %s", t, strings.Join(ret, " "))
		case "error":
			c.logger.Error().Msgf("extension: (console.error): %s", strings.Join(ret, " "))
		}
		if wsEventManager, found := c.wsEventManager.Get(); found && c.ext != nil {
			wsEventManager.SendEvent(events.ConsoleLog, fmt.Sprintf("%s (console.%s): %s", c.ext.ID, t, strings.Join(ret, " ")))
		}
		return goja.Undefined()
	}
}

//func (c *console) logFunc(t string) (ret func(c goja.FunctionCall) goja.Value) {
//	defer func() {
//		if r := recover(); r != nil {
//			c.logger.Error().Msgf("extension: Panic from console: %v", r)
//			ret = func(call goja.FunctionCall) goja.Value {
//				return goja.Undefined()
//			}
//		}
//	}()
//
//	return func(call goja.FunctionCall) goja.Value {
//		var ret []string
//		for _, arg := range call.Arguments {
//			if arg == nil || arg.Export() == nil || arg.ExportType() == nil {
//				ret = append(ret, "undefined")
//				continue
//			}
//			if bytes, ok := arg.Export().([]byte); ok {
//				ret = append(ret, fmt.Sprintf("%s", string(bytes)))
//				continue
//			}
//			if arg.ExportType().Kind() == reflect.Struct || arg.ExportType().Kind() == reflect.Map || arg.ExportType().Kind() == reflect.Slice {
//				ret = append(ret, strings.ReplaceAll(spew.Sprint(arg.Export()), "\n", ""))
//			} else {
//				ret = append(ret, fmt.Sprintf("%+v", arg.Export()))
//			}
//		}
//
//		switch t {
//		case "log", "warn", "info", "debug":
//			c.logger.Debug().Msgf("extension: [console.%s] %s", t, strings.Join(ret, " "))
//		case "error":
//			c.logger.Error().Msgf("extension: [console.error] %s", strings.Join(ret, " "))
//		}
//		return goja.Undefined()
//	}
//}
