package scripting

import (
	stdcontext "context"
	"fmt"
	"strconv"
	"strings"
	"sync/atomic"

	tea "charm.land/bubbletea/v2"
	"github.com/atotto/clipboard"
	"github.com/idursun/jjui/internal/ui/actionmeta"
	"github.com/idursun/jjui/internal/ui/choose"
	"github.com/idursun/jjui/internal/ui/common"
	uicontext "github.com/idursun/jjui/internal/ui/context"
	"github.com/idursun/jjui/internal/ui/exec_process"
	"github.com/idursun/jjui/internal/ui/input"
	"github.com/idursun/jjui/internal/ui/intents"
	"github.com/idursun/jjui/internal/ui/revisions"
	lua "github.com/yuin/gopher-lua"
)

type step struct {
	cmd     tea.Cmd
	matcher func(tea.Msg) (bool, []lua.LValue)
}

type Runner struct {
	ctx        *uicontext.MainContext
	main       *lua.LState
	thread     *lua.LState
	cancel     stdcontext.CancelFunc
	fn         *lua.LFunction
	started    bool
	await      func(tea.Msg) (bool, []lua.LValue)
	resumeArgs []lua.LValue
	done       bool
}

var nextActionCompletionID atomic.Uint64

func RunScript(ctx *uicontext.MainContext, src string) (*Runner, tea.Cmd, error) {
	L, err := vmFromContext(ctx)
	if err != nil {
		return nil, nil, err
	}

	r := &Runner{ctx: ctx, main: L}

	fn, err := L.LoadString(src)
	if err != nil {
		return nil, nil, fmt.Errorf("lua: %w", err)
	}
	r.fn = fn
	r.thread, r.cancel = L.NewThread()

	cmd := r.resume()
	if r.done {
		r.close()
	}
	return r, cmd, nil
}

func (r *Runner) close() {
	if r.cancel != nil {
		r.cancel()
		r.cancel = nil
	}
}

func (r *Runner) resume() tea.Cmd {
	if r.done {
		return nil
	}
	var cmds []tea.Cmd
	for {
		var fn *lua.LFunction
		if !r.started {
			fn = r.fn
		}
		args := r.resumeArgs
		r.resumeArgs = nil
		state, err, values := r.main.Resume(r.thread, fn, args...)
		r.started = true
		if err != nil {
			r.done = true
			cmds = append(cmds, intents.Invoke(intents.AddMessage{Text: err.Error(), Err: err}))
			break
		}
		for _, v := range values {
			if ud, ok := v.(*lua.LUserData); ok {
				if st, ok := ud.Value.(step); ok {
					if st.matcher != nil {
						r.await = st.matcher
						if st.cmd != nil {
							cmds = append(cmds, st.cmd)
						}
						return tea.Sequence(cmds...)
					}
					if st.cmd != nil {
						cmds = append(cmds, st.cmd)
					}
				}
			}
		}
		if state == lua.ResumeOK {
			r.done = true
			break
		}
		// continue to resume to collect subsequent steps until an await or completion
		if len(cmds) > 0 {
			continue
		}
	}
	if len(cmds) == 0 {
		return nil
	}
	return tea.Sequence(cmds...)
}

// HandleMsg resumes the script if waiting for a matching message.
func (r *Runner) HandleMsg(msg tea.Msg) tea.Cmd {
	if r.await == nil {
		return nil
	}
	ok, resume := r.await(msg)
	if !ok {
		return nil
	}
	r.await = nil
	r.resumeArgs = resume
	cmd := r.resume()
	if r.done {
		r.close()
	}
	return cmd
}

func (r *Runner) Done() bool {
	return r.done && r.await == nil
}

