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

package ui

import (
	"context"
	"fmt"
	"log/slog"
	"sync"

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/config"
	"github.com/derailed/k9s/internal/model"
	"github.com/derailed/k9s/internal/model1"
	"github.com/derailed/k9s/internal/render"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/k9s/internal/vul"
	"github.com/derailed/tcell/v2"
	"github.com/derailed/tview"
	"k8s.io/apimachinery/pkg/util/sets"
)

const maxTruncate = 50

type (
	// ColorerFunc represents a row colorer.
	ColorerFunc func(ns string, evt model1.RowEvent) tcell.Color

	// DecorateFunc represents a row decorator.
	DecorateFunc func(*model1.TableData)

	// SelectedRowFunc a table selection callback.
	SelectedRowFunc func(r int)
)

// Table represents tabular data.
type Table struct {
	*SelectTable
	gvr            *client.GVR
	sortCol        model1.SortColumn
	selectedColIdx int
	manualSort     bool
	Path           string
	Extras         string
	actions        *KeyActions
	cmdBuff        *model.FishBuff
	styles         *config.Styles
	viewSetting    *config.ViewSetting
	colorerFn      model1.ColorerFunc
	decorateFn     DecorateFunc
	wide           bool
	toast          bool
	hasMetrics     bool
	ctx            context.Context
	mx             sync.RWMutex
	readOnly       bool
	noIcon         bool
	fullGVR        bool
}

// NewTable returns a new table view.
func NewTable(gvr *client.GVR) *Table {
	return &Table{
		SelectTable: &SelectTable{
			Table: tview.NewTable(),
			model: model.NewTable(gvr),
			marks: sets.New[string](),
		},
		ctx:     context.Background(),
		gvr:     gvr,
		actions: NewKeyActions(),
		cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
		sortCol: model1.SortColumn{ASC: true},
	}
}

// SetFullGVR toggles full GVR title display.
func (t *Table) SetFullGVR(b bool) {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.fullGVR = b
}

// SetNoIcon toggles no icon mode.
func (t *Table) SetNoIcon(b bool) {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.noIcon = b
}

// SetReadOnly toggles read-only mode.
func (t *Table) SetReadOnly(ro bool) {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.readOnly = ro
}

func (t *Table) setSortCol(sc model1.SortColumn) {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.sortCol = sc
}

func (t *Table) toggleSortCol() {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.sortCol.ASC = !t.sortCol.ASC
}

func (t *Table) getSortCol() model1.SortColumn {
	t.mx.RLock()
	defer t.mx.RUnlock()

	return t.sortCol
}

func (t *Table) setMSort(b bool) {
	t.mx.Lock()
	defer t.mx.Unlock()

	t.manualSort = b
}

func (t *Table) getMSort() bool {
	t.mx.RLock()
	defer t.mx.RUnlock()

	return t.manualSort
}

func (t *Table) getSelectedColIdx() int {
	t.mx.RLock()
	defer t.mx.RUnlock()

	return t.selectedColIdx
}

// initSelectedColumn initializes the selected column index based on current sort column.
func (t *Table) initSelectedColumn() {
	data := t.GetFilteredData()
	if data == nil || data.HeaderCount() == 0 {
		return
	}

	sc := t.getSortCol()
	if sc.Name == "" {
		t.mx.Lock()
		t.selectedColIdx = 0
		t.mx.Unlock()
		return
	}

	// Find the visual column index for the current sort column
	header := data.Header()
	visibleCol := 0
	for _, h := range header {
		if t.shouldExcludeColumn(h) {
			continue
		}
		if h.Name == sc.Name {
			t.mx.Lock()
			t.selectedColIdx = visibleCol
			t.mx.Unlock()
			return
		}
		visibleCol++
	}

	// If sort column not found in visible columns, default to 0
	t.mx.Lock()
	t.selectedColIdx = 0
	t.mx.Unlock()
}

// moveSelectedColumn moves the column selection by delta (-1 for left, +1 for right).
func (t *Table) moveSelectedColumn(delta int) {
	data := t.GetFilteredData()
	if data == nil || data.HeaderCount() == 0 {
		return
	}

	// Count visible columns
	visibleCount := 0
	for _, h := range data.Header() {
		if !t.shouldExcludeColumn(h) {
			visibleCount++
		}
	}

	if visibleCount == 0 {
		return
	}

	t.mx.Lock()
	t.selectedColIdx += delta
	// Wrap around
	if t.selectedColIdx >= visibleCount {
		t.selectedColIdx = 0
	} else if t.selectedColIdx < 0 {
		t.selectedColIdx = visibleCount - 1
	}
	t.mx.Unlock()

	t.Refresh()
}

