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

package model

import (
	"context"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"sync/atomic"
	"time"

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/dao"
	"github.com/derailed/k9s/internal/render"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/k9s/internal/xray"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

const initTreeRefreshRate = 500 * time.Millisecond

// TreeListener represents a tree model listener.
type TreeListener interface {
	// TreeChanged notifies the model data changed.
	TreeChanged(*xray.TreeNode)

	// TreeLoadFailed notifies the load failed.
	TreeLoadFailed(error)
}

// Tree represents a tree model.
type Tree struct {
	gvr         *client.GVR
	namespace   string
	root        *xray.TreeNode
	listeners   []TreeListener
	inUpdate    int32
	refreshRate time.Duration
	query       string
}

// NewTree returns a new model.
func NewTree(gvr *client.GVR) *Tree {
	return &Tree{
		gvr:         gvr,
		refreshRate: 2 * time.Second,
	}
}

// ClearFilter clears out active filter.
func (t *Tree) ClearFilter() {
	t.query = ""
}

// SetFilter sets the current filter.
func (t *Tree) SetFilter(q string) {
	t.query = q
}

// AddListener adds a listener.
func (t *Tree) AddListener(l TreeListener) {
	t.listeners = append(t.listeners, l)
}

// RemoveListener delete a listener.
func (t *Tree) RemoveListener(l TreeListener) {
	victim := -1
	for i, lis := range t.listeners {
		if lis == l {
			victim = i
			break
		}
	}

	if victim >= 0 {
		t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)
	}
}

// Watch initiates model updates.
func (t *Tree) Watch(ctx context.Context) {
	t.Refresh(ctx)
	go t.updater(ctx)
}

// Refresh update the model now.
func (t *Tree) Refresh(ctx context.Context) {
	t.refresh(ctx)
}

// GetNamespace returns the model namespace.
func (t *Tree) GetNamespace() string {
	return t.namespace
}

// SetNamespace sets up model namespace.
func (t *Tree) SetNamespace(ns string) {
	t.namespace = ns
	if t.root == nil {
		return
	}
	t.root.Clear()
}

// SetRefreshRate sets model refresh duration.
func (t *Tree) SetRefreshRate(d time.Duration) {
	t.refreshRate = d
}

// ClusterWide checks if resource is scope for all namespaces.
func (t *Tree) ClusterWide() bool {
	return client.IsClusterWide(t.namespace)
}

// InNamespace checks if current namespace matches desired namespace.
func (t *Tree) InNamespace(ns string) bool {
	return t.namespace == ns
}

// Empty return true if no model data.
func (t *Tree) Empty() bool {
	return t.root.IsLeaf()
}

// Peek returns model data.
func (t *Tree) Peek() *xray.TreeNode {
	return t.root
}

// Describe describes a given resource.
func (t *Tree) Describe(ctx context.Context, gvr *client.GVR, path string) (string, error) {
	meta, err := t.getMeta(ctx, gvr)
	if err != nil {
		return "", err
	}

	desc, ok := meta.DAO.(dao.Describer)
	if !ok {
		return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
	}

	return desc.Describe(path)
}

// ToYAML returns a resource yaml.
func (t *Tree) ToYAML(ctx context.Context, gvr *client.GVR, path string) (string, error) {
	meta, err := t.getMeta(ctx, gvr)
	if err != nil {
		return "", err
	}

	desc, ok := meta.DAO.(dao.Describer)
	if !ok {
		return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
	}

	return desc.ToYAML(path, false)
}

func (t *Tree) updater(ctx context.Context) {
	defer slog.Debug("Tree-model canceled", slogs.GVR, t.gvr)

	rate := initTreeRefreshRate
	for {
		select {
		case <-ctx.Done():
			t.root = nil
			return
		case <-time.After(rate):
			rate = t.refreshRate
			t.refresh(ctx)
		}
	}
}

func (t *Tree) refresh(ctx context.Context) {
	if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {
		slog.Debug("Dropping update...")
		return
	}
	defer atomic.StoreInt32(&t.inUpdate, 0)

	if err := t.reconcile(ctx); err != nil {
		slog.Error("Reconcile failed", slogs.Error, err)
		t.fireTreeLoadFailed(err)
		return
	}
}

func (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
	factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
	if !ok {
		return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
	}
	a.Init(factory, t.gvr)

	return a.List(ctx, client.CleanseNamespace(t.namespace))
}

func (t *Tree) reconcile(ctx context.Context) error {
	meta := t.resourceMeta()
	oo, err := t.list(ctx, meta.DAO)
	if err != nil {
		return err
	}

	ns := client.CleanseNamespace(t.namespace)
	root := xray.NewTreeNode(t.gvr, t.gvr.R())
	ctx = context.WithValue(ctx, xray.KeyParent, root)
	if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
		table, ok := oo[0].(*metav1.Table)
		if !ok {
			return fmt.Errorf("expecting a Table but got %T", oo[0])
		}
		if err := genericTreeHydrate(ctx, ns, table, meta.TreeRenderer); err != nil {
			return err
		}
	} else if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
		return err
	}

	root.Sort()
	if t.query != "" {
		t.root = root.Filter(t.query, rxMatch)
	}
	if t.root == nil || t.root.Diff(root) {
		t.root = root
		t.fireTreeChanged(t.root)
	}

	return nil
}

func (t *Tree) resourceMeta() ResourceMeta {
	meta, ok := Registry[t.gvr]
	if !ok {
		meta = ResourceMeta{
			DAO:      &dao.Table{},
			Renderer: &render.Table{},
		}
	}
	if meta.DAO == nil {
		meta.DAO = &dao.Resource{}
	}

	return meta
}

func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
	for _, l := range t.listeners {
		l.TreeChanged(root)
	}
}

func (t *Tree) fireTreeLoadFailed(err error) {
	for _, l := range t.listeners {
		l.TreeLoadFailed(err)
	}
}

func (t *Tree) getMeta(ctx context.Context, gvr *client.GVR) (ResourceMeta, error) {
	meta := t.resourceMeta()
	factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
	if !ok {
		return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
	}
	meta.DAO.Init(factory, gvr)

	return meta, nil
}

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

func rxMatch(q, path string) bool {
	rx := regexp.MustCompile(`(?i)` + q)

	tokens := strings.Split(path, "::")
	for _, t := range tokens {
		if rx.MatchString(t) {
			return true
		}
	}
	return false
}

func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {
	if re == nil {
		return fmt.Errorf("no tree renderer defined for this resource")
	}
	pool := internal.NewWorkerPool(ctx, internal.DefaultPoolSize)
	for _, o := range oo {
		pool.Add(func(_ context.Context) error {
			return re.Render(ctx, ns, o)
		})
	}
	errs := pool.Drain()
	if len(errs) > 0 {
		return errs[0]
	}

	return nil
}

func genericTreeHydrate(ctx context.Context, ns string, table *metav1.Table, re TreeRenderer) error {
	tre, ok := re.(*xray.Generic)
	if !ok {
		return fmt.Errorf("expecting xray.Generic renderer but got %T", re)
	}

	tre.SetTable(ns, table)
	// BOZO!! Need table row sorter!!
	for _, row := range table.Rows {
		if err := tre.Render(ctx, ns, row); err != nil {
			return err
		}
	}

	return nil
}