func registerAPI(L *lua.LState, ctx *uicontext.MainContext) {
	revisionsTable := L.NewTable()
	revisionsTable.RawSetString("current", L.NewFunction(func(L *lua.LState) int {
		if rev, ok := ctx.SelectedItem.(uicontext.SelectedRevision); ok {
			L.Push(lua.LString(rev.ChangeId))
			return 1
		}
		return 0
	}))
	revisionsTable.RawSetString("checked", L.NewFunction(func(L *lua.LState) int {
		tbl := L.NewTable()
		for _, item := range ctx.CheckedItems {
			if rev, ok := item.(uicontext.SelectedRevision); ok {
				tbl.Append(lua.LString(rev.ChangeId))
			}
		}
		L.Push(tbl)
		return 1
	}))
	revisionsTable.RawSetString("refresh", L.NewFunction(func(L *lua.LState) int {
		payload := payloadFromTop(L)
		intent := intents.Refresh{
			KeepSelections:   boolVal(payload, "keep_selections"),
			SelectedRevision: stringVal(payload, "selected_revision"),
		}
		return yieldStep(L, step{cmd: revisions.RevisionsCmd(intent), matcher: matchUpdateRevisionsSuccess})
	}))
	revisionsTable.RawSetString("navigate", L.NewFunction(func(L *lua.LState) int {
		payload := payloadFromTop(L)
		target := parseNavigateTarget(stringVal(payload, "target"))
		intent := intents.Navigate{
			Delta:      intVal(payload, "by"),
			IsPage:     boolVal(payload, "page"),
			Target:     target,
			ChangeID:   stringVal(payload, "to"),
			FallbackID: stringVal(payload, "fallback"),
		}
		if v, ok := payload["ensureView"]; ok {
			if b, ok := v.(bool); ok {
				intent.EnsureView = boolPtr(b)
			}
		}
		if v, ok := payload["allowStream"]; ok {
			if b, ok := v.(bool); ok {
				intent.AllowStream = boolPtr(b)
			}
		}
		return yieldStep(L, step{cmd: revisions.RevisionsCmd(intent)})
	}))

	revsetTable := L.NewTable()
	revsetTable.RawSetString("current", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(ctx.CurrentRevset))
		return 1
	}))
	revsetTable.RawSetString("default", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(ctx.DefaultRevset))
		return 1
	}))

	contextTable := L.NewTable()
	contextTable.RawSetString("change_id", L.NewFunction(func(L *lua.LState) int {
		switch item := ctx.SelectedItem.(type) {
		case uicontext.SelectedRevision:
			L.Push(lua.LString(item.ChangeId))
			return 1
		case uicontext.SelectedFile:
			L.Push(lua.LString(item.ChangeId))
			return 1
		}
		return 0
	}))
	contextTable.RawSetString("commit_id", L.NewFunction(func(L *lua.LState) int {
		switch item := ctx.SelectedItem.(type) {
		case uicontext.SelectedRevision:
			L.Push(lua.LString(item.CommitId))
			return 1
		case uicontext.SelectedFile:
			L.Push(lua.LString(item.CommitId))
			return 1
		case uicontext.SelectedCommit:
			L.Push(lua.LString(item.CommitId))
			return 1
		}
		return 0
	}))
	contextTable.RawSetString("file", L.NewFunction(func(L *lua.LState) int {
		if item, ok := ctx.SelectedItem.(uicontext.SelectedFile); ok {
			L.Push(lua.LString(item.File))
			return 1
		}
		return 0
	}))
	contextTable.RawSetString("operation_id", L.NewFunction(func(L *lua.LState) int {
		if item, ok := ctx.SelectedItem.(uicontext.SelectedOperation); ok {
			L.Push(lua.LString(item.OperationId))
			return 1
		}
		return 0
	}))
	contextTable.RawSetString("checked_files", L.NewFunction(func(L *lua.LState) int {
		tbl := L.NewTable()
		for _, item := range ctx.CheckedItems {
			if f, ok := item.(uicontext.SelectedFile); ok {
				tbl.Append(lua.LString(f.File))
			}
		}
		L.Push(tbl)
		return 1
	}))
	contextTable.RawSetString("checked_change_ids", L.NewFunction(func(L *lua.LState) int {
		tbl := L.NewTable()
		for _, item := range ctx.CheckedItems {
			switch i := item.(type) {
			case uicontext.SelectedRevision:
				tbl.Append(lua.LString(i.ChangeId))
			case uicontext.SelectedFile:
				tbl.Append(lua.LString(i.ChangeId))
			}
		}
		L.Push(tbl)
		return 1
	}))
	contextTable.RawSetString("checked_commit_ids", L.NewFunction(func(L *lua.LState) int {
		tbl := L.NewTable()
		for _, item := range ctx.CheckedItems {
			switch i := item.(type) {
			case uicontext.SelectedRevision:
				tbl.Append(lua.LString(i.CommitId))
			case uicontext.SelectedFile:
				tbl.Append(lua.LString(i.CommitId))
			case uicontext.SelectedCommit:
				tbl.Append(lua.LString(i.CommitId))
			}
		}
		L.Push(tbl)
		return 1
	}))

	jjAsyncFn := L.NewFunction(func(L *lua.LState) int {
		args := argsFromLua(L)
		return yieldStep(L, step{cmd: ctx.RunCommand(args)})
	})
	jjInteractiveFn := L.NewFunction(func(L *lua.LState) int {
		args := argsFromLua(L)
		return yieldStep(L, step{cmd: ctx.RunInteractiveCommand(args, nil)})
	})
	jjFn := L.NewFunction(func(L *lua.LState) int {
		args := argsFromLua(L)
		out, err := ctx.RunCommandImmediate(args)
		if err != nil {
			L.Push(lua.LNil)
			L.Push(lua.LString(err.Error()))
			return 2
		}
		L.Push(lua.LString(out))
		L.Push(lua.LNil)
		return 2
	})
	flashFn := L.NewFunction(func(L *lua.LState) int {
		intent := intents.AddMessage{}
		switch v := L.Get(1).(type) {
		case *lua.LTable:
			payload := luaTableToMap(v)
			intent.Text = stringVal(payload, "text")
			if boolVal(payload, "error") {
				intent.Err = fmt.Errorf("%s", intent.Text)
			}
			intent.Sticky = boolVal(payload, "sticky")
		default:
			intent.Text = L.CheckString(1)
		}
		return yieldStep(L, step{cmd: intents.Invoke(intent)})
	})
	setThemeFn := L.NewFunction(func(L *lua.LState) int {
		name := L.CheckString(1)
		return yieldStep(L, step{cmd: intents.Invoke(intents.ChangeTheme{Name: name})})
	})
	copyToClipboardFn := L.NewFunction(func(L *lua.LState) int {
		text := L.CheckString(1)
		if err := clipboard.WriteAll(text); err != nil {
			L.Push(lua.LNil)
			L.Push(lua.LString(err.Error()))
			return 2
		}
		L.Push(lua.LBool(true))
		L.Push(lua.LNil)
		return 2
	})
	execShellFn := L.NewFunction(func(L *lua.LState) int {
		command := L.CheckString(1)
		msg := common.ExecMsg{
			Line: command,
			Mode: common.ExecShell,
		}
		return yieldStep(L, step{cmd: exec_process.ExecLine(ctx, msg)})
	})
	splitLinesFn := L.NewFunction(func(L *lua.LState) int {
		text := L.CheckString(1)
		keepEmpty := false
		if L.GetTop() >= 2 {
			keepEmpty = L.CheckBool(2)
		}
		tbl := L.NewTable()
		for line := range strings.SplitSeq(text, "\n") {
			line = strings.TrimSuffix(line, "\r")
			if line == "" && !keepEmpty {
				continue
			}
			tbl.Append(lua.LString(line))
		}
		L.Push(tbl)
		return 1
	})
	chooseFn := L.NewFunction(func(L *lua.LState) int {
		var (
			options []string
			title   string
			filter  bool
			ordered bool
		)
		if L.GetTop() == 1 {
			if tbl, ok := L.Get(1).(*lua.LTable); ok {
				if optVal := tbl.RawGetString("options"); optVal != lua.LNil {
					if optTbl, ok := optVal.(*lua.LTable); ok {
						options = stringSliceFromTable(optTbl)
					} else if s, ok := optVal.(lua.LString); ok {
						options = []string{s.String()}
					}
				}
				if titleVal := tbl.RawGetString("title"); titleVal != lua.LNil {
					title = titleVal.String()
				}
				if filterVal := tbl.RawGetString("filter"); filterVal != lua.LNil {
					filter = bool(filterVal.(lua.LBool))
				}
				if orderedVal := tbl.RawGetString("ordered"); orderedVal != lua.LNil {
					ordered = bool(orderedVal.(lua.LBool))
				}
				if options == nil {
					options = stringSliceFromTable(tbl)
				}
				return yieldStep(L, step{cmd: choose.ShowOrdered(options, title, filter, ordered), matcher: matchChoose})
			}
		}
		options = argsFromLua(L)
		return yieldStep(L, step{cmd: choose.ShowWithTitle(options, "", false), matcher: matchChoose})
	})
	inputFn := L.NewFunction(func(L *lua.LState) int {
		var title, prompt, value string
		if L.GetTop() == 1 {
			if tbl, ok := L.Get(1).(*lua.LTable); ok {
				if titleVal := tbl.RawGetString("title"); titleVal != lua.LNil {
					title = titleVal.String()
				}
				if promptVal := tbl.RawGetString("prompt"); promptVal != lua.LNil {
					prompt = promptVal.String()
				}
				if valueVal := tbl.RawGetString("value"); valueVal != lua.LNil {
					value = valueVal.String()
				}
				return yieldStep(L, step{cmd: input.ShowWithTitle(title, prompt, value), matcher: matchInput})
			}
		}
		return yieldStep(L, step{cmd: input.ShowWithTitle("", "", ""), matcher: matchInput})
	})
	waitCloseFn := L.NewFunction(func(L *lua.LState) int {
		return yieldStep(L, step{matcher: matchCloseViewMsg})
	})
	waitRefreshFn := L.NewFunction(func(L *lua.LState) int {
		return yieldStep(L, step{matcher: matchUpdateRevisionsSuccess})
	})
	changeWsFn := L.NewFunction(func(L *lua.LState) int {
		path := L.CheckString(1)
		prev := ctx.Location
		ctx.ChangeWorkspace(path)
		out, err := ctx.RunCommandImmediate([]string{"root"})
		if err != nil {
			ctx.ChangeWorkspace(prev)
			L.Push(lua.LNil)
			L.Push(lua.LString("not a jj repo: " + path))
			return 2
		}
		ctx.ChangeWorkspace(strings.TrimSpace(string(out)))
		L.Push(lua.LBool(true))
		L.Push(lua.LNil)
		return 2
	})

	// make sure we have a `jjui` namespace
	root := L.NewTable()
	root.RawSetString("revisions", revisionsTable)
	root.RawSetString("revset", revsetTable)
	root.RawSetString("context", contextTable)
	root.RawSetString("jj_async", jjAsyncFn)
	root.RawSetString("jj_interactive", jjInteractiveFn)
	root.RawSetString("jj", jjFn)
	root.RawSetString("flash", flashFn)
	root.RawSetString("set_theme", setThemeFn)
	root.RawSetString("copy_to_clipboard", copyToClipboardFn)
	root.RawSetString("exec_shell", execShellFn)
	root.RawSetString("split_lines", splitLinesFn)
	root.RawSetString("choose", chooseFn)
	root.RawSetString("input", inputFn)
	root.RawSetString("wait_close", waitCloseFn)
	root.RawSetString("wait_refresh", waitRefreshFn)
	root.RawSetString("change_workspace", changeWsFn)
	builtinRoot := L.NewTable()
	root.RawSetString("builtin", builtinRoot)
	registerGeneratedActionAPI(L, root, false)
	registerGeneratedActionAPI(L, builtinRoot, true)
	L.SetGlobal("jjui", root)

	// but also expose at the top level for convenience
	L.SetGlobal("revisions", revisionsTable)
	L.SetGlobal("revset", revsetTable)
	if diffTable, ok := root.RawGetString("diff").(*lua.LTable); ok {
		L.SetGlobal("diff", diffTable)
	}
	if uiTable, ok := root.RawGetString("ui").(*lua.LTable); ok {
		L.SetGlobal("ui", uiTable)
	}
	L.SetGlobal("context", contextTable)
	L.SetGlobal("jj_async", jjAsyncFn)
	L.SetGlobal("jj_interactive", jjInteractiveFn)
	L.SetGlobal("jj", jjFn)
	L.SetGlobal("flash", flashFn)
	L.SetGlobal("set_theme", setThemeFn)
	L.SetGlobal("copy_to_clipboard", copyToClipboardFn)
	L.SetGlobal("exec_shell", execShellFn)
	L.SetGlobal("split_lines", splitLinesFn)
	L.SetGlobal("choose", chooseFn)
	L.SetGlobal("input", inputFn)
	L.SetGlobal("wait_close", waitCloseFn)
	L.SetGlobal("wait_refresh", waitRefreshFn)
	L.SetGlobal("change_workspace", changeWsFn)
}