// SelectNextColumn moves the column selection to the right.
func (t *Table) SelectNextColumn() {
	t.moveSelectedColumn(1)
}

// SelectPrevColumn moves the column selection to the left.
func (t *Table) SelectPrevColumn() {
	t.moveSelectedColumn(-1)
}

// SortSelectedColumn sorts by the currently selected column.
func (t *Table) SortSelectedColumn() {
	data := t.GetFilteredData()
	if data == nil || data.HeaderCount() == 0 {
		return
	}

	idx := t.getSelectedColIdx()
	if idx < 0 {
		return
	}

	// Map visual column index to actual header column name
	// (accounting for hidden columns)
	header := data.Header()
	visibleCol := 0
	var colName string
	for _, h := range header {
		if t.shouldExcludeColumn(h) {
			continue
		}
		if visibleCol == idx {
			colName = h.Name
			break
		}
		visibleCol++
	}

	if colName == "" {
		return
	}

	sc := t.getSortCol()

	// Toggle direction if same column, otherwise default to ascending
	asc := true
	if sc.Name == colName {
		asc = !sc.ASC
	}

	t.SetSortCol(colName, asc)
	t.setMSort(true)
	t.Refresh()
}

// SetViewSetting sets custom view config is present.
func (t *Table) SetViewSetting(vs *config.ViewSetting) bool {
	t.mx.Lock()
	defer t.mx.Unlock()

	if !t.viewSetting.Equals(vs) {
		t.viewSetting = vs
		slog.Debug("Updating custom view setting", slogs.GVR, t.gvr, slogs.ViewSetting, vs)
		t.model.SetViewSetting(t.ctx, vs)
		return true
	}

	return false
}

// GetViewSetting return current view settings if any.
func (t *Table) GetViewSetting() *config.ViewSetting {
	t.mx.RLock()
	defer t.mx.RUnlock()

	return t.viewSetting
}

func (t *Table) GetContext() context.Context {
	return t.ctx
}

func (t *Table) SetContext(ctx context.Context) {
	t.ctx = ctx
}

// Init initializes the component.
func (t *Table) Init(ctx context.Context) {
	t.SetFixed(1, 0)
	t.SetBorder(true)
	t.SetBorderAttributes(tcell.AttrBold)
	t.SetBorderPadding(0, 0, 1, 1)
	t.SetSelectable(true, false)
	t.SetSelectionChangedFunc(t.selectionChanged)
	t.SetBackgroundColor(tcell.ColorDefault)
	t.Select(1, 0)
	t.styles = mustExtractStyles(ctx)
	t.StylesChanged(t.styles)
}

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

// ViewSettingsChanged notifies listener the view configuration changed.
func (t *Table) ViewSettingsChanged(vs *config.ViewSetting) {
	if t.SetViewSetting(vs) {
		if vs == nil {
			if !t.getMSort() && !t.sortCol.IsSet() {
				t.setSortCol(model1.SortColumn{})
			}
		} else {
			t.setMSort(false)
		}
		t.Refresh()
	}
}

// StylesChanged notifies the skin changed.
func (t *Table) StylesChanged(s *config.Styles) {
	t.SetBackgroundColor(s.Table().BgColor.Color())
	t.SetBorderColor(s.Frame().Border.FgColor.Color())
	t.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())
	t.SetSelectedStyle(
		tcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()).
			Background(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold))
	t.selFgColor = s.Table().CursorFgColor.Color()
	t.selBgColor = s.Table().CursorBgColor.Color()
	t.Refresh()
}

// ResetToast resets toast flag.
func (t *Table) ResetToast() {
	t.toast = false
	t.Refresh()
}

// ToggleToast toggles to show toast resources.
func (t *Table) ToggleToast() {
	t.toast = !t.toast
	t.Refresh()
}

// ToggleWide toggles wide col display.
func (t *Table) ToggleWide() {
	t.wide = !t.wide
	t.Refresh()
}

// Actions returns active menu bindings.
func (t *Table) Actions() *KeyActions {
	return t.actions
}

// Styles returns styling configurator.
func (t *Table) Styles() *config.Styles {
	return t.styles
}

// FilterInput filters user commands.
func (t *Table) FilterInput(r rune) bool {
	if !t.cmdBuff.IsActive() {
		return false
	}
	t.cmdBuff.Add(r)
	t.ClearSelection()
	t.doUpdate(t.filtered(t.GetModel().Peek()))
	t.UpdateTitle()
	t.SelectFirstRow()

	return true
}

// Filter filters out table data.
func (t *Table) Filter(string) {
	t.ClearSelection()
	t.doUpdate(t.filtered(t.GetModel().Peek()))
	t.UpdateTitle()
	t.SelectFirstRow()
}

