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

package model

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

	"github.com/derailed/k9s/internal"
	"github.com/derailed/k9s/internal/client"
	"github.com/derailed/k9s/internal/color"
	"github.com/derailed/k9s/internal/config"
	"github.com/derailed/k9s/internal/dao"
	"github.com/derailed/k9s/internal/slogs"
)

// LogsListener represents a log model listener.
type LogsListener interface {
	// LogChanged notifies the model changed.
	LogChanged([][]byte)

	// LogCleared indicates logs are cleared.
	LogCleared()

	// LogFailed indicates a log failure.
	LogFailed(error)

	// LogStop indicates logging was canceled.
	LogStop()

	// LogResume indicates logging has resumed.
	LogResume()

	// LogCanceled indicates no more logs will come.
	LogCanceled()
}

// Log represents a resource logger.
type Log struct {
	factory      dao.Factory
	lines        *dao.LogItems
	listeners    []LogsListener
	gvr          *client.GVR
	logOptions   *dao.LogOptions
	cancelFn     context.CancelFunc
	mx           sync.RWMutex
	filter       string
	lastSent     int
	flushTimeout time.Duration
}

// NewLog returns a new model.
func NewLog(gvr *client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *Log {
	return &Log{
		gvr:          gvr,
		logOptions:   opts,
		lines:        dao.NewLogItems(),
		flushTimeout: flushTimeout,
	}
}

func (l *Log) GVR() *client.GVR {
	return l.gvr
}

func (l *Log) LogOptions() *dao.LogOptions {
	return l.logOptions
}

// SinceSeconds returns since seconds option.
func (l *Log) SinceSeconds() int64 {
	l.mx.RLock()
	defer l.mx.RUnlock()

	return l.logOptions.SinceSeconds
}

// IsHead returns log head option.
func (l *Log) IsHead() bool {
	l.mx.RLock()
	defer l.mx.RUnlock()

	return l.logOptions.Head
}

// ToggleShowTimestamp toggles to logs timestamps.
func (l *Log) ToggleShowTimestamp(b bool) {
	l.logOptions.ShowTimestamp = b
	l.Refresh()
}

func (l *Log) Head(ctx context.Context) {
	l.mx.Lock()
	l.logOptions.Head = true
	l.mx.Unlock()
	l.Restart(ctx)
}

// SetSinceSeconds sets the logs retrieval time.
func (l *Log) SetSinceSeconds(ctx context.Context, i int64) {
	l.logOptions.SinceSeconds, l.logOptions.Head = i, false
	l.Restart(ctx)
}

// Configure sets logger configuration.
func (l *Log) Configure(opts config.Logger) {
	l.logOptions.Lines = opts.TailCount
	l.logOptions.SinceSeconds = opts.SinceSeconds
}

// GetPath returns resource path.
func (l *Log) GetPath() string {
	return l.logOptions.Path
}

// GetContainer returns the resource container if any or "" otherwise.
func (l *Log) GetContainer() string {
	return l.logOptions.Container
}

// HasDefaultContainer returns true if the pod has a default container, false otherwise.
func (l *Log) HasDefaultContainer() bool {
	return l.logOptions.DefaultContainer != ""
}

// Init initializes the model.
func (l *Log) Init(f dao.Factory) {
	l.factory = f
}

// Clear the logs.
func (l *Log) Clear() {
	l.mx.Lock()
	l.lines.Clear()
	l.lastSent = 0
	l.mx.Unlock()

	l.fireLogCleared()
}

// Refresh refreshes the logs.
func (l *Log) Refresh() {
	l.fireLogCleared()
	ll := make([][]byte, l.lines.Len())
	l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
	l.fireLogChanged(ll)
}

// Restart restarts the logger.
func (l *Log) Restart(ctx context.Context) {
	l.Stop()
	l.Clear()
	l.fireLogResume()
	l.Start(ctx)
}

// Start starts logging.
func (l *Log) Start(ctx context.Context) {
	if err := l.load(ctx); err != nil {
		slog.Error("Tail logs failed!", slogs.Error, err)
		l.fireLogError(err)
	}
}

// Stop terminates logging.
func (l *Log) Stop() {
	l.cancel()
}

// Set sets the log lines (for testing only!)
func (l *Log) Set(lines *dao.LogItems) {
	l.mx.Lock()
	l.lines.Merge(lines)
	l.mx.Unlock()

	l.fireLogCleared()
	ll := make([][]byte, l.lines.Len())
	l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
	l.fireLogChanged(ll)
}

// ClearFilter resets the log filter if any.
func (l *Log) ClearFilter() {
	l.mx.Lock()
	l.filter = ""
	l.mx.Unlock()

	l.fireLogCleared()
	ll := make([][]byte, l.lines.Len())
	l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
	l.fireLogChanged(ll)
}

// Filter filters the model using either fuzzy or regexp.
func (l *Log) Filter(q string) {
	l.mx.Lock()
	l.filter = q
	l.mx.Unlock()

	l.fireLogCleared()
	l.fireLogBuffChanged(0)
}

func (l *Log) cancel() {
	l.mx.Lock()
	defer l.mx.Unlock()
	if l.cancelFn != nil {
		l.cancelFn()
		l.cancelFn = nil
	}
}

func (l *Log) load(ctx context.Context) error {
	accessor, err := dao.AccessorFor(l.factory, l.gvr)
	if err != nil {
		return err
	}
	loggable, ok := accessor.(dao.Loggable)
	if !ok {
		return fmt.Errorf("resource %s is not Loggable", l.gvr)
	}

	l.cancel()
	ctx = context.WithValue(ctx, internal.KeyFactory, l.factory)
	ctx, l.cancelFn = context.WithCancel(ctx)

	cc, err := loggable.TailLogs(ctx, l.logOptions)
	if err != nil {
		slog.Error("Tail logs failed", slogs.Error, err)
		l.cancel()
		l.fireLogError(err)
	}
	for _, c := range cc {
		go l.updateLogs(ctx, c)
	}

	return nil
}

// Append adds a log line.
func (l *Log) Append(line *dao.LogItem) {
	if line == nil || line.IsEmpty() {
		return
	}
	l.mx.Lock()
	defer l.mx.Unlock()
	l.logOptions.SinceTime = line.GetTimestamp()
	if l.lines.Len() < int(l.logOptions.Lines) {
		l.lines.Add(line)
		return
	}
	l.lines.Shift(line)
	l.lastSent--
	if l.lastSent < 0 {
		l.lastSent = 0
	}
}

// Notify fires of notifications to the listeners.
func (l *Log) Notify() {
	l.mx.Lock()
	defer l.mx.Unlock()

	if l.lastSent < l.lines.Len() {
		l.fireLogBuffChanged(l.lastSent)
		l.lastSent = l.lines.Len()
	}
}

// ToggleAllContainers toggles to show all containers logs.
func (l *Log) ToggleAllContainers(ctx context.Context) {
	l.logOptions.ToggleAllContainers()
	l.Restart(ctx)
}

func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
	for {
		select {
		case item, ok := <-c:
			if !ok {
				l.Append(item)
				l.Notify()
				return
			}
			if item == dao.ItemEOF {
				l.fireCanceled()
				return
			}
			l.Append(item)
			var overflow bool
			l.mx.RLock()
			overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines
			l.mx.RUnlock()
			if overflow {
				l.Notify()
			}
		case <-time.After(l.flushTimeout):
			l.Notify()
		case <-ctx.Done():
			return
		}
	}
}

