package cmdpacks

import (
	"context"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/pkg/errors"
	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
	"ucl.lmika.dev/repl"
	"ucl.lmika.dev/ucl"
)

type StandardCommands struct {
	TableService         *tables.Service
	State                *controllers.State
	ReadController       *controllers.TableReadController
	WriteController      *controllers.TableWriteController
	ExportController     *controllers.ExportController
	KeyBindingController *controllers.KeyBindingController
	PBProvider           services.PasteboardProvider
	SettingsController   *controllers.SettingsController
	ItemRenderer         *itemrenderer.Service

	modUI ucl.Module
}

func NewStandardCommands(
	tableService *tables.Service,
	state *controllers.State,
	readController *controllers.TableReadController,
	writeController *controllers.TableWriteController,
	exportController *controllers.ExportController,
	keyBindingController *controllers.KeyBindingController,
	pbProvider services.PasteboardProvider,
	settingsController *controllers.SettingsController,
	itemRenderer *itemrenderer.Service,
) StandardCommands {
	modUI, ckbs := moduleUI(tableService, state, readController, itemRenderer)
	keyBindingController.SetCustomKeyBindingSource(ckbs)

	return StandardCommands{
		TableService:         tableService,
		State:                state,
		ReadController:       readController,
		WriteController:      writeController,
		ExportController:     exportController,
		KeyBindingController: keyBindingController,
		PBProvider:           pbProvider,
		SettingsController:   settingsController,
		ItemRenderer:         itemRenderer,
		modUI:                modUI,
	}
}

var cmdQuitDoc = repl.Doc{
	Brief: "Quits dynamo-browse",
	Detailed: `
		This will quit dynamo-browse immediately, without prompting to apply
		any changes.
    `,
}

func (sc StandardCommands) cmdQuit(ctx context.Context, args ucl.CallArgs) (any, error) {
	commandctrl.PostMsg(ctx, tea.Quit)
	return nil, nil
}

var cmdTableDoc = repl.Doc{
	Brief: "Prompt for table to scan",
	Usage: "[NAME]",
	Args: []repl.ArgDoc{
		{Name: "name", Brief: "Name of the table to scan"},
	},
	Detailed: `
		If called with an argument, it will scan the table with that name and
		replace the current result set. If called without an argument, it will
		prompt for a table to scan.

		This command is intended only for interactive sessions and is not suitable
		for scripting. The scan or table prompts will happen asynchronously.
    `,
}

func (sc StandardCommands) cmdTable(ctx context.Context, args ucl.CallArgs) (any, error) {
	var tableName string
	if err := args.Bind(&tableName); err == nil {
		commandctrl.PostMsg(ctx, sc.ReadController.ScanTable(tableName))
		return nil, nil
	}

	commandctrl.PostMsg(ctx, sc.ReadController.ListTables(false))
	return nil, nil
}

var cmdExportDoc = repl.Doc{
	Brief: "Exports a result-set as CSV",
	Usage: "FILENAME [-all]",
	Args: []repl.ArgDoc{
		{Name: "filename", Brief: "Filename to export to"},
		{Name: "-all", Brief: "Export all results from the table"},
	},
	Detailed: `
		The fields of the current table view will be treated as the header of the
		exported table.

		This command is intended only for interactive sessions and is not suitable
		for scripting. The export will run asynchronously.
    `,
}

func (sc StandardCommands) cmdExport(ctx context.Context, args ucl.CallArgs) (any, error) {
	var filename string
	if err := args.Bind(&filename); err != nil {
		return nil, errors.New("expected filename")
	}

	opts := controllers.ExportOptions{
		AllResults: args.HasSwitch("all"),
	}

	commandctrl.PostMsg(ctx, sc.ExportController.ExportCSV(filename, opts))
	return nil, nil
}

var cmdMarkDoc = repl.Doc{
	Brief: "Set the marked items of the current result-set",
	Usage: "[WHAT] [-where EXPR]",
	Args: []repl.ArgDoc{
		{Name: "what", Brief: "Items to mark. Defaults to 'all'"},
		{Name: "-where", Brief: "Filter expression select items to mark"},
	},
	Detailed: `
		WHAT can be one of:

		- all: Mark all items in the current result-set
		- none: Unmark all items in the current result-set
		- toggle: Toggle the marked state of all items in the current result-set
    `,
}

func (sc StandardCommands) cmdMark(ctx context.Context, args ucl.CallArgs) (any, error) {
	var markOp = controllers.MarkOpMark

	if args.NArgs() > 0 {
		var markOpStr string
		if err := args.Bind(&markOpStr); err == nil {
			switch markOpStr {
			case "all":
				markOp = controllers.MarkOpMark
			case "none":
				markOp = controllers.MarkOpUnmark
			case "toggle":
				markOp = controllers.MarkOpToggle
			default:
				return nil, errors.New("unrecognised mark operation")
			}
		}
	}

	var whereExpr = ""
	if args.HasSwitch("where") {
		if err := args.BindSwitch("where", &whereExpr); err != nil {
			return nil, err
		}
	}

	commandctrl.PostMsg(ctx, sc.ReadController.Mark(markOp, whereExpr))
	return nil, nil
}

