package controllogic

import (
	"sync"

	lua "github.com/yuin/gopher-lua"
)

// luaRuntime holds a sandboxed Lua VM for one action.lua node. The VM is created
// lazily and reused; access is serialised so concurrent flow activations of the
// same node don't share VM state unsafely. Host functions exposed to scripts:
//
//	get("ds:name")   → number   read a signal / sys / local value (NaN if absent)
//	set("ds:name", v)            write a value (bare name → graph-local var)
//	log(msg)                     append to the server log
//
// The os, io, package, debug and code-loading globals are removed, mirroring the
// dsp LuaNode sandbox.
type luaRuntime struct {
	script string

	mu sync.Mutex
	L  *lua.LState

	// Bound to the current activation while run() holds mu.
	curResolve Resolver
	curSet     func(target string, val float64)
	curLog     func(msg string)
}

func newLuaRuntime(script string) *luaRuntime {
	return &luaRuntime{script: script}
}

func (lr *luaRuntime) ensure() error {
	if lr.L != nil {
		return nil
	}
	L := lua.NewState(lua.Options{SkipOpenLibs: true})
	for _, pair := range []struct {
		name string
		fn   lua.LGFunction
	}{
		{lua.LoadLibName, lua.OpenPackage},
		{lua.BaseLibName, lua.OpenBase},
		{lua.MathLibName, lua.OpenMath},
		{lua.StringLibName, lua.OpenString},
		{lua.TabLibName, lua.OpenTable},
	} {
		if err := L.CallByParam(lua.P{Fn: L.NewFunction(pair.fn), NRet: 0, Protect: true}, lua.LString(pair.name)); err != nil {
			L.Close()
			return err
		}
	}
	L.SetGlobal("load", lua.LNil)
	L.SetGlobal("loadfile", lua.LNil)
	L.SetGlobal("dofile", lua.LNil)
	L.SetGlobal("require", lua.LNil)

	L.SetGlobal("get", L.NewFunction(func(s *lua.LState) int {
		target := s.CheckString(1)
		ds, name, ok := parseRef(target)
		var v float64
		if ok && lr.curResolve != nil {
			v = lr.curResolve(ds, name)
		}
		s.Push(lua.LNumber(v))
		return 1
	}))
	L.SetGlobal("set", L.NewFunction(func(s *lua.LState) int {
		target := s.CheckString(1)
		val := float64(s.CheckNumber(2))
		if lr.curSet != nil {
			lr.curSet(target, val)
		}
		return 0
	}))
	L.SetGlobal("log", L.NewFunction(func(s *lua.LState) int {
		if lr.curLog != nil {
			lr.curLog(s.CheckString(1))
		}
		return 0
	}))

	lr.L = L
	return nil
}

// run executes the script once with the given host hooks bound.
func (lr *luaRuntime) run(resolve Resolver, set func(string, float64), logf func(string)) {
	lr.mu.Lock()
	defer lr.mu.Unlock()

	if err := lr.ensure(); err != nil {
		logf("lua init error: " + err.Error())
		return
	}

	lr.curResolve = resolve
	lr.curSet = set
	lr.curLog = logf
	defer func() {
		lr.curResolve = nil
		lr.curSet = nil
		lr.curLog = nil
		if r := recover(); r != nil {
			logf("lua panic")
		}
	}()

	lr.L.SetTop(0)
	if err := lr.L.DoString(lr.script); err != nil {
		logf("lua error: " + err.Error())
	}
}