func registerGeneratedActionAPI(L *lua.LState, root *lua.LTable, builtIn bool) {
	actions := actionmeta.BuiltInActions()
	for _, actionID := range actions {
		scopes := actionmeta.ActionScopes(actionID)
		for _, scope := range scopes {
			scopeTable := ensureScopeTable(L, root, scope)
			token := actionTokenFromCanonical(actionID)
			if token == "" {
				continue
			}
			// Keep existing utility helpers (e.g. jjui.revisions.refresh) intact.
			if scopeTable.RawGetString(token) != lua.LNil {
				continue
			}
			scopeTable.RawSetString(token, generatedActionFn(L, actionID, builtIn))
			if token == "cancel" && scopeTable.RawGetString("close") == lua.LNil {
				scopeTable.RawSetString("close", generatedActionFn(L, actionID, builtIn))
			}
		}
	}
}

func ensureScopeTable(L *lua.LState, root *lua.LTable, scope string) *lua.LTable {
	current := root
	for segment := range strings.SplitSeq(scope, ".") {
		existing := current.RawGetString(segment)
		if tbl, ok := existing.(*lua.LTable); ok {
			current = tbl
			continue
		}
		next := L.NewTable()
		current.RawSetString(segment, next)
		current = next
	}
	return current
}

func generatedActionFn(L *lua.LState, canonical string, builtIn bool) *lua.LFunction {
	var positionalKey string
	if required := actionmeta.ActionRequiredArgs(canonical); len(required) == 1 && actionmeta.ActionArgSchema(canonical)[required[0]] == "string" {
		positionalKey = required[0]
	}
	return L.NewFunction(func(L *lua.LState) int {
		var args map[string]any
		if positionalKey != "" && L.GetTop() >= 1 {
			if s, ok := L.Get(1).(lua.LString); ok {
				args = map[string]any{positionalKey: s.String()}
			} else {
				args = optionalLuaMapArg(L, 1)
			}
		} else {
			args = optionalLuaMapArg(L, 1)
		}
		completionID := strconv.FormatUint(nextActionCompletionID.Add(1), 10)
		return yieldStep(L, step{cmd: func() tea.Msg {
			return common.DispatchActionMsg{
				Action:       canonical,
				Args:         args,
				BuiltIn:      builtIn,
				CompletionID: completionID,
			}
		}, matcher: matchActionCompleted(completionID)})
	})
}

func optionalLuaMapArg(L *lua.LState, pos int) map[string]any {
	if L.GetTop() < pos || L.Get(pos) == lua.LNil {
		return nil
	}
	tbl, ok := L.Get(pos).(*lua.LTable)
	if !ok {
		L.ArgError(pos, "expected table or nil")
		return nil
	}
	return luaTableToMap(tbl)
}

func payloadFromTop(L *lua.LState) map[string]any {
	if L.GetTop() >= 1 && L.CheckAny(1) != lua.LNil {
		if tbl, ok := L.Get(1).(*lua.LTable); ok {
			return luaTableToMap(tbl)
		}
	}
	return map[string]any{}
}

func boolVal(payload map[string]any, key string) bool {
	if v, ok := payload[key]; ok {
		if b, ok := v.(bool); ok {
			return b
		}
	}
	return false
}

func intVal(payload map[string]any, key string) int {
	if v, ok := payload[key]; ok {
		switch n := v.(type) {
		case int:
			return n
		case int64:
			return int(n)
		case float64:
			return int(n)
		case float32:
			return int(n)
		}
	}
	return 0
}

func stringVal(payload map[string]any, key string) string {
	if v, ok := payload[key]; ok {
		if s, ok := v.(string); ok {
			return s
		}
	}
	return ""
}