var cmdNextPageDoc = repl.Doc{
	Brief: "Retrieve and display the next page of the current result-set",
	Detailed: `
		This command is intended only for interactive sessions and is not suitable
		for scripting. Fetching the next page will run asynchronously.
    `,
}

func (sc StandardCommands) cmdNextPage(ctx context.Context, args ucl.CallArgs) (any, error) {
	commandctrl.PostMsg(ctx, sc.ReadController.NextPage())
	return nil, nil
}

var cmdDeleteDoc = repl.Doc{
	Brief: "Delete the marked items of the current result-set",
	Detailed: `
		The user will be prompted to confirm the deletion. If approved, the
		items will be deleted immediately.

		This command is intended only for interactive sessions and is not suitable
		for scripting.
    `,
}

func (sc StandardCommands) cmdDelete(ctx context.Context, args ucl.CallArgs) (any, error) {
	commandctrl.PostMsg(ctx, sc.WriteController.DeleteMarked())
	return nil, nil
}

var cmdNewItemDoc = repl.Doc{
	Brief: "Adds a new item to the current result-set",
	Detailed: `
		The user will be prompted to enter the values for each required attribute,
		such as the partition and sort key. The new item will be commited to the database
		upon the next write.

		This command is intended only for interactive sessions and is not suitable
		for scripting.
    `,
}

func (sc StandardCommands) cmdNewItem(ctx context.Context, args ucl.CallArgs) (any, error) {
	commandctrl.PostMsg(ctx, sc.WriteController.NewItem())
	return nil, nil
}

var cmdCloneDoc = repl.Doc{
	Brief: "Adds a copy of the selected item as a new item to the current result-set",
	Detailed: `
		The user will be prompted to enter the partition and sort key. All other
		attributes will be cloned from the selected item. The new item will be
		commited to the database upon the next write.

		This command is intended only for interactive sessions and is not suitable
		for scripting.
    `,
}

func (sc StandardCommands) cmdClone(ctx context.Context, args ucl.CallArgs) (any, error) {
	selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx)
	if !ok {
		return nil, errors.New("no item selected")
	}

	commandctrl.PostMsg(ctx, sc.WriteController.CloneItem(selectedItemIndex))
	return nil, nil
}

var cmdSetAttrDoc = repl.Doc{
	Brief: "Modify a field value of the selected or marked items",
	Usage: "ATTR [TYPE]",
	Args: []repl.ArgDoc{
		{Name: "attr", Brief: "Attribute to modify"},
		{Name: "-S", Brief: "Set attribute to a string"},
		{Name: "-N", Brief: "Set attribute to a number"},
		{Name: "-BOOL", Brief: "Set attribute to a boolean"},
		{Name: "-NULL", Brief: "Set attribute to a null"},
		{Name: "-TO", Brief: "Set attribute to the result of an expression"},
	},
	Detailed: `
		The user will be prompted to enter the new value for the attribute.
		If the attribute type is not known, then a type will need to be specified.
		Otherwise, the type will be unchanged. The modified item will be
		commited to the database upon the next write.
    `,
}

func (sc StandardCommands) cmdSetAttr(ctx context.Context, args ucl.CallArgs) (any, error) {
	var fieldName string
	if err := args.Bind(&fieldName); err != nil {
		return nil, errors.New("expected field name")
	}

	selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx)
	if !ok {
		return nil, errors.New("no item selected")
	}

	var itemType = models.UnsetItemType
	switch {
	case args.HasSwitch("S"):
		itemType = models.StringItemType
	case args.HasSwitch("N"):
		itemType = models.NumberItemType
	case args.HasSwitch("BOOL"):
		itemType = models.BoolItemType
	case args.HasSwitch("NULL"):
		itemType = models.NullItemType
	case args.HasSwitch("TO"):
		itemType = models.ExprValueItemType
	}

	commandctrl.PostMsg(ctx, sc.WriteController.SetAttributeValue(selectedItemIndex, itemType, fieldName))
	return nil, nil
}

var cmdDelAttrDoc = repl.Doc{
	Brief: "Remove the field of the selected or marked items",
	Usage: "ATTR",
	Args: []repl.ArgDoc{
		{Name: "attr", Brief: "Attribute to remove"},
	},
	Detailed: `
		The modified item will be commited to the database upon the next write.
    `,
}

func (sc StandardCommands) cmdDelAttr(ctx context.Context, args ucl.CallArgs) (any, error) {
	var fieldName string
	if err := args.Bind(&fieldName); err != nil {
		return nil, errors.New("expected field name")
	}

	selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx)
	if !ok {
		return nil, errors.New("no item selected")
	}

	commandctrl.PostMsg(ctx, sc.WriteController.DeleteAttribute(selectedItemIndex, fieldName))
	return nil, nil
}

