package luavm

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"time"

	lua "github.com/yuin/gopher-lua"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/klog/v2"
	luajson "layeh.com/gopher-json"

	configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
	"github.com/karmada-io/karmada/pkg/util/fixedpool"
	"github.com/karmada-io/karmada/pkg/util/lifted"
)

// VM Defines a struct that implements the luaVM.
type VM struct {
	// UseOpenLibs flag to enable open libraries. Libraries are disabled by default while running, but enabled during testing to allow the use of print statements.
	UseOpenLibs bool
	Pool        *fixedpool.FixedPool
}

// New creates a manager for lua VM
func New(useOpenLibs bool, poolSize int) *VM {
	vm := &VM{
		UseOpenLibs: useOpenLibs,
	}
	vm.Pool = fixedpool.New(
		"luavm",
		func() (any, error) { return vm.NewLuaState() },
		func(a any) { a.(*lua.LState).Close() },
		poolSize)
	return vm
}

// NewLuaState creates a new lua state.
func (vm *VM) NewLuaState() (*lua.LState, error) {
	l := lua.NewState(lua.Options{
		SkipOpenLibs: !vm.UseOpenLibs,
	})
	// Opens table library to allow access to functions to manipulate tables
	err := vm.setLib(l)
	if err != nil {
		return nil, err
	}
	// preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work
	l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader)
	// preload kube library. Allows the 'local kube = require("kube")' to work
	l.PreloadModule(KubeLibName, KubeLoader)
	return l, err
}

// RunScript got a lua vm from pool, and execute script with given arguments.
func (vm *VM) RunScript(script string, fnName string, nRets int, args ...interface{}) ([]lua.LValue, error) {
	a, err := vm.Pool.Get()
	if err != nil {
		return nil, err
	}
	defer vm.Pool.Put(a)

	l := a.(*lua.LState)
	l.Pop(l.GetTop())

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	l.SetContext(ctx)

	err = l.DoString(script)
	if err != nil {
		return nil, err
	}

	vArgs := make([]lua.LValue, len(args))
	for i, arg := range args {
		vArgs[i], err = decodeValue(l, arg)
		if err != nil {
			return nil, err
		}
	}

	f := l.GetGlobal(fnName)
	if f.Type() == lua.LTNil {
		return nil, fmt.Errorf("not found function %v", fnName)
	}
	if f.Type() != lua.LTFunction {
		return nil, fmt.Errorf("%s is not a function: %s", fnName, f.Type())
	}

	err = l.CallByParam(lua.P{
		Fn:      f,
		NRet:    nRets,
		Protect: true,
	}, vArgs...)
	if err != nil {
		return nil, err
	}

	// get rets from stack: [ret1, ret2, ret3 ...]
	rets := make([]lua.LValue, nRets)
	for i := range rets {
		rets[i] = l.Get(i + 1)
	}
	// pop all the values in stack
	l.Pop(l.GetTop())
	return rets, nil
}

// GetReplicas returns the desired replicas of the object as well as the requirements of each replica by lua script.
func (vm *VM) GetReplicas(obj *unstructured.Unstructured, script string) (replica int32, requires *workv1alpha2.ReplicaRequirements, err error) {
	results, err := vm.RunScript(script, "GetReplicas", 2, obj)
	if err != nil {
		return 0, nil, err
	}

	replica, err = ConvertLuaResultToInt(results[0])
	if err != nil {
		return 0, nil, err
	}

	replicaRequirementResult := results[1]
	requires = &workv1alpha2.ReplicaRequirements{}
	if replicaRequirementResult.Type() == lua.LTTable {
		err = ConvertLuaResultInto(replicaRequirementResult, requires)
		if err != nil {
			klog.Errorf("ConvertLuaResultToReplicaRequirements err %v", err.Error())
			return 0, nil, err
		}
	} else if replicaRequirementResult.Type() == lua.LTNil {
		requires = nil
	} else {
		return 0, nil, fmt.Errorf("expect the returned requires type is table but got %s", replicaRequirementResult.Type())
	}

	return
}

// ReviseReplica revises the replica of the given object by lua.
func (vm *VM) ReviseReplica(object *unstructured.Unstructured, replica int64, script string) (*unstructured.Unstructured, error) {
	results, err := vm.RunScript(script, "ReviseReplica", 1, object, replica)
	if err != nil {
		return nil, err
	}

	luaResult := results[0]
	reviseReplicaResult := &unstructured.Unstructured{}
	if luaResult.Type() == lua.LTTable {
		err := ConvertLuaResultInto(luaResult, reviseReplicaResult)
		if err != nil {
			return nil, err
		}
		return reviseReplicaResult, nil
	}

	return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type())
}

func (vm *VM) setLib(l *lua.LState) error {
	for _, pair := range []struct {
		n string
		f lua.LGFunction
	}{
		{lua.LoadLibName, lua.OpenPackage},
		{lua.BaseLibName, lua.OpenBase},
		{lua.TabLibName, lua.OpenTable},
		{lua.StringLibName, lua.OpenString},
		{lua.MathLibName, lua.OpenMath},
		// load our 'safe' version of the OS library
		{lua.OsLibName, lifted.OpenSafeOs},
	} {
		if err := l.CallByParam(lua.P{
			Fn:      l.NewFunction(pair.f),
			NRet:    0,
			Protect: true,
		}, lua.LString(pair.n)); err != nil {
			return err
		}
	}
	return nil
}

