package plugin_ui

import (
	"fmt"
	"seanime/internal/util/result"

	"github.com/dop251/goja"
	"github.com/goccy/go-json"
	"github.com/google/uuid"
)

const (
	MaxActionsPerType = 3 // A plugin can only at most X actions of a certain type
)

// ActionManager
//
// Actions are buttons, dropdown items, and context menu items that are displayed in certain places in the UI.
// They are defined in the plugin code and are used to trigger events.
//
// The ActionManager is responsible for registering, rendering, and handling events for actions.
type ActionManager struct {
	ctx *Context

	animePageButtons            *result.Map[string, *AnimePageButton]
	animePageDropdownItems      *result.Map[string, *AnimePageDropdownMenuItem]
	animeLibraryDropdownItems   *result.Map[string, *AnimeLibraryDropdownMenuItem]
	mangaPageButtons            *result.Map[string, *MangaPageButton]
	mediaCardContextMenuItems   *result.Map[string, *MediaCardContextMenuItem]
	episodeCardContextMenuItems *result.Map[string, *EpisodeCardContextMenuItem]
	episodeGridItemMenuItems    *result.Map[string, *EpisodeGridItemMenuItem]
}

type BaseActionProps struct {
	ID    string            `json:"id"`
	Label string            `json:"label"`
	Style map[string]string `json:"style,omitempty"`
}

// Base action struct that all action types embed
type BaseAction struct {
	BaseActionProps
}

// GetProps returns the base action properties
func (a *BaseAction) GetProps() BaseActionProps {
	return a.BaseActionProps
}

// SetProps sets the base action properties
func (a *BaseAction) SetProps(props BaseActionProps) {
	a.BaseActionProps = props
}

// UnmountAll unmounts all actions
// It should be called when the plugin is unloaded
func (a *ActionManager) UnmountAll() {

	if a.animePageButtons.ClearN() > 0 {
		a.renderAnimePageButtons()
	}
	if a.animePageDropdownItems.ClearN() > 0 {
		a.renderAnimePageDropdownItems()
	}
	if a.animeLibraryDropdownItems.ClearN() > 0 {
		a.renderAnimeLibraryDropdownItems()
	}
	if a.mangaPageButtons.ClearN() > 0 {
		a.renderMangaPageButtons()
	}
	if a.mediaCardContextMenuItems.ClearN() > 0 {
		a.renderMediaCardContextMenuItems()
	}
	if a.episodeCardContextMenuItems.ClearN() > 0 {
		a.renderEpisodeCardContextMenuItems()
	}
	if a.episodeGridItemMenuItems.ClearN() > 0 {
		a.renderEpisodeGridItemMenuItems()
	}
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type AnimePageButton struct {
	BaseAction
	Intent string `json:"intent,omitempty"`
}

func (a *AnimePageButton) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)

	_ = obj.Set("setIntent", func(intent string) {
		a.Intent = intent
	})

	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type EpisodeCardContextMenuItem struct {
	BaseAction
}

