package model

import (
	"context"
	"regexp"
	"strings"
	"sync/atomic"
	"time"

	backoff "github.com/cenkalti/backoff/v4"
	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/dao"
	"github.com/rs/zerolog/log"
	"github.com/sahilm/fuzzy"
)

// Values tracks Helm values representations.
type Values struct {
	gvr       client.GVR
	inUpdate  int32
	path      string
	query     string
	lines     []string
	allValues bool
	listeners []ResourceViewerListener
	options   ViewerToggleOpts
}

// NewValues return a new Helm values resource model.
func NewValues(gvr client.GVR, path string) *Values {
	return &Values{
		gvr:       gvr,
		path:      path,
		allValues: false,
		lines:     getValues(path, false),
	}
}

func getHelmDao() *dao.Helm {
	return Registry["helm"].DAO.(*dao.Helm)
}

func getValues(path string, allValues bool) []string {
	vals, err := getHelmDao().GetValues(path, allValues)
	if err != nil {
		log.Error().Err(err).Msgf("Failed to get Helm values")
	}
	return strings.Split(string(vals), "\n")
}

// ToggleValues toggles between user supplied values and computed values.
func (v *Values) ToggleValues() {
	v.allValues = !v.allValues
	lines := getValues(v.path, v.allValues)
	v.lines = lines
}

// GetPath returns the active resource path.
func (v *Values) GetPath() string {
	return v.path
}

// SetOptions toggle model options.
func (v *Values) SetOptions(ctx context.Context, opts ViewerToggleOpts) {
	v.options = opts
	if err := v.refresh(ctx); err != nil {
		v.fireResourceFailed(err)
	}
}

// Filter filters the model.
func (v *Values) Filter(q string) {
	v.query = q
	v.filterChanged(v.lines)
}

func (v *Values) filterChanged(lines []string) {
	v.fireResourceChanged(lines, v.filter(v.query, lines))
}

func (v *Values) filter(q string, lines []string) fuzzy.Matches {
	if q == "" {
		return nil
	}
	if dao.IsFuzzySelector(q) {
		return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
	}
	return v.rxFilter(q, lines)
}

func (*Values) fuzzyFilter(q string, lines []string) fuzzy.Matches {
	return fuzzy.Find(q, lines)
}

func (*Values) rxFilter(q string, lines []string) fuzzy.Matches {
	rx, err := regexp.Compile(`(?i)` + q)
	if err != nil {
		return nil
	}
	matches := make(fuzzy.Matches, 0, len(lines))
	for i, l := range lines {
		if loc := rx.FindStringIndex(l); len(loc) == 2 {
			matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
		}
	}

	return matches
}

func (v *Values) fireResourceChanged(lines []string, matches fuzzy.Matches) {
	for _, l := range v.listeners {
		l.ResourceChanged(lines, matches)
	}
}

func (v *Values) fireResourceFailed(err error) {
	for _, l := range v.listeners {
		l.ResourceFailed(err)
	}
}

// ClearFilter clear out the filter.
func (v *Values) ClearFilter() {
	v.query = ""
}

// Peek returns the current model data.
func (v *Values) Peek() []string {
	return v.lines
}

// Refresh updates model data.
func (v *Values) Refresh(ctx context.Context) error {
	return v.refresh(ctx)
}

// Watch watches for Values changes.
func (v *Values) Watch(ctx context.Context) error {
	if err := v.refresh(ctx); err != nil {
		return err
	}
	go v.updater(ctx)

	return nil
}

func (v *Values) updater(ctx context.Context) {
	defer log.Debug().Msgf("YAML canceled -- %q", v.gvr)

	backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
	delay := defaultReaderRefreshRate
	for {
		select {
		case <-ctx.Done():
			return
		case <-time.After(delay):
			if err := v.refresh(ctx); err != nil {
				v.fireResourceFailed(err)
				if delay = backOff.NextBackOff(); delay == backoff.Stop {
					log.Error().Err(err).Msgf("giving up retrieving chart values")
					return
				}
			} else {
				backOff.Reset()
				delay = defaultReaderRefreshRate
			}
		}
	}
}

func (v *Values) refresh(ctx context.Context) error {
	if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) {
		log.Debug().Msgf("Dropping update...")
		return nil
	}
	defer atomic.StoreInt32(&v.inUpdate, 0)

	if err := v.reconcile(ctx); err != nil {
		return err
	}

	return nil
}

func (v *Values) reconcile(_ context.Context) error {
	v.fireResourceChanged(v.lines, v.filter(v.query, v.lines))

	return nil
}

// AddListener adds a new model listener.
func (v *Values) AddListener(l ResourceViewerListener) {
	v.listeners = append(v.listeners, l)
}

// RemoveListener delete a listener from the list.
func (v *Values) RemoveListener(l ResourceViewerListener) {
	victim := -1
	for i, lis := range v.listeners {
		if lis == l {
			victim = i
			break
		}
	}

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