// Retain returns the objects that based on the "desired" object but with values retained from the "observed" object by lua.
func (vm *VM) Retain(desired *unstructured.Unstructured, observed *unstructured.Unstructured, script string) (retained *unstructured.Unstructured, err error) {
	results, err := vm.RunScript(script, "Retain", 1, desired, observed)
	if err != nil {
		return nil, err
	}

	luaResult := results[0]
	retainResult := &unstructured.Unstructured{}
	if luaResult.Type() == lua.LTTable {
		err := ConvertLuaResultInto(luaResult, retainResult)
		if err != nil {
			return nil, err
		}
		return retainResult, nil
	}
	return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type())
}

// AggregateStatus returns the objects that based on the 'object' but with status aggregated by lua.
func (vm *VM) AggregateStatus(object *unstructured.Unstructured, items []workv1alpha2.AggregatedStatusItem, script string) (*unstructured.Unstructured, error) {
	results, err := vm.RunScript(script, "AggregateStatus", 1, object, items)
	if err != nil {
		return nil, err
	}

	luaResult := results[0]
	aggregateStatus := &unstructured.Unstructured{}
	if luaResult.Type() == lua.LTTable {
		err := ConvertLuaResultInto(luaResult, aggregateStatus)
		if err != nil {
			return nil, err
		}
		return aggregateStatus, nil
	}
	return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type())
}

// InterpretHealth returns the health state of the object by lua.
func (vm *VM) InterpretHealth(object *unstructured.Unstructured, script string) (bool, error) {
	results, err := vm.RunScript(script, "InterpretHealth", 1, object)
	if err != nil {
		return false, err
	}

	var health bool
	health, err = ConvertLuaResultToBool(results[0])
	if err != nil {
		return false, err
	}
	return health, nil
}

// ReflectStatus returns the status of the object by lua.
func (vm *VM) ReflectStatus(object *unstructured.Unstructured, script string) (status *runtime.RawExtension, err error) {
	results, err := vm.RunScript(script, "ReflectStatus", 1, object)
	if err != nil {
		return nil, err
	}

	luaStatusResult := results[0]
	if luaStatusResult.Type() != lua.LTTable {
		return nil, fmt.Errorf("expect the returned replica type is table but got %s", luaStatusResult.Type())
	}

	status = &runtime.RawExtension{}
	err = ConvertLuaResultInto(luaStatusResult, status)
	return status, err
}

// GetDependencies returns the dependent resources of the given object by lua.
func (vm *VM) GetDependencies(object *unstructured.Unstructured, script string) (dependencies []configv1alpha1.DependentObjectReference, err error) {
	results, err := vm.RunScript(script, "GetDependencies", 1, object)
	if err != nil {
		return nil, err
	}

	luaResult := results[0]

	if luaResult.Type() != lua.LTTable {
		return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type())
	}
	err = ConvertLuaResultInto(luaResult, &dependencies)
	return
}

// NewWithContext creates a lua VM with the given context.
func NewWithContext(ctx context.Context) (*lua.LState, error) {
	vm := VM{}
	l := lua.NewState(lua.Options{
		SkipOpenLibs: !vm.UseOpenLibs,
	})
	// Opens table library to allow access to functions to manipulate tables
	err := vm.setLib(l)
	if err != nil {
		return nil, err
	}
	// preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work
	l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader)
	// preload kube library. Allows the 'local kube = require("kube")' to work
	l.PreloadModule(KubeLibName, KubeLoader)
	if ctx != nil {
		l.SetContext(ctx)
	}
	return l, nil
}

// nolint:gocyclo
func decodeValue(L *lua.LState, value interface{}) (lua.LValue, error) {
	// We handle simple type without json for better performance.
	switch converted := value.(type) {
	case []interface{}:
		arr := L.CreateTable(len(converted), 0)
		for _, item := range converted {
			v, err := decodeValue(L, item)
			if err != nil {
				return nil, err
			}
			arr.Append(v)
		}
		return arr, nil
	case map[string]interface{}:
		tbl := L.CreateTable(0, len(converted))
		for key, item := range converted {
			v, err := decodeValue(L, item)
			if err != nil {
				return nil, err
			}
			tbl.RawSetString(key, v)
		}
		return tbl, nil
	case nil:
		return lua.LNil, nil
	}

	v := reflect.ValueOf(value)
	switch {
	case v.CanInt():
		return lua.LNumber(v.Int()), nil
	case v.CanUint():
		return lua.LNumber(v.Uint()), nil
	case v.CanFloat():
		return lua.LNumber(v.Float()), nil
	}

	switch t := v.Type(); t.Kind() {
	case reflect.String:
		return lua.LString(v.String()), nil
	case reflect.Bool:
		return lua.LBool(v.Bool()), nil
	case reflect.Pointer:
		if v.IsNil() {
			return lua.LNil, nil
		}
	}

	// Other types can't be handled, ask for help from json
	data, err := json.Marshal(value)
	if err != nil {
		return nil, fmt.Errorf("json Marshal obj %#v error: %v", value, err)
	}

	lv, err := luajson.Decode(L, data)
	if err != nil {
		return nil, fmt.Errorf("lua Decode obj %#v error: %v", value, err)
	}
	return lv, nil
}