// Hints returns the view hints.
func (t *Table) Hints() model.MenuHints {
	return t.actions.Hints()
}

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

// GetFilteredData fetch filtered tabular data.
func (t *Table) GetFilteredData() *model1.TableData {
	return t.filtered(t.GetModel().Peek())
}

// SetDecorateFn specifies the default row decorator.
func (t *Table) SetDecorateFn(f DecorateFunc) {
	t.decorateFn = f
}

// SetColorerFn specifies the default colorer.
func (t *Table) SetColorerFn(f model1.ColorerFunc) {
	t.colorerFn = f
}

// SetSortCol sets in sort column index and order.
func (t *Table) SetSortCol(name string, asc bool) {
	t.setSortCol(model1.SortColumn{Name: name, ASC: asc})
}

// Update table content.
func (t *Table) Update(data *model1.TableData, hasMetrics bool) *model1.TableData {
	if t.decorateFn != nil {
		t.decorateFn(data)
	}
	t.hasMetrics = hasMetrics

	return t.doUpdate(t.filtered(data))
}

func (t *Table) GetNamespace() string {
	if t.GetModel() != nil {
		return t.GetModel().GetNamespace()
	}

	return client.NamespaceAll
}

func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
	if client.IsAllNamespaces(data.GetNamespace()) {
		t.actions.Add(
			KeyShiftP,
			NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false),
		)
	} else {
		t.actions.Delete(KeyShiftP)
	}

	oldSortCol := t.getSortCol()
	t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort()))

	// Initialize selected column index to match the current sort column
	// This ensures the highlight starts at the sorted column
	newSortCol := t.getSortCol()
	if oldSortCol.Name != newSortCol.Name {
		t.initSelectedColumn()
	}

	return data
}

func (t *Table) shouldExcludeColumn(h model1.HeaderColumn) bool {
	return (h.Hide || (!t.wide && h.Wide)) ||
		(h.Name == "NAMESPACE" && !t.GetModel().ClusterWide()) ||
		(h.MX && !t.hasMetrics) ||
		(h.VS && vul.ImgScanner == nil)
}

func (t *Table) UpdateUI(cdata, data *model1.TableData) {
	t.Clear()
	fg := t.styles.Table().Header.FgColor.Color()
	bg := t.styles.Table().Header.BgColor.Color()

	var col int
	for _, h := range cdata.Header() {
		if t.shouldExcludeColumn(h) {
			continue
		}
		t.AddHeaderCell(col, h)
		c := t.GetCell(0, col)
		c.SetBackgroundColor(bg)
		c.SetTextColor(fg)
		col++
	}
	cdata.Sort(t.getSortCol())

	pads := make(MaxyPad, cdata.HeaderCount())
	ComputeMaxColumns(pads, t.getSortCol().Name, cdata)
	cdata.RowsRange(func(row int, re model1.RowEvent) bool {
		ore, ok := data.FindRow(re.Row.ID)
		if !ok {
			slog.Error("Unable to find original row event", slogs.RowID, re.Row.ID)
			return true
		}
		t.buildRow(row+1, re, ore, cdata.Header(), pads)

		return true
	})

	t.updateSelection(true)
	t.UpdateTitle()
}

func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) {
	color := model1.DefaultColorer
	if t.colorerFn != nil {
		color = t.colorerFn
	}

	marked := t.IsMarked(re.Row.ID)
	var col int
	ns := t.GetModel().GetNamespace()
	for c, field := range re.Row.Fields {
		if c >= len(h) {
			slog.Error("Field/header overflow detected. Check your mappings!",
				slogs.GVR, t.GVR(),
				slogs.Cell, c,
				slogs.HeaderSize, len(h),
			)
			continue
		}
		if t.shouldExcludeColumn(h[c]) {
			continue
		}

		if !re.Deltas.IsBlank() && !h.IsTimeCol(c) {
			var old string
			if c < len(ore.Deltas) {
				old = ore.Deltas[c]
			}
			if c < len(re.Deltas) {
				old = re.Deltas[c]
			}
			field += Deltas(old, field)
		}

		if h[c].Decorator != nil {
			field = h[c].Decorator(field)
		}
		if h[c].Align == tview.AlignLeft {
			field = formatCell(field, pads[c])
		}

		cell := tview.NewTableCell(field)
		cell.SetExpansion(1)
		cell.SetAlign(h[c].Align)
		fgColor := color(ns, h, &re)
		cell.SetTextColor(fgColor)
		if marked {
			cell.SetTextColor(t.styles.Table().MarkColor.Color())
		}
		if col == 0 {
			cell.SetReference(re.Row.ID)
		}
		t.SetCell(r, col, cell)
		col++
	}
}

