// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package ui

import (
	"log/slog"
	"os"
	"sync"

	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/config"
	"github.com/derailed/k9s/internal/model"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/tcell/v2"
	"github.com/derailed/tview"
)

// App represents an application.
type App struct {
	*tview.Application
	Configurator

	Main    *Pages
	flash   *model.Flash
	actions *KeyActions
	views   map[string]tview.Primitive
	cmdBuff *model.FishBuff
	running bool
	mx      sync.RWMutex
}

// NewApp returns a new app.
func NewApp(cfg *config.Config, _ string) *App {
	a := App{
		Application:  tview.NewApplication(),
		actions:      NewKeyActions(),
		Configurator: Configurator{Config: cfg, Styles: config.NewStyles()},
		Main:         NewPages(),
		flash:        model.NewFlash(model.DefaultFlashDelay),
		cmdBuff:      model.NewFishBuff(':', model.CommandBuffer),
	}

	a.views = map[string]tview.Primitive{
		"menu":   NewMenu(a.Styles),
		"logo":   NewLogo(a.Styles),
		"prompt": NewPrompt(&a, a.Config.K9s.UI.NoIcons, a.Styles),
		"crumbs": NewCrumbs(a.Styles),
	}

	return &a
}

// Init initializes the application.
func (a *App) Init() {
	a.bindKeys()
	a.Prompt().SetModel(a.cmdBuff)
	a.cmdBuff.AddListener(a)
	a.Styles.AddListener(a)

	a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.UI.EnableMouse)
}

// QueueUpdate queues up a ui action.
func (a *App) QueueUpdate(f func()) {
	if a.Application == nil {
		return
	}
	go func() {
		a.Application.QueueUpdate(f)
	}()
}

// QueueUpdateDraw queues up a ui action and redraw the ui.
func (a *App) QueueUpdateDraw(f func()) {
	if a.Application == nil {
		return
	}
	go func() {
		a.Application.QueueUpdateDraw(f)
	}()
}

// IsRunning checks if app is actually running.
func (a *App) IsRunning() bool {
	a.mx.RLock()
	defer a.mx.RUnlock()
	return a.running
}

// SetRunning sets the app run state.
func (a *App) SetRunning(f bool) {
	a.mx.Lock()
	defer a.mx.Unlock()
	a.running = f
}

// BufferCompleted indicates input was accepted.
func (*App) BufferCompleted(_, _ string) {}

// BufferChanged indicates the buffer was changed.
func (*App) BufferChanged(_, _ string) {}

// BufferActive indicates the buff activity changed.
func (a *App) BufferActive(state bool, _ model.BufferKind) {
	flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
	if !ok {
		return
	}

	if state && flex.ItemAt(1) != a.Prompt() {
		flex.AddItemAtIndex(1, a.Prompt(), 3, 1, false)
	} else if !state && flex.ItemAt(1) == a.Prompt() {
		flex.RemoveItemAtIndex(1)
		a.SetFocus(flex)
	}
}

// SuggestionChanged notifies of update to command suggestions.
func (*App) SuggestionChanged([]string) {}

// StylesChanged notifies the skin changed.
func (a *App) StylesChanged(s *config.Styles) {
	a.Main.SetBackgroundColor(s.BgColor())
	if f, ok := a.Main.GetPrimitive("main").(*tview.Flex); ok {
		f.SetBackgroundColor(s.BgColor())
		if !a.Config.K9s.IsHeadless() {
			if h, ok := f.ItemAt(0).(*tview.Flex); ok {
				h.SetBackgroundColor(s.BgColor())
			} else {
				slog.Warn("Header not found", slogs.Subsys, "styles", slogs.Component, "app")
			}
		}
	} else {
		slog.Error("Main panel not found", slogs.Subsys, "styles", slogs.Component, "app")
	}
}

// Conn returns an api server connection.
func (a *App) Conn() client.Connection {
	return a.Config.GetConnection()
}

func (a *App) bindKeys() {
	a.actions = NewKeyActionsFromMap(KeyMap{
		KeyColon:       NewKeyAction("Cmd", a.activateCmd, false),
		tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
		tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false),
		tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
		tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
	})
}

// BailOut exits the application.
func (a *App) BailOut(exitCode int) {
	if err := a.Config.Save(true); err != nil {
		slog.Error("Config save failed!", slogs.Error, err)
	}

	a.Stop()
	os.Exit(exitCode)
}

// ResetPrompt reset the prompt model and marks buffer as active.
func (a *App) ResetPrompt(m PromptModel) {
	m.ClearText(false)
	a.Prompt().SetModel(m)
	a.SetFocus(a.Prompt())
	m.SetActive(true)
}

// ResetCmd clear out user command.
func (a *App) ResetCmd() {
	a.cmdBuff.Reset()
}

func (a *App) saveCmd(*tcell.EventKey) *tcell.EventKey {
	if err := a.Config.Save(true); err != nil {
		a.Flash().Err(err)
	}
	a.Flash().Info("current context config saved")

	return nil
}

// ActivateCmd toggle command mode.
func (a *App) ActivateCmd(b bool) {
	a.cmdBuff.SetActive(b)
}

// GetCmd retrieves user command.
func (a *App) GetCmd() string {
	return a.cmdBuff.GetText()
}

// CmdBuff returns the app cmd model.
func (a *App) CmdBuff() *model.FishBuff {
	return a.cmdBuff
}

// HasCmd check if cmd buffer is active and has a command.
func (a *App) HasCmd() bool {
	return a.cmdBuff.IsActive() && !a.cmdBuff.Empty()
}

// InCmdMode check if command mode is active.
func (a *App) InCmdMode() bool {
	return a.Prompt().InCmdMode()
}

// HasAction checks if key matches a registered binding.
func (a *App) HasAction(key tcell.Key) (KeyAction, bool) {
	return a.actions.Get(key)
}

// GetActions returns a collection of actions.
func (a *App) GetActions() *KeyActions {
	return a.actions
}

// AddActions returns the application actions.
func (a *App) AddActions(aa *KeyActions) {
	a.actions.Merge(aa)
}

// Views return the application root views.
func (a *App) Views() map[string]tview.Primitive {
	return a.views
}

func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
	if !a.cmdBuff.IsActive() {
		return evt
	}
	a.cmdBuff.ClearText(true)

	return nil
}

func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
	if a.InCmdMode() {
		return evt
	}
	a.ResetPrompt(a.cmdBuff)
	a.cmdBuff.ClearText(true)

	return nil
}

// RedrawCmd forces a redraw.
func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
	a.QueueUpdateDraw(func() {})
	return evt
}

// View Accessors...

// Crumbs return app crumbs.
func (a *App) Crumbs() *Crumbs {
	return a.views["crumbs"].(*Crumbs)
}

// Logo return the app logo.
func (a *App) Logo() *Logo {
	return a.views["logo"].(*Logo)
}

// Prompt returns command prompt.
func (a *App) Prompt() *Prompt {
	return a.views["prompt"].(*Prompt)
}

// Menu returns app menu.
func (a *App) Menu() *Menu {
	return a.views["menu"].(*Menu)
}

// Flash returns a flash model.
func (a *App) Flash() *model.Flash {
	return a.flash
}

// ----------------------------------------------------------------------------
// Helpers...

// AsKey converts rune to keyboard key.
func AsKey(evt *tcell.EventKey) tcell.Key {
	if evt.Key() != tcell.KeyRune {
		return evt.Key()
	}
	key := tcell.Key(evt.Rune())
	if evt.Modifiers() == tcell.ModAlt {
		key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
	}
	return key
}