var cmdPutDoc = repl.Doc{
	Brief: "Commit changes to the table",
	Detailed: `
		This will put all new and modified items.

		This command is intended only for interactive sessions and is not suitable
		for scripting. The user will be prompted to confirm the changes.
    `,
}

func (sc StandardCommands) cmdPut(ctx context.Context, args ucl.CallArgs) (any, error) {
	commandctrl.PostMsg(ctx, sc.WriteController.PutItems())
	return nil, nil
}

var cmdTouchDoc = repl.Doc{
	Brief: "Put the currently selected item",
	Detailed: `
		This will put the currently selected item, regardless of whether it has been
		modified.

		This command is intended only for interactive sessions and is not suitable
		for scripting. The user will be prompted to confirm the touch.
    `,
}

func (sc StandardCommands) cmdTouch(ctx context.Context, args ucl.CallArgs) (any, error) {
	selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx)
	if !ok {
		return nil, errors.New("no item selected")
	}

	commandctrl.PostMsg(ctx, sc.WriteController.TouchItem(selectedItemIndex))
	return nil, nil
}

var cmdNoisyTouchDoc = repl.Doc{
	Brief: "Put the currently selected item by deleting it first",
	Detailed: `
		This will put the currently selected item, regardless of whether it has been
		modified. It does so by removing the item from the table, then adding it back again.

		This command is intended only for interactive sessions and is not suitable
		for scripting. The user will be prompted to confirm the touch.
    `,
}

func (sc StandardCommands) cmdNoisyTouch(ctx context.Context, args ucl.CallArgs) (any, error) {
	selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx)
	if !ok {
		return nil, errors.New("no item selected")
	}

	commandctrl.PostMsg(ctx, sc.WriteController.NoisyTouchItem(selectedItemIndex))
	return nil, nil
}

var cmdRebindDoc = repl.Doc{
	Brief: "Binds a new key to an action",
	Usage: "ACTION KEY",
	Args: []repl.ArgDoc{
		{Name: "action", Brief: "Action to bind"},
		{Name: "key", Brief: "Key to bind"},
	},
	Detailed: `
		If the key is already bound to an action, it will be replaced.
		The set of actions this command accepts is well-defined. For binding
		to arbitrary actions, use the ui:bind command.
    `,
}

func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (any, error) {
	var bindingName, newKey string
	if err := args.Bind(&bindingName, &newKey); err != nil {
		return nil, errors.New("expected: bindingName newKey")
	}

	// TODO: should only force if not interactive
	commandctrl.PostMsg(ctx, sc.KeyBindingController.Rebind(bindingName, newKey, false))
	return nil, nil
}

func (sc StandardCommands) InstOptions() []ucl.InstOption {
	return []ucl.InstOption{
		ucl.WithModule(moduleRS(sc.TableService, sc.State)),
		ucl.WithModule(sc.modUI),
		ucl.WithModule(modulePB(sc.PBProvider)),
		ucl.WithModule(moduleOpt(sc.SettingsController)),
		ucl.WithModule(moduleAttrValue()),
		ucl.WithModule(moduleAsync(sc.TableService, sc.State)),
	}
}

func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) {
	ucl.SetBuiltin("quit", sc.cmdQuit)
	ucl.SetBuiltin("table", sc.cmdTable)
	ucl.SetBuiltin("export", sc.cmdExport)
	ucl.SetBuiltin("mark", sc.cmdMark)
	ucl.SetBuiltin("next-page", sc.cmdNextPage)
	ucl.SetBuiltin("delete", sc.cmdDelete)
	ucl.SetBuiltin("new-item", sc.cmdNewItem)
	ucl.SetBuiltin("clone", sc.cmdClone)
	ucl.SetBuiltin("set-attr", sc.cmdSetAttr)
	ucl.SetBuiltin("del-attr", sc.cmdDelAttr)
	ucl.SetBuiltin("put", sc.cmdPut)
	ucl.SetBuiltin("touch", sc.cmdTouch)
	ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch)
	ucl.SetBuiltin("rebind", sc.cmdRebind)

	// Aliases
	ucl.SetBuiltin("sa", sc.cmdSetAttr)
	ucl.SetBuiltin("da", sc.cmdDelAttr)
	ucl.SetBuiltin("np", sc.cmdNextPage)
	ucl.SetBuiltin("w", sc.cmdPut)
	ucl.SetBuiltin("q", sc.cmdQuit)

	ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController})
	ucl.SetPseudoVar("table", tablePVar{sc.State, sc.TableService, sc.ReadController})
	ucl.SetPseudoVar("item", itemPVar{sc.State})
	ucl.SetPseudoVar("marked", markedSetPVar{sc.State, sc.ReadController})
}

func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error {
	_, err := ucl.EvalString(ctx, uclPrelude)
	return err
}

const uclPrelude = `
ui:command unmark { mark none }
ui:command set-opt { |n k| opt:set $n $k }

ui:bind "view.toggle-marked-items" "M" {
  markedCount = len @marked
  if (eq $markedCount (len @resultset)) {
    mark none
  } else {
	mark all
  }
}
`