func argsFromLua(L *lua.LState) []string {
	if L.GetTop() == 0 {
		return nil
	}
	if tbl, ok := L.Get(1).(*lua.LTable); ok {
		return stringSliceFromTable(tbl)
	}
	var out []string
	top := L.GetTop()
	for i := 1; i <= top; i++ {
		out = append(out, L.CheckString(i))
	}
	return out
}

func stringSliceFromTable(tbl *lua.LTable) []string {
	var out []string
	tbl.ForEach(func(_, value lua.LValue) {
		if s, ok := value.(lua.LString); ok {
			out = append(out, s.String())
		}
	})
	return out
}

func luaTableToMap(tbl *lua.LTable) map[string]any {
	result := map[string]any{}
	tbl.ForEach(func(key, value lua.LValue) {
		if key.Type() != lua.LTString {
			return
		}
		result[key.String()] = luaValueToGo(value)
	})
	return result
}

func luaTableToSlice(tbl *lua.LTable) []any {
	var result []any
	tbl.ForEach(func(_, value lua.LValue) {
		result = append(result, luaValueToGo(value))
	})
	return result
}

func luaValueToGo(value lua.LValue) any {
	switch value.Type() {
	case lua.LTBool:
		return bool(value.(lua.LBool))
	case lua.LTNumber:
		return float64(value.(lua.LNumber))
	case lua.LTString:
		return value.String()
	case lua.LTTable:
		t := value.(*lua.LTable)
		// Heuristic: if keys are string, convert to map; otherwise, slice.
		isMap := false
		t.ForEach(func(key, _ lua.LValue) {
			if key.Type() == lua.LTString {
				isMap = true
			}
		})
		if isMap {
			return luaTableToMap(t)
		}
		return luaTableToSlice(t)
	default:
		return nil
	}
}