// SortColCmd designates a sorted column.
func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
	return func(*tcell.EventKey) *tcell.EventKey {
		sc := t.getSortCol()
		sc.ASC = !sc.ASC
		if sc.Name != name {
			sc.ASC = asc
		}
		sc.Name = name
		t.setSortCol(sc)
		t.setMSort(true)

		// Sync selected column index with the new sort column
		t.initSelectedColumn()

		t.Refresh()
		return nil
	}
}

// SortInvertCmd reverses sorting order.
func (t *Table) SortInvertCmd(*tcell.EventKey) *tcell.EventKey {
	t.toggleSortCol()
	t.Refresh()

	return nil
}

// ClearMarks clear out marked items.
func (t *Table) ClearMarks() {
	t.SelectTable.ClearMarks()
	t.Refresh()
}

// Refresh update the table data.
func (t *Table) Refresh() {
	data := t.model.Peek()
	if data.HeaderCount() == 0 {
		return
	}
	cdata := t.Update(data, t.hasMetrics)
	t.UpdateUI(cdata, data)
}

// GetSelectedRow returns the entire selected row or nil if nothing selected.
func (t *Table) GetSelectedRow(path string) *model1.Row {
	data := t.model.Peek()
	re, ok := data.FindRow(path)
	if !ok {
		return nil
	}

	return &re.Row
}

// NameColIndex returns the index of the resource name column.
func (t *Table) NameColIndex() int {
	col := 0
	if client.IsClusterScoped(t.GetModel().GetNamespace()) {
		return col
	}
	if t.GetModel().ClusterWide() {
		col++
	}

	return col
}

// AddHeaderCell configures a table cell header.
func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {
	sc := t.getSortCol()
	sortCol := h.Name == sc.Name
	selectedCol := col == t.getSelectedColIdx()
	styles := t.styles.Table()
	c := tview.NewTableCell(columnIndicator(sortCol, selectedCol, sc.ASC, &styles, h.Name))
	c.SetExpansion(1)
	c.SetSelectable(false)
	c.SetAlign(h.Align)
	t.SetCell(0, col, c)
}

func (t *Table) filtered(data *model1.TableData) *model1.TableData {
	return data.Filter(model1.FilterOpts{
		Toast:  t.toast,
		Filter: t.cmdBuff.GetText(),
	})
}

// CmdBuff returns the associated command buffer.
func (t *Table) CmdBuff() *model.FishBuff {
	return t.cmdBuff
}

// ShowDeleted marks row as deleted.
func (t *Table) ShowDeleted() {
	r, _ := t.GetSelection()
	cols := t.GetColumnCount()
	for x := range cols {
		t.GetCell(r, x).SetAttributes(tcell.AttrDim)
	}
}

// UpdateTitle refreshes the table title.
func (t *Table) UpdateTitle() {
	t.SetTitle(t.styleTitle())
}

func (t *Table) styleTitle() string {
	rc := int64(t.GetRowCount())
	if rc > 0 {
		rc--
	}

	ns := t.GetModel().GetNamespace()
	if client.IsClusterWide(ns) || ns == client.NotNamespaced {
		ns = client.NamespaceAll
	}
	path := t.Path
	if path != "" {
		cns, n := client.Namespaced(path)
		if cns == client.ClusterScope {
			ns = n
		} else {
			ns = path
		}
	}
	if t.Extras != "" {
		ns = t.Extras
	}

	resource := t.gvr.R()
	if t.fullGVR {
		resource = t.gvr.String()
	}

	var (
		title  string
		styles = t.styles.Frame()
	)
	if ns == client.ClusterScope {
		title = SkinTitle(fmt.Sprintf(TitleFmt, resource, render.AsThousands(rc)), &styles)
	} else {
		title = SkinTitle(fmt.Sprintf(NSTitleFmt, resource, ns, render.AsThousands(rc)), &styles)
	}

	buff := t.cmdBuff.GetText()
	if internal.IsLabelSelector(buff) {
		if sel, err := ExtractLabelSelector(buff); err == nil {
			buff = render.Truncate(sel.String(), maxTruncate)
		}
	} else if l := t.GetModel().GetLabelSelector(); l != nil && !l.Empty() {
		buff = render.Truncate(l.String(), maxTruncate)
	} else if buff != "" {
		buff = render.Truncate(buff, maxTruncate)
	}
	if buff == "" {
		return title
	}

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

// ROIndicator returns an icon showing whether the session is in readonly mode or not.
func ROIndicator(ro, noIC bool) string {
	switch {
	case noIC:
		return ""
	case ro:
		return lockedIC
	default:
		return unlockedIC
	}
}