func (a *EpisodeCardContextMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type EpisodeGridItemMenuItem struct {
	BaseAction
	Type string `json:"type"`
}

func (a *EpisodeGridItemMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type MangaPageButton struct {
	BaseAction
	Intent string `json:"intent,omitempty"`
}

func (a *MangaPageButton) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)

	_ = obj.Set("setIntent", func(intent string) {
		a.Intent = intent
	})

	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type AnimePageDropdownMenuItem struct {
	BaseAction
}

func (a *AnimePageDropdownMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type AnimeLibraryDropdownMenuItem struct {
	BaseAction
}

func (a *AnimeLibraryDropdownMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

type MediaCardContextMenuItemFor string

const (
	MediaCardContextMenuItemForAnime MediaCardContextMenuItemFor = "anime"
	MediaCardContextMenuItemForManga MediaCardContextMenuItemFor = "manga"
	MediaCardContextMenuItemForBoth  MediaCardContextMenuItemFor = "both"
)

type MediaCardContextMenuItem struct {
	BaseAction
	For MediaCardContextMenuItemFor `json:"for"` // anime, manga, both
}

func (a *MediaCardContextMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
	obj := actionManager.ctx.vm.NewObject()
	actionManager.bindSharedToObject(obj, a)

	_ = obj.Set("setFor", func(_for MediaCardContextMenuItemFor) {
		a.For = _for
	})

	return obj
}

// ///////////////////////////////////////////////////////////////////////////////////////////////////////////

func NewActionManager(ctx *Context) *ActionManager {
	return &ActionManager{
		ctx: ctx,

		animePageButtons:            result.NewResultMap[string, *AnimePageButton](),
		animeLibraryDropdownItems:   result.NewResultMap[string, *AnimeLibraryDropdownMenuItem](),
		animePageDropdownItems:      result.NewResultMap[string, *AnimePageDropdownMenuItem](),
		mangaPageButtons:            result.NewResultMap[string, *MangaPageButton](),
		mediaCardContextMenuItems:   result.NewResultMap[string, *MediaCardContextMenuItem](),
		episodeCardContextMenuItems: result.NewResultMap[string, *EpisodeCardContextMenuItem](),
		episodeGridItemMenuItems:    result.NewResultMap[string, *EpisodeGridItemMenuItem](),
	}
}

// renderAnimePageButtons is called when the client requests the buttons to display on the anime page.
func (a *ActionManager) renderAnimePageButtons() {
	buttons := make([]*AnimePageButton, 0)
	a.animePageButtons.Range(func(key string, value *AnimePageButton) bool {
		buttons = append(buttons, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderAnimePageButtonsEvent, ServerActionRenderAnimePageButtonsEventPayload{
		Buttons: buttons,
	})
}

func (a *ActionManager) renderAnimePageDropdownItems() {
	items := make([]*AnimePageDropdownMenuItem, 0)
	a.animePageDropdownItems.Range(func(key string, value *AnimePageDropdownMenuItem) bool {
		items = append(items, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderAnimePageDropdownItemsEvent, ServerActionRenderAnimePageDropdownItemsEventPayload{
		Items: items,
	})
}

func (a *ActionManager) renderAnimeLibraryDropdownItems() {
	items := make([]*AnimeLibraryDropdownMenuItem, 0)
	a.animeLibraryDropdownItems.Range(func(key string, value *AnimeLibraryDropdownMenuItem) bool {
		items = append(items, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderAnimeLibraryDropdownItemsEvent, ServerActionRenderAnimeLibraryDropdownItemsEventPayload{
		Items: items,
	})
}

func (a *ActionManager) renderMangaPageButtons() {
	buttons := make([]*MangaPageButton, 0)
	a.mangaPageButtons.Range(func(key string, value *MangaPageButton) bool {
		buttons = append(buttons, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderMangaPageButtonsEvent, ServerActionRenderMangaPageButtonsEventPayload{
		Buttons: buttons,
	})
}

func (a *ActionManager) renderMediaCardContextMenuItems() {
	items := make([]*MediaCardContextMenuItem, 0)
	a.mediaCardContextMenuItems.Range(func(key string, value *MediaCardContextMenuItem) bool {
		items = append(items, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderMediaCardContextMenuItemsEvent, ServerActionRenderMediaCardContextMenuItemsEventPayload{
		Items: items,
	})
}

func (a *ActionManager) renderEpisodeCardContextMenuItems() {
	items := make([]*EpisodeCardContextMenuItem, 0)
	a.episodeCardContextMenuItems.Range(func(key string, value *EpisodeCardContextMenuItem) bool {
		items = append(items, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderEpisodeCardContextMenuItemsEvent, ServerActionRenderEpisodeCardContextMenuItemsEventPayload{
		Items: items,
	})
}

func (a *ActionManager) renderEpisodeGridItemMenuItems() {
	items := make([]*EpisodeGridItemMenuItem, 0)
	a.episodeGridItemMenuItems.Range(func(key string, value *EpisodeGridItemMenuItem) bool {
		items = append(items, value)
		return true
	})

	a.ctx.SendEventToClient(ServerActionRenderEpisodeGridItemMenuItemsEvent, ServerActionRenderEpisodeGridItemMenuItemsEventPayload{
		Items: items,
	})
}

// bind binds 'action' to the ctx object
//
//	Example:
//	ctx.action.newAnimePageButton(...)
func (a *ActionManager) bind(ctxObj *goja.Object) {
	actionObj := a.ctx.vm.NewObject()
	_ = actionObj.Set("newAnimePageButton", a.jsNewAnimePageButton)
	_ = actionObj.Set("newAnimePageDropdownItem", a.jsNewAnimePageDropdownItem)
	_ = actionObj.Set("newAnimeLibraryDropdownItem", a.jsNewAnimeLibraryDropdownItem)
	_ = actionObj.Set("newMediaCardContextMenuItem", a.jsNewMediaCardContextMenuItem)
	_ = actionObj.Set("newMangaPageButton", a.jsNewMangaPageButton)
	_ = actionObj.Set("newEpisodeCardContextMenuItem", a.jsNewEpisodeCardContextMenuItem)
	_ = actionObj.Set("newEpisodeGridItemMenuItem", a.jsNewEpisodeGridItemMenuItem)
	_ = ctxObj.Set("action", actionObj)
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Actions
////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewEpisodeCardContextMenuItem
//
//	Example:
//	const downloadButton = ctx.newEpisodeCardContextMenuItem({
//		label: "Download",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewEpisodeCardContextMenuItem(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &EpisodeCardContextMenuItem{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewEpisodeGridItemMenuItem
//
//	Example:
//	const downloadButton = ctx.newEpisodeGridItemContextMenuItem({
//		label: "Download",
//		onClick: "download-button-clicked",
//		type: "library",
//	})
func (a *ActionManager) jsNewEpisodeGridItemMenuItem(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &EpisodeGridItemMenuItem{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewAnimePageButton
//
//	Example:
//	const downloadButton = ctx.newAnimePageButton({
//		label: "Download",
//		intent: "primary",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewAnimePageButton(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &AnimePageButton{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewAnimePageDropdownItem
//
//	Example:
//	const downloadButton = ctx.newAnimePageDropdownItem({
//		label: "Download",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewAnimePageDropdownItem(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &AnimePageDropdownMenuItem{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewAnimeLibraryDropdownItem
//
//	Example:
//	const downloadButton = ctx.newAnimeLibraryDropdownItem({
//		label: "Download",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewAnimeLibraryDropdownItem(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &AnimeLibraryDropdownMenuItem{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewMediaCardContextMenuItem
//
//	Example:
//	const downloadButton = ctx.newMediaCardContextMenuItem({
//		label: "Download",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewMediaCardContextMenuItem(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &MediaCardContextMenuItem{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

// jsNewMangaPageButton
//
//	Example:
//	const downloadButton = ctx.newMangaPageButton({
//		label: "Download",
//		onClick: "download-button-clicked",
//	})
func (a *ActionManager) jsNewMangaPageButton(call goja.FunctionCall) goja.Value {
	// Create a new action
	action := &MangaPageButton{}

	// Get the props
	a.unmarshalProps(call, action)
	action.ID = uuid.New().String()

	// Create the object
	obj := action.CreateObject(a)
	return obj
}

// ///////////////////////////////////////////////////////////////////////////////////
// Shared
// ///////////////////////////////////////////////////////////////////////////////////
// bindSharedToObject binds shared methods to action objects
//
//	Example:
//	const downloadButton = ctx.newAnimePageButton(...)
//	downloadButton.mount()
//	downloadButton.unmount()
//	downloadButton.setLabel("Downloading...")
func (a *ActionManager) bindSharedToObject(obj *goja.Object, action interface{}) {
	var id string
	var props BaseActionProps
	var mapToUse interface{}

	switch act := action.(type) {
	case *AnimePageButton:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.animePageButtons
	case *MangaPageButton:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.mangaPageButtons
	case *AnimePageDropdownMenuItem:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.animePageDropdownItems
	case *AnimeLibraryDropdownMenuItem:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.animeLibraryDropdownItems
	case *MediaCardContextMenuItem:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.mediaCardContextMenuItems
	case *EpisodeCardContextMenuItem:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.episodeCardContextMenuItems
	case *EpisodeGridItemMenuItem:
		id = act.ID
		props = act.GetProps()
		mapToUse = a.episodeGridItemMenuItems
	}

	_ = obj.Set("mount", func() {
		switch m := mapToUse.(type) {
		case *result.Map[string, *AnimePageButton]:
			if btn, ok := action.(*AnimePageButton); ok {
				m.Set(id, btn)
				a.renderAnimePageButtons()
			}
		case *result.Map[string, *MangaPageButton]:
			if btn, ok := action.(*MangaPageButton); ok {
				m.Set(id, btn)
				a.renderMangaPageButtons()
			}
		case *result.Map[string, *AnimePageDropdownMenuItem]:
			if item, ok := action.(*AnimePageDropdownMenuItem); ok {
				m.Set(id, item)
				a.renderAnimePageDropdownItems()
			}
		case *result.Map[string, *AnimeLibraryDropdownMenuItem]:
			if item, ok := action.(*AnimeLibraryDropdownMenuItem); ok {
				m.Set(id, item)
				a.renderAnimeLibraryDropdownItems()
			}
		case *result.Map[string, *MediaCardContextMenuItem]:
			if item, ok := action.(*MediaCardContextMenuItem); ok {
				if item.For == "" {
					item.For = MediaCardContextMenuItemForBoth
				}
				m.Set(id, item)
				a.renderMediaCardContextMenuItems()
			}
		case *result.Map[string, *EpisodeCardContextMenuItem]:
			if item, ok := action.(*EpisodeCardContextMenuItem); ok {
				m.Set(id, item)
				a.renderEpisodeCardContextMenuItems()
			}
		case *result.Map[string, *EpisodeGridItemMenuItem]:
			if item, ok := action.(*EpisodeGridItemMenuItem); ok {
				m.Set(id, item)
				a.renderEpisodeGridItemMenuItems()
			}
		}
	})

	_ = obj.Set("unmount", func() {
		switch m := mapToUse.(type) {
		case *result.Map[string, *AnimePageButton]:
			m.Delete(id)
			a.renderAnimePageButtons()
		case *result.Map[string, *MangaPageButton]:
			m.Delete(id)
			a.renderMangaPageButtons()
		case *result.Map[string, *AnimePageDropdownMenuItem]:
			m.Delete(id)
			a.renderAnimePageDropdownItems()
		case *result.Map[string, *AnimeLibraryDropdownMenuItem]:
			m.Delete(id)
			a.renderAnimeLibraryDropdownItems()
		case *result.Map[string, *MediaCardContextMenuItem]:
			m.Delete(id)
			a.renderMediaCardContextMenuItems()
		case *result.Map[string, *EpisodeCardContextMenuItem]:
			m.Delete(id)
			a.renderEpisodeCardContextMenuItems()
		case *result.Map[string, *EpisodeGridItemMenuItem]:
			m.Delete(id)
			a.renderEpisodeGridItemMenuItems()
		}
	})

	_ = obj.Set("setLabel", func(label string) {
		newProps := props
		newProps.Label = label

		switch act := action.(type) {
		case *AnimePageButton:
			act.SetProps(newProps)
		case *MangaPageButton:
			act.SetProps(newProps)
		case *AnimePageDropdownMenuItem:
			act.SetProps(newProps)
		case *AnimeLibraryDropdownMenuItem:
			act.SetProps(newProps)
		case *MediaCardContextMenuItem:
			act.SetProps(newProps)
		case *EpisodeCardContextMenuItem:
			act.SetProps(newProps)
		case *EpisodeGridItemMenuItem:
			act.SetProps(newProps)
		}

	})

	_ = obj.Set("setStyle", func(style map[string]string) {
		newProps := props
		newProps.Style = style

		switch act := action.(type) {
		case *AnimePageButton:
			act.SetProps(newProps)
			a.renderAnimePageButtons()
		case *MangaPageButton:
			act.SetProps(newProps)
			a.renderMangaPageButtons()
		case *AnimePageDropdownMenuItem:
			act.SetProps(newProps)
			a.renderAnimePageDropdownItems()
		case *AnimeLibraryDropdownMenuItem:
			act.SetProps(newProps)
			a.renderAnimeLibraryDropdownItems()
		case *MediaCardContextMenuItem:
			act.SetProps(newProps)
			a.renderMediaCardContextMenuItems()
		case *EpisodeCardContextMenuItem:
			act.SetProps(newProps)
			a.renderEpisodeCardContextMenuItems()
		case *EpisodeGridItemMenuItem:
			act.SetProps(newProps)
		}
	})

	_ = obj.Set("onClick", func(call goja.FunctionCall) goja.Value {
		if len(call.Arguments) < 1 {
			a.ctx.handleTypeError("onClick requires a callback function")
		}

		callback, ok := goja.AssertFunction(call.Argument(0))
		if !ok {
			a.ctx.handleTypeError("onClick requires a callback function")
		}

		eventListener := a.ctx.RegisterEventListener(ClientActionClickedEvent)

		eventListener.SetCallback(func(event *ClientPluginEvent) {
			payload := ClientActionClickedEventPayload{}
			if event.ParsePayloadAs(ClientActionClickedEvent, &payload) && payload.ActionID == id {
				a.ctx.scheduler.ScheduleAsync(func() error {
					_, err := callback(goja.Undefined(), a.ctx.vm.ToValue(payload.Event))
					return err
				})
			}
		})

		return goja.Undefined()
	})
}

/////////////////////////////////////////////////////////////////////////////////////
// Utils
/////////////////////////////////////////////////////////////////////////////////////

func (a *ActionManager) unmarshalProps(call goja.FunctionCall, ret interface{}) {
	if len(call.Arguments) < 1 {
		a.ctx.handleException(fmt.Errorf("expected 1 argument"))
	}

	props := call.Arguments[0].Export()
	if props == nil {
		a.ctx.handleException(fmt.Errorf("expected props object"))
	}

	marshaled, err := json.Marshal(props)
	if err != nil {
		a.ctx.handleException(err)
	}

	err = json.Unmarshal(marshaled, ret)
	if err != nil {
		a.ctx.handleException(err)
	}
}
