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

package view

import (
	"context"
	"fmt"
	"log/slog"
	"strconv"
	"strings"

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/config"
	"github.com/derailed/k9s/internal/model"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/k9s/internal/ui"
	"github.com/derailed/k9s/internal/view/cmd"
	"github.com/derailed/tcell/v2"
	"github.com/derailed/tview"
	"github.com/sahilm/fuzzy"
	"k8s.io/apimachinery/pkg/labels"
)

const (
	liveViewTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
	yamlAction       = "YAML"
)

// LiveView represents a live text viewer.
type LiveView struct {
	*tview.Flex

	title                     string
	model                     model.ResourceViewer
	text                      *tview.TextView
	actions                   *ui.KeyActions
	app                       *App
	cmdBuff                   *model.FishBuff
	currentRegion, maxRegions int
	cancel                    context.CancelFunc
	fullScreen                bool
	managedField              bool
	autoRefresh               bool
}

// NewLiveView returns a live viewer.
func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView {
	v := LiveView{
		Flex:          tview.NewFlex(),
		text:          tview.NewTextView(),
		app:           app,
		title:         title,
		actions:       ui.NewKeyActions(),
		currentRegion: 0,
		maxRegions:    0,
		cmdBuff:       model.NewFishBuff('/', model.FilterBuffer),
		model:         m,
		autoRefresh:   app.Config.K9s.LiveViewAutoRefresh,
	}
	v.AddItem(v.text, 0, 1, true)

	return &v
}

func (*LiveView) SetCommand(*cmd.Interpreter)            {}
func (*LiveView) SetFilter(string, bool)                 {}
func (*LiveView) SetLabelSelector(labels.Selector, bool) {}

// Init initializes the viewer.
func (v *LiveView) Init(_ context.Context) error {
	if v.title != "" {
		v.SetBorder(true)
	}
	v.text.SetScrollable(true).SetWrap(true).SetRegions(true)
	v.text.SetDynamicColors(true)
	v.text.SetHighlightColor(tcell.ColorOrange)
	v.SetTitleColor(tcell.ColorAqua)
	v.SetInputCapture(v.keyboard)
	v.SetBorderPadding(0, 0, 1, 1)
	v.updateTitle()

	v.app.Styles.AddListener(v)
	v.StylesChanged(v.app.Styles)
	v.setFullScreen(v.app.Config.K9s.UI.DefaultsToFullScreen)

	v.app.Prompt().SetModel(v.cmdBuff)
	v.cmdBuff.AddListener(v)

	v.bindKeys()
	v.SetInputCapture(v.keyboard)
	if v.model != nil {
		v.model.AddListener(v)
	}

	return nil
}

// InCmdMode checks if prompt is active.
func (v *LiveView) InCmdMode() bool {
	return v.cmdBuff.InCmdMode()
}

// ResourceFailed notifies when there is an issue.
func (v *LiveView) ResourceFailed(err error) {
	v.text.SetTextAlign(tview.AlignCenter)
	x, _, w, _ := v.GetRect()
	v.text.SetText(cowTalk(err.Error(), x+w))
}

// ResourceChanged notifies when the filter changes.
func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
	v.app.QueueUpdateDraw(func() {
		v.text.SetTextAlign(tview.AlignLeft)
		v.currentRegion, v.maxRegions = 0, len(matches)

		if v.text.GetText(true) == "" {
			v.text.ScrollToBeginning()
		}

		lines = linesWithRegions(lines, matches)
		v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(lines, "\n")))
		v.text.Highlight()
		if v.currentRegion < v.maxRegions {
			v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
			v.text.ScrollToHighlight()
		}
		v.updateTitle()
	})
}

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

// BufferCompleted indicates input was accepted.
func (v *LiveView) BufferCompleted(text, _ string) {
	v.model.Filter(text)
}

// BufferActive indicates the buff activity changed.
func (v *LiveView) BufferActive(state bool, k model.BufferKind) {
	v.app.BufferActive(state, k)
}

func (v *LiveView) bindKeys() {
	v.actions.Bulk(ui.KeyMap{
		tcell.KeyEnter:  ui.NewSharedKeyAction("Filter", v.filterCmd, false),
		tcell.KeyEscape: ui.NewKeyAction("Back", v.resetCmd, false),
		ui.KeyQ:         ui.NewKeyAction("Back", v.resetCmd, false),
		tcell.KeyCtrlS:  ui.NewKeyAction("Save", v.saveCmd, false),
		ui.KeyC:         ui.NewKeyAction("Copy", cpCmd(v.app.Flash(), v.text), true),
		ui.KeyF:         ui.NewKeyAction("Toggle FullScreen", v.toggleFullScreenCmd, true),
		ui.KeyR:         ui.NewKeyAction("Toggle Auto-Refresh", v.toggleRefreshCmd, true),
		ui.KeyN:         ui.NewKeyAction("Next Match", v.nextCmd, true),
		ui.KeyShiftN:    ui.NewKeyAction("Prev Match", v.prevCmd, true),
		ui.KeySlash:     ui.NewSharedKeyAction("Filter Mode", v.activateCmd, false),
		tcell.KeyDelete: ui.NewSharedKeyAction("Erase", v.eraseCmd, false),
	})

	if !v.app.Config.IsReadOnly() {
		v.actions.Add(ui.KeyE, ui.NewKeyAction("Edit", v.editCmd, true))
	}
	if v.title == yamlAction {
		v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true))
	}
	if _, ok := v.model.(model.EncDecResourceViewer); ok {
		v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true))
	}
}

func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {
	m, ok := v.model.(model.EncDecResourceViewer)
	if !ok {
		return evt
	}

	m.Toggle()
	v.Start()
	return nil
}

func (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
	path := v.model.GetPath()
	if path == "" {
		return evt
	}
	v.Stop()
	defer v.Start()
	if err := editRes(v.app, v.model.GVR(), path); err != nil {
		v.app.Flash().Err(err)
	}

	return nil
}

// ToggleRefreshCmd is used for pausing the refreshing of data on config map and secrets.
func (v *LiveView) toggleRefreshCmd(*tcell.EventKey) *tcell.EventKey {
	v.autoRefresh = !v.autoRefresh
	if v.autoRefresh {
		v.Start()
		v.app.Flash().Info("Auto-refresh is enabled")
		return nil
	}
	v.Stop()
	v.app.Flash().Info("Auto-refresh is disabled")

	return nil
}

func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
	if a, ok := v.actions.Get(ui.AsKey(evt)); ok {
		return a.Action(evt)
	}

	return evt
}

// StylesChanged notifies the skin changed.
func (v *LiveView) StylesChanged(s *config.Styles) {
	v.SetBackgroundColor(s.BgColor())
	v.text.SetTextColor(s.FgColor())
	v.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())
}

// Actions returns menu actions.
func (v *LiveView) Actions() *ui.KeyActions {
	return v.actions
}

// Name returns the component name.
func (v *LiveView) Name() string { return v.title }

// Start starts the view updater.
func (v *LiveView) Start() {
	if v.autoRefresh {
		var ctx context.Context
		ctx, v.cancel = context.WithCancel(v.defaultCtx())

		if err := v.model.Watch(ctx); err != nil {
			slog.Error("LiveView watcher failed", slogs.Error, err)
		}
		return
	}
	if err := v.model.Refresh(v.defaultCtx()); err != nil {
		slog.Error("LiveView refresh failed", slogs.Error, err)
	}
}

func (v *LiveView) defaultCtx() context.Context {
	return context.WithValue(context.Background(), internal.KeyFactory, v.app.factory)
}

// Stop terminates the updater.
func (v *LiveView) Stop() {
	if v.cancel != nil {
		v.cancel()
		v.cancel = nil
	}
	v.app.Styles.RemoveListener(v)
}

// Hints returns menu hints.
func (v *LiveView) Hints() model.MenuHints {
	return v.actions.Hints()
}

// ExtraHints returns additional hints.
func (*LiveView) ExtraHints() map[string]string {
	return nil
}

func (v *LiveView) toggleManagedCmd(evt *tcell.EventKey) *tcell.EventKey {
	if v.app.InCmdMode() {
		return evt
	}

	v.managedField = !v.managedField
	v.model.SetOptions(v.defaultCtx(), map[string]bool{model.ManagedFieldsOpts: v.managedField})

	v.app.Flash().Info("toggled managed fields")
	return nil
}

func (v *LiveView) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
	if v.app.InCmdMode() {
		return evt
	}

	v.setFullScreen(!v.fullScreen)

	return nil
}

func (v *LiveView) setFullScreen(isFullScreen bool) {
	v.fullScreen = isFullScreen
	v.SetFullScreen(isFullScreen)
	v.SetBorder(!isFullScreen)
	if isFullScreen {
		v.SetBorderPadding(0, 0, 0, 0)
	} else {
		v.SetBorderPadding(0, 0, 1, 1)
	}
}

func (v *LiveView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
	if v.cmdBuff.Empty() {
		return evt
	}

	v.currentRegion++
	if v.currentRegion >= v.maxRegions {
		v.currentRegion = 0
	}
	v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
	v.text.ScrollToHighlight()
	v.updateTitle()

	return nil
}

func (v *LiveView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
	if v.cmdBuff.Empty() {
		return evt
	}

	v.currentRegion--
	if v.currentRegion < 0 {
		v.currentRegion = v.maxRegions - 1
	}
	v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
	v.text.ScrollToHighlight()
	v.updateTitle()

	return nil
}

func (v *LiveView) filterCmd(*tcell.EventKey) *tcell.EventKey {
	v.model.Filter(v.cmdBuff.GetText())
	v.cmdBuff.SetActive(false)
	v.updateTitle()

	return nil
}

func (v *LiveView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
	if v.app.InCmdMode() {
		return evt
	}
	v.app.ResetPrompt(v.cmdBuff)

	return nil
}

func (v *LiveView) eraseCmd(*tcell.EventKey) *tcell.EventKey {
	if !v.cmdBuff.IsActive() {
		return nil
	}
	v.cmdBuff.Delete()

	return nil
}

func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
	if !v.cmdBuff.InCmdMode() {
		v.cmdBuff.Reset()
		return v.app.PrevCmd(evt)
	}

	if v.cmdBuff.GetText() != "" {
		v.model.ClearFilter()
	}
	v.cmdBuff.SetActive(false)
	v.cmdBuff.Reset()
	v.updateTitle()

	return nil
}

func (v *LiveView) saveCmd(*tcell.EventKey) *tcell.EventKey {
	name := fmt.Sprintf("%s--%s", strings.Replace(v.model.GetPath(), "/", "-", 1), strings.ToLower(v.title))
	if _, err := saveYAML(v.app.Config.K9s.ContextScreenDumpDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil {
		v.app.Flash().Err(err)
	} else {
		v.app.Flash().Infof("File %q saved successfully!", name)
	}

	return nil
}

func (v *LiveView) updateTitle() {
	if v.title == "" {
		return
	}
	var fmat string
	if v.model != nil {
		fmat = fmt.Sprintf(liveViewTitleFmt, v.title, v.model.GetPath())
	}

	var (
		buff   = v.cmdBuff.GetText()
		styles = v.app.Styles.Frame()
	)
	if buff == "" {
		v.SetTitle(ui.SkinTitle(fmat, &styles))
		return
	}

	if v.maxRegions > 0 {
		buff += fmt.Sprintf("[%d:%d]", v.currentRegion+1, v.maxRegions)
	}
	fmat += fmt.Sprintf(ui.SearchFmt, buff)
	v.SetTitle(ui.SkinTitle(fmat, &styles))
}