// AddListener adds a new model listener.
func (l *Log) AddListener(listener LogsListener) {
	l.mx.Lock()
	defer l.mx.Unlock()

	l.listeners = append(l.listeners, listener)
}

// RemoveListener delete a listener from the list.
func (l *Log) RemoveListener(listener LogsListener) {
	l.mx.Lock()
	defer l.mx.Unlock()

	victim := -1
	for i, lis := range l.listeners {
		if lis == listener {
			victim = i
			break
		}
	}

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

func (l *Log) applyFilter(index int, q string) ([][]byte, error) {
	if q == "" {
		return nil, nil
	}
	matches, indices, err := l.lines.Filter(index, q, l.logOptions.ShowTimestamp)
	if err != nil {
		return nil, err
	}

	// No filter!
	if matches == nil {
		ll := make([][]byte, l.lines.Len())
		l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
		return ll, nil
	}
	// Blank filter
	if len(matches) == 0 {
		return nil, nil
	}
	filtered := make([][]byte, 0, len(matches))
	ll := make([][]byte, l.lines.Len())
	l.lines.Lines(index, l.logOptions.ShowTimestamp, ll)
	for i, idx := range matches {
		filtered = append(filtered, color.Highlight(ll[idx], indices[i], 209))
	}

	return filtered, nil
}

func (l *Log) fireLogBuffChanged(index int) {
	ll := make([][]byte, l.lines.Len()-index)
	if l.filter == "" {
		l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
	} else {
		ff, err := l.applyFilter(index, l.filter)
		if err != nil {
			l.fireLogError(err)
			return
		}
		ll = ff
	}

	if len(ll) > 0 {
		l.fireLogChanged(ll)
	}
}

func (l *Log) fireLogResume() {
	for _, lis := range l.listeners {
		lis.LogResume()
	}
}

func (l *Log) fireCanceled() {
	for _, lis := range l.listeners {
		lis.LogCanceled()
	}
}

func (l *Log) fireLogError(err error) {
	for _, lis := range l.listeners {
		lis.LogFailed(err)
	}
}

func (l *Log) fireLogChanged(lines [][]byte) {
	for _, lis := range l.listeners {
		lis.LogChanged(lines)
	}
}

func (l *Log) fireLogCleared() {
	var ll []LogsListener
	l.mx.RLock()
	ll = l.listeners
	l.mx.RUnlock()
	for _, lis := range ll {
		lis.LogCleared()
	}
}
