package commandctrl

import (
	"context"
	"fmt"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/pkg/errors"
	"log"
	"os"
	"path/filepath"
	"strings"
	"ucl.lmika.dev/ucl"
	"ucl.lmika.dev/ucl/builtins"

	"github.com/lmika/dynamo-browse/internal/common/ui/events"
	"github.com/lmika/shellwords"
)

const commandsCategory = "commands"

type cmdMessage struct {
	cmd string
}

type CommandController struct {
	uclInst            *ucl.Inst
	historyProvider    IterProvider
	commandList        *CommandList
	lookupExtensions   []CommandLookupExtension
	completionProvider CommandCompletionProvider
	uiStateProvider    UIStateProvider
	cmdChan            chan cmdMessage
	msgChan            chan tea.Msg
	interactive        bool
}

func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController {
	cc := &CommandController{
		historyProvider:  historyProvider,
		commandList:      nil,
		lookupExtensions: nil,
		cmdChan:          make(chan cmdMessage),
		msgChan:          make(chan tea.Msg),
		interactive:      true,
	}

	options := []ucl.InstOption{
		ucl.WithOut(ucl.LineHandler(cc.printLine)),
		ucl.WithModule(builtins.OS()),
		ucl.WithModule(builtins.FS(nil)),
	}
	for _, pkg := range pkgs {
		options = append(options, pkg.InstOptions()...)
	}

	cc.uclInst = ucl.New(options...)

	for _, pkg := range pkgs {
		pkg.ConfigureUCL(cc.uclInst)
	}

	go cc.cmdLooper()

	return cc
}

func (c *CommandController) AddCommands(ctx *CommandList) {
	ctx.parent = c.commandList
	c.commandList = ctx
}

func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) {
	for msg := range c.msgChan {
		msgSender(msg)
	}
}

func (c *CommandController) SetUIStateProvider(provider UIStateProvider) {
	c.uiStateProvider = provider
}

func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) {
	c.lookupExtensions = append(c.lookupExtensions, ext)
}

func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) {
	c.completionProvider = provider
}

func (c *CommandController) Prompt() tea.Msg {
	return events.PromptForInputMsg{
		Prompt:  ":",
		History: c.historyProvider.Iter(context.Background(), commandsCategory),
		OnDone: func(value string) tea.Msg {
			return c.Execute(value)
		},
		// TEMP
		OnTabComplete: func(value string) (string, bool) {
			if c.completionProvider == nil {
				return "", false
			}

			if strings.HasPrefix(value, "sa ") || strings.HasPrefix(value, "da ") {
				tokens := shellwords.Split(strings.TrimSpace(value))
				lastToken := tokens[len(tokens)-1]

				options := c.completionProvider.AttributesWithPrefix(lastToken)
				if len(options) == 1 {
					return value[:len(value)-len(lastToken)] + options[0], true
				}
			}
			return "", false
		},
		// END TEMP
	}
}

func (c *CommandController) Execute(commandInput string) tea.Msg {
	return c.execute(ExecContext{FromFile: false}, commandInput)
}

func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Msg {
	input := strings.TrimSpace(commandInput)
	if input == "" {
		return nil
	}

	select {
	case c.cmdChan <- cmdMessage{cmd: input}:
		// good
	default:
		return events.Error(errors.New("command currently running"))
	}

	return nil
}

func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) {
	return c.uclInst.Eval(ctx, commandInput)
}

func (c *CommandController) cmdLooper() {
	ctx := context.WithValue(context.Background(), commandCtlKey, c)

	for {
		select {
		case cmdChan := <-c.cmdChan:
			res, err := c.ExecuteAndWait(ctx, cmdChan.cmd)
			if err != nil {
				c.postMessage(events.Error(err))
			} else if res != nil {
				c.postMessage(events.StatusMsg(fmt.Sprint(res)))
			}
		}
	}
}

func (c *CommandController) Alias(commandName string) Command {
	return func(ctx ExecContext, args ucl.CallArgs) tea.Msg {
		command := c.lookupCommand(commandName)
		if command == nil {
			return events.Error(errors.New("no such command: " + commandName))
		}

		return command(ctx, args)
	}
}

func (c *CommandController) lookupCommand(name string) Command {
	for ctx := c.commandList; ctx != nil; ctx = ctx.parent {
		if cmd, ok := ctx.Commands[name]; ok {
			return cmd
		}
	}

	for _, exts := range c.lookupExtensions {
		if cmd := exts.LookupCommand(name); cmd != nil {
			return cmd
		}
	}
	return nil
}

func (c *CommandController) ExecuteFile(filename string) error {
	oldInteractive := c.interactive
	c.interactive = false
	defer func() {
		c.interactive = oldInteractive
	}()

	baseFilename := filepath.Base(filename)

	if rcFile, err := os.ReadFile(filename); err == nil {
		if err := c.executeFile(rcFile, baseFilename); err != nil {
			return errors.Wrapf(err, "error executing %v", filename)
		}
	} else {
		return errors.Wrapf(err, "error loading %v", filename)
	}
	return nil
}

func (c *CommandController) executeFile(file []byte, filename string) error {
	//msg := c.execute(ExecContext{FromFile: true}, string(file))
	//switch m := msg.(type) {
	//case events.ErrorMsg:
	//	log.Printf("%v: error - %v", filename, m.Error())
	//case events.StatusMsg:
	//	log.Printf("%v: %v", filename, string(m))
	//}
	return nil
}

func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) {
	command := c.lookupCommand(name)
	if command == nil {
		return nil, errors.New("no such command: " + name)
	}

	res := command(ExecContext{}, args)
	if errMsg, isErrMsg := res.(events.ErrorMsg); isErrMsg {
		return nil, errMsg
	}
	return teaMsgWrapper{res}, nil
}

func (c *CommandController) printLine(s string) {
	if c.msgChan == nil || !c.interactive {
		log.Println(s)
		return
	}

	select {
	case c.msgChan <- events.StatusMsg(s):
	default:
		log.Println(s)
	}
}

func (c *CommandController) postMessage(msg tea.Msg) {
	if c.msgChan == nil {
		return
	}

	c.msgChan <- msg
}

type teaMsgWrapper struct {
	msg tea.Msg
}