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

package view

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

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/config"
	"github.com/derailed/k9s/internal/dao"
	"github.com/derailed/k9s/internal/model"
	"github.com/derailed/k9s/internal/render"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/k9s/internal/ui"
	"github.com/derailed/k9s/internal/ui/dialog"
	"github.com/derailed/k9s/internal/view/cmd"
	"github.com/derailed/k9s/internal/xray"
	"github.com/derailed/tcell/v2"
	"github.com/derailed/tview"
	"github.com/sahilm/fuzzy"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/sets"
)

const xrayTitle = "Xray"

var _ ResourceViewer = (*Xray)(nil)

// Xray represents an xray tree view.
type Xray struct {
	*ui.Tree

	app      *App
	gvr      *client.GVR
	meta     *metav1.APIResource
	model    *model.Tree
	cancelFn context.CancelFunc
	envFn    EnvFunc
}

// NewXray returns a new view.
func NewXray(gvr *client.GVR) ResourceViewer {
	return &Xray{
		gvr:   gvr,
		Tree:  ui.NewTree(),
		model: model.NewTree(gvr),
	}
}

func (*Xray) SetCommand(*cmd.Interpreter)      {}
func (*Xray) SetFilter(string)                 {}
func (*Xray) SetLabelSelector(labels.Selector) {}

// Init initializes the view.
func (x *Xray) Init(ctx context.Context) error {
	x.envFn = x.k9sEnv

	if err := x.Tree.Init(ctx); err != nil {
		return err
	}
	x.SetKeyListenerFn(x.keyEntered)

	var err error
	x.meta, err = dao.MetaAccess.MetaFor(x.gvr)
	if err != nil {
		return err
	}

	if x.app, err = extractApp(ctx); err != nil {
		return err
	}

	x.bindKeys()
	x.SetBackgroundColor(x.app.Styles.Xray().BgColor.Color())
	x.SetBorderColor(x.app.Styles.Xray().FgColor.Color())
	x.SetBorderFocusColor(x.app.Styles.Frame().Border.FocusColor.Color())
	x.SetGraphicsColor(x.app.Styles.Xray().GraphicColor.Color())
	x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, cases.Title(language.Und, cases.NoLower).String(x.gvr.R())))

	x.model.SetRefreshRate(x.app.Config.K9s.RefreshDuration())
	x.model.SetNamespace(client.CleanseNamespace(x.app.Config.ActiveNamespace()))
	x.model.AddListener(x)

	x.SetChangedFunc(func(n *tview.TreeNode) {
		spec, ok := n.GetReference().(xray.NodeSpec)
		if !ok {
			slog.Error("No ref found on node", slogs.FQN, n.GetText())
			return
		}
		x.SetSelectedItem(spec.AsPath())
		x.refreshActions()
	})
	x.refreshActions()

	return nil
}

// InCmdMode checks if prompt is active.
func (*Xray) InCmdMode() bool {
	return false
}

// ExtraHints returns additional hints.
func (x *Xray) ExtraHints() map[string]string {
	if x.app.Config.K9s.UI.NoIcons {
		return nil
	}
	return xray.EmojiInfo()
}

// SetInstance sets specific resource instance.
func (*Xray) SetInstance(string) {}

func (x *Xray) bindKeys() {
	x.Actions().Bulk(ui.KeyMap{
		ui.KeySlash:     ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false),
		tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false),
		tcell.KeyEnter:  ui.NewKeyAction("Goto", x.gotoCmd, true),
	})
}

func (x *Xray) keyEntered() {
	x.ClearSelection()
	x.update(x.filter(x.model.Peek()))
}

func (x *Xray) refreshActions() {
	aa := ui.NewKeyActions()

	defer func() {
		if err := pluginActions(x, aa); err != nil {
			slog.Warn("Plugins load failed", slogs.Error, err)
		}
		if err := hotKeyActions(x, aa); err != nil {
			slog.Warn("HotKeys load failed", slogs.Error, err)
		}

		x.Actions().Merge(aa)
		x.app.Menu().HydrateMenu(x.Hints())
	}()

	x.Actions().Clear()
	x.bindKeys()
	x.BindKeys()

	spec := x.selectedSpec()
	if spec == nil {
		return
	}

	gvr := spec.GVR()
	var err error
	x.meta, err = dao.MetaAccess.MetaFor(gvr)
	if err != nil {
		slog.Warn("No meta found!",
			slogs.GVR, gvr,
			slogs.Error, err,
		)
		return
	}

	if client.Can(x.meta.Verbs, "edit") {
		aa.Add(ui.KeyE, ui.NewKeyAction("Edit", x.editCmd, true))
	}
	if client.Can(x.meta.Verbs, "delete") {
		aa.Add(tcell.KeyCtrlD, ui.NewKeyAction("Delete", x.deleteCmd, true))
	}
	if !dao.IsK9sMeta(x.meta) {
		aa.Bulk(ui.KeyMap{
			ui.KeyY: ui.NewKeyAction(yamlAction, x.viewCmd, true),
			ui.KeyD: ui.NewKeyAction("Describe", x.describeCmd, true),
		})
	}

	switch gvr {
	case client.NsGVR:
		x.Actions().Delete(tcell.KeyEnter)
	case client.CoGVR:
		x.Actions().Delete(tcell.KeyEnter)
		aa.Bulk(ui.KeyMap{
			ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true),
			ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true),
			ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true),
		})
	case client.PodGVR:
		aa.Bulk(ui.KeyMap{
			ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true),
			ui.KeyA: ui.NewKeyAction("Attach", x.attachCmd, true),
			ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true),
			ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true),
		})
	}
	x.Actions().Merge(aa)
}

// GetSelectedPath returns the current selection as string.
func (x *Xray) GetSelectedPath() string {
	spec := x.selectedSpec()
	if spec == nil {
		return ""
	}
	return spec.Path()
}

func (x *Xray) selectedSpec() *xray.NodeSpec {
	node := x.GetCurrentNode()
	if node == nil {
		return nil
	}

	ref, ok := node.GetReference().(xray.NodeSpec)
	if !ok {
		slog.Error("Expecting a NodeSpec",
			slogs.Path, node.GetText(),
			slogs.RefType, fmt.Sprintf("%T", node.GetReference()),
		)
		return nil
	}

	return &ref
}

// EnvFn returns an plugin env function if available.
func (x *Xray) EnvFn() EnvFunc {
	return x.envFn
}

func (x *Xray) k9sEnv() Env {
	env := k8sEnv(x.app.Conn().Config())

	spec := x.selectedSpec()
	if spec == nil {
		return env
	}

	env["FILTER"] = x.CmdBuff().GetText()
	if env["FILTER"] == "" {
		ns, n := client.Namespaced(spec.Path())
		env["NAMESPACE"], env["FILTER"] = ns, n
	}

	switch spec.GVR() {
	case client.CoGVR:
		_, co := client.Namespaced(spec.Path())
		env["CONTAINER"] = co
		ns, n := client.Namespaced(*spec.ParentPath())
		env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co
	default:
		ns, n := client.Namespaced(spec.Path())
		env["NAMESPACE"], env["NAME"] = ns, n
	}

	return env
}

// Aliases returns all available aliases.
func (x *Xray) Aliases() sets.Set[string] {
	return aliases(x.meta, x.app.command.AliasesFor(client.NewGVRFromMeta(x.meta)))
}

func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {
	return func(*tcell.EventKey) *tcell.EventKey {
		spec := x.selectedSpec()
		if spec == nil {
			return nil
		}

		x.showLogs(spec, prev)

		return nil
	}
}

func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) {
	// Need to load and wait for pods
	path, co := spec.Path(), ""
	if spec.GVR() == client.CoGVR {
		_, coName := client.Namespaced(spec.Path())
		path, co = *spec.ParentPath(), coName
	}

	ns, _ := client.Namespaced(path)
	_, err := x.app.factory.CanForResource(ns, client.PodGVR, client.ListAccess)
	if err != nil {
		x.app.Flash().Err(err)
		return
	}

	opts := dao.LogOptions{
		Path:      path,
		Container: co,
		Previous:  prev,
	}
	if err := x.app.inject(NewLog(client.PodGVR, &opts), false); err != nil {
		x.app.Flash().Err(err)
	}
}

func (x *Xray) shellCmd(*tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return nil
	}

	if spec.Status() != "ok" {
		x.app.Flash().Errf("%s is not in a running state", spec.Path())
		return nil
	}

	path, co := spec.Path(), ""
	if spec.GVR() == client.CoGVR {
		_, co = client.Namespaced(spec.Path())
		path = *spec.ParentPath()
	}

	if err := containerShellIn(x.app, x, path, co); err != nil {
		x.app.Flash().Err(err)
	}

	return nil
}

func (x *Xray) attachCmd(*tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return nil
	}

	if spec.Status() != "ok" {
		x.app.Flash().Errf("%s is not in a running state", spec.Path())
		return nil
	}

	path, co := spec.Path(), ""
	if spec.GVR() == client.CoGVR {
		path = *spec.ParentPath()
	}

	if err := containerAttachIn(x.app, x, path, co); err != nil {
		x.app.Flash().Err(err)
	}

	return nil
}

func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return evt
	}

	ctx := x.defaultContext()
	raw, err := x.model.ToYAML(ctx, spec.GVR(), spec.Path())
	if err != nil {
		x.App().Flash().Errf("unable to get resource %q -- %s", spec.GVR(), err)
		return nil
	}

	details := NewDetails(x.app, yamlAction, spec.Path(), contentYAML, true).Update(raw)
	if err := x.app.inject(details, false); err != nil {
		x.app.Flash().Err(err)
	}

	return nil
}

func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return evt
	}

	x.Stop()
	defer x.Start()
	{
		meta, err := dao.MetaAccess.MetaFor(spec.GVR())
		if err != nil {
			slog.Warn("No meta found!",
				slogs.GVR, spec.GVR(),
				slogs.Error, err,
			)
			return nil
		}
		x.resourceDelete(spec.GVR(), spec, fmt.Sprintf("Delete %s %s?", meta.SingularName, spec.Path()))
	}

	return nil
}

func (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return evt
	}

	x.describe(spec.GVR(), spec.Path())

	return nil
}

func (x *Xray) describe(gvr *client.GVR, path string) {
	ctx := context.Background()
	ctx = context.WithValue(ctx, internal.KeyFactory, x.app.factory)

	yaml, err := x.model.Describe(ctx, gvr, path)
	if err != nil {
		x.app.Flash().Errf("Describe command failed: %s", err)
		return
	}

	details := NewDetails(x.app, "Describe", path, contentYAML, true).Update(yaml)
	if err := x.app.inject(details, false); err != nil {
		x.app.Flash().Err(err)
	}
}

func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {
	spec := x.selectedSpec()
	if spec == nil {
		return evt
	}

	x.Stop()
	defer x.Start()
	{
		ns, n := client.Namespaced(spec.Path())
		args := make([]string, 0, 10)
		args = append(args,
			"edit",
			spec.GVR().R(),
			"-n", ns,
			"--context", x.app.Config.K9s.ActiveContextName(),
		)
		if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
			args = append(args, "--kubeconfig", *cfg)
		}
		if err := runK(x.app, &shellOpts{args: append(args, n)}); err != nil {
			x.app.Flash().Errf("Edit exec failed: %s", err)
		}
	}

	return evt
}

func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
	if x.app.InCmdMode() {
		return evt
	}
	x.app.ResetPrompt(x.CmdBuff())

	return nil
}

func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
	if !x.CmdBuff().InCmdMode() {
		x.CmdBuff().Reset()
		return x.app.PrevCmd(evt)
	}
	x.CmdBuff().Reset()
	x.model.ClearFilter()
	x.Start()

	return nil
}

func (x *Xray) gotoCmd(*tcell.EventKey) *tcell.EventKey {
	if x.CmdBuff().IsActive() {
		if internal.IsLabelSelector(x.CmdBuff().GetText()) {
			x.Start()
		}
		x.CmdBuff().SetActive(false)
		x.GetRoot().ExpandAll()

		return nil
	}

	spec := x.selectedSpec()
	if spec == nil {
		return nil
	}
	if len(strings.Split(spec.Path(), "/")) == 1 {
		return nil
	}
	x.app.gotoResource(spec.GVR().String(), spec.Path(), false, true)

	return nil
}

func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {
	q := x.CmdBuff().GetText()
	if x.CmdBuff().Empty() || internal.IsLabelSelector(q) {
		return root
	}

	x.UpdateTitle()
	if f, ok := internal.IsFuzzySelector(q); ok {
		return root.Filter(f, fuzzyFilter)
	}

	if internal.IsInverseSelector(q) {
		return root.Filter(q, rxInverseFilter)
	}

	return root.Filter(q, rxFilter)
}

// TreeNodeSelected callback for node selection.
func (x *Xray) TreeNodeSelected() {
	x.app.QueueUpdateDraw(func() {
		n := x.GetCurrentNode()
		if n != nil {
			n.SetColor(x.app.Styles.Xray().CursorColor.Color())
		}
	})
}

// TreeLoadFailed notifies the load failed.
func (x *Xray) TreeLoadFailed(err error) {
	x.app.Flash().Err(err)
}

func (x *Xray) update(node *xray.TreeNode) {
	root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles)
	if node == nil {
		x.app.QueueUpdateDraw(func() {
			x.SetRoot(root)
		})
		return
	}

	for _, c := range node.Children {
		x.hydrate(root, c)
	}
	if x.GetSelectedItem() == "" {
		x.SetSelectedItem(node.Spec().Path())
	}

	x.app.QueueUpdateDraw(func() {
		x.SetRoot(root)
		root.Walk(func(node, parent *tview.TreeNode) bool {
			spec, ok := node.GetReference().(xray.NodeSpec)
			if !ok {
				slog.Error("Expecting a NodeSpec",
					slogs.FQN, node.GetText(),
					slogs.RefType, fmt.Sprintf("%T", node.GetReference()),
				)
				return false
			}
			// BOZO!! Figure this out expand/collapse but the root
			if parent != nil {
				node.SetExpanded(x.ExpandNodes())
			} else {
				node.SetExpanded(true)
			}

			if spec.AsPath() == x.GetSelectedItem() {
				node.SetExpanded(true).SetSelectable(true)
				x.SetCurrentNode(node)
			}
			return true
		})
	})
}

// TreeChanged notifies the model data changed.
func (x *Xray) TreeChanged(node *xray.TreeNode) {
	x.Count = node.Count(x.gvr)
	x.update(x.filter(node))
	x.UpdateTitle()
}

func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
	node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles)
	for _, c := range n.Children {
		x.hydrate(node, c)
	}
	parent.AddChild(node)
}

// SetEnvFn sets the custom environment function.
func (*Xray) SetEnvFn(EnvFunc) {}

// Refresh updates the view.
func (*Xray) Refresh() {}

// BufferCompleted indicates the buffer was changed.
func (x *Xray) BufferCompleted(_, _ string) {
	x.update(x.filter(x.model.Peek()))
}

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

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

func (x *Xray) defaultContext() context.Context {
	ctx := context.WithValue(context.Background(), internal.KeyFactory, x.app.factory)
	ctx = context.WithValue(ctx, internal.KeyFields, "")
	if x.CmdBuff().Empty() {
		ctx = context.WithValue(ctx, internal.KeyLabels, labels.Everything())
	} else {
		if sel, err := ui.TrimLabelSelector(x.CmdBuff().GetText()); err == nil {
			ctx = context.WithValue(ctx, internal.KeyLabels, sel)
		}
	}

	return ctx
}

// Start initializes resource watch loop.
func (x *Xray) Start() {
	x.Stop()
	x.CmdBuff().AddListener(x)

	ctx := x.defaultContext()
	ctx, x.cancelFn = context.WithCancel(ctx)
	x.model.Watch(ctx)
	x.UpdateTitle()
}

// Stop terminates watch loop.
func (x *Xray) Stop() {
	if x.cancelFn == nil {
		return
	}
	x.cancelFn()
	x.cancelFn = nil
	x.CmdBuff().RemoveListener(x)
}

// AddBindKeysFn sets up extra key bindings.
func (*Xray) AddBindKeysFn(BindKeysFunc) {}

// SetContextFn sets custom context.
func (*Xray) SetContextFn(ContextFunc) {}

// Name returns the component name.
func (*Xray) Name() string { return "XRay" }

// GetTable returns the underlying table.
func (*Xray) GetTable() *Table { return nil }

// GVR returns a resource descriptor.
func (x *Xray) GVR() *client.GVR { return x.gvr }

// App returns the current app handle.
func (x *Xray) App() *App {
	return x.app
}

// UpdateTitle updates the view title.
func (x *Xray) UpdateTitle() {
	t := x.styleTitle()
	x.app.QueueUpdateDraw(func() {
		x.SetTitle(t)
	})
}

func (x *Xray) styleTitle() string {
	base := fmt.Sprintf("%s-%s", xrayTitle, cases.Title(language.Und, cases.NoLower).String(x.gvr.R()))
	ns := x.model.GetNamespace()
	if client.IsAllNamespaces(ns) {
		ns = client.NamespaceAll
	}

	var (
		title  string
		styles = x.app.Styles.Frame()
	)
	if ns == client.ClusterScope {
		title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), &styles)
	} else {
		title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), &styles)
	}

	buff := x.CmdBuff().GetText()
	if buff == "" {
		return title
	}
	if internal.IsLabelSelector(buff) {
		if sel, err := ui.TrimLabelSelector(buff); err == nil {
			buff = sel.String()
		}
	}

	return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles)
}

func (x *Xray) resourceDelete(gvr *client.GVR, spec *xray.NodeSpec, msg string) {
	d := x.app.Styles.Dialog()
	dialog.ShowDelete(&d, x.app.Content.Pages, msg, func(_ *metav1.DeletionPropagation, force bool) {
		x.app.Flash().Infof("Delete resource %s %s", spec.GVR(), spec.Path())
		accessor, err := dao.AccessorFor(x.app.factory, gvr)
		if err != nil {
			slog.Error("No accessor found",
				slogs.GVR, gvr,
				slogs.Error, err,
			)
			return
		}

		nuker, ok := accessor.(dao.Nuker)
		if !ok {
			x.app.Flash().Errf("Invalid nuker %T", accessor)
			return
		}
		grace := dao.DefaultGrace
		if force {
			grace = dao.ForceGrace
		}
		if err := nuker.Delete(context.Background(), spec.Path(), nil, grace); err != nil {
			x.app.Flash().Errf("Delete failed with `%s", err)
		} else {
			x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), spec.Path())
			x.app.factory.DeleteForwarder(spec.Path())
		}
		x.Refresh()
	}, func() {})
}

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

func fuzzyFilter(q, path string) bool {
	q = strings.TrimSpace(q[2:])
	mm := fuzzy.Find(q, []string{path})

	return len(mm) > 0
}

func rxFilter(q, path string) bool {
	rx := regexp.MustCompile(`(?i)` + q)
	tokens := strings.Split(path, xray.PathSeparator)
	for _, t := range tokens {
		if rx.MatchString(t) {
			return true
		}
	}

	return false
}

func rxInverseFilter(q, path string) bool {
	q = strings.TrimSpace(q[1:])
	rx := regexp.MustCompile(`(?i)` + q)
	tokens := strings.Split(path, xray.PathSeparator)
	for _, t := range tokens {
		if rx.MatchString(t) {
			return false
		}
	}

	return true
}

func makeTreeNode(node *xray.TreeNode, expanded, showIcons bool, styles *config.Styles) *tview.TreeNode {
	n := tview.NewTreeNode("No data...")
	if node != nil {
		n.SetText(node.Title(showIcons))
		n.SetReference(node.Spec())
	}
	n.SetSelectable(true)
	n.SetExpanded(expanded)
	n.SetColor(styles.Xray().CursorColor.Color())
	n.SetSelectedFunc(func() {
		n.SetExpanded(!n.IsExpanded())
	})
	return n
}