func yieldStep(L *lua.LState, st step) int {
	ud := L.NewUserData()
	ud.Value = st
	return L.Yield(ud)
}

func boolPtr(v bool) *bool {
	return &v
}

func parseNavigateTarget(val string) intents.NavigationTarget {
	switch strings.ToLower(val) {
	case "parent":
		return intents.TargetParent
	case "child", "children":
		return intents.TargetChild
	case "working", "working_copy", "work":
		return intents.TargetWorkingCopy
	default:
		return intents.TargetNone
	}
}

func matchUpdateRevisionsSuccess(msg tea.Msg) (bool, []lua.LValue) {
	switch msg.(type) {
	case common.UpdateRevisionsSuccessMsg, common.UpdateRevisionsFailedMsg:
		return true, nil
	default:
		return false, nil
	}
}

func matchCloseViewMsg(msg tea.Msg) (bool, []lua.LValue) {
	if closeMsg, ok := msg.(common.CloseViewMsg); ok {
		return true, []lua.LValue{lua.LBool(closeMsg.Applied)}
	}
	return false, nil
}

func matchActionCompleted(id string) func(tea.Msg) (bool, []lua.LValue) {
	return func(msg tea.Msg) (bool, []lua.LValue) {
		completed, ok := msg.(common.ActionCompletedMsg)
		return ok && completed.ID == id, nil
	}
}

func matchChoose(msg tea.Msg) (bool, []lua.LValue) {
	switch msg := msg.(type) {
	case choose.SelectedMsg:
		return true, []lua.LValue{lua.LString(msg.Value)}
	case choose.CancelledMsg:
		return true, []lua.LValue{lua.LNil}
	default:
		return false, nil
	}
}

func matchInput(msg tea.Msg) (bool, []lua.LValue) {
	switch msg := msg.(type) {
	case input.SelectedMsg:
		return true, []lua.LValue{lua.LString(msg.Value)}
	case input.CancelledMsg:
		return true, []lua.LValue{lua.LNil}
	default:
		return false, nil
	}
}

func actionTokenFromCanonical(actionID string) string {
	if idx := strings.LastIndexByte(actionID, '.'); idx >= 0 && idx < len(actionID)-1 {
		return actionID[idx+1:]
	}
	return actionID
}
