package view

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"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/ui"
	"github.com/derailed/tview"
	"github.com/gdamore/tcell"
	"github.com/rs/zerolog/log"
)

const (
	logTitle    = "logs"
	logBuffSize = 100

	// FlushTimeout represents a duration between log flushes.
	FlushTimeout = 200 * time.Millisecond

	logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
	logFmt   = " Logs([fg:bg:]%s) "
)

// Log represents a generic log viewer.
type Log struct {
	*tview.Flex

	app             *App
	logs            *Details
	indicator       *LogIndicator
	ansiWriter      io.Writer
	path, container string
	cancelFn        context.CancelFunc
	previous        bool
	gvr             client.GVR
}

var _ model.Component = &Log{}

// NewLog returns a new viewer.
func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
	return &Log{
		gvr:       gvr,
		Flex:      tview.NewFlex(),
		path:      path,
		container: co,
		previous:  prev,
	}
}

// Init initialiazes the viewer.
func (l *Log) Init(ctx context.Context) (err error) {
	log.Debug().Msgf(">>> Logs INIT")
	if l.app, err = extractApp(ctx); err != nil {
		return err
	}

	l.SetBorder(true)
	l.SetBorderPadding(0, 0, 1, 1)
	l.SetDirection(tview.FlexRow)

	l.indicator = NewLogIndicator(l.app.Styles)
	l.AddItem(l.indicator, 1, 1, false)

	l.logs = NewDetails(l.app, "", "")
	if err = l.logs.Init(ctx); err != nil {
		return err
	}
	l.logs.SetWrap(false)
	l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize)

	l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor, l.app.Styles.Views().Log.BgColor)
	l.AddItem(l.logs, 0, 1, true)
	l.bindKeys()
	l.logs.SetInputCapture(l.keyboard)

	l.StylesChanged(l.app.Styles)
	l.app.Styles.AddListener(l)

	return nil
}

// StylesChanged reports skin changes.
func (l *Log) StylesChanged(s *config.Styles) {
	l.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
	l.logs.SetTextColor(config.AsColor(s.Views().Log.FgColor))
	l.logs.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
}

// Hints returns a collection of menu hints.
func (l *Log) Hints() model.MenuHints {
	return l.logs.Actions().Hints()
}

// Start runs the component.
func (l *Log) Start() {
	l.Stop()
	if err := l.doLoad(); err != nil {
		l.app.Flash().Err(err)
		l.log("😂 Doh! No logs are available at this time. Check again later on...")
		return
	}
	l.app.SetFocus(l)
}

// Stop terminates the component.
func (l *Log) Stop() {
	if l.cancelFn != nil {
		log.Debug().Msgf("<<<< Logger STOP!")
		l.cancelFn()
		l.cancelFn = nil
	}
	l.app.Styles.RemoveListener(l)
}

// Name returns the component name.
func (l *Log) Name() string { return logTitle }

func (l *Log) bindKeys() {
	l.logs.Actions().Set(ui.KeyActions{
		tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true),
		ui.KeyC:         ui.NewKeyAction("Clear", l.clearCmd, true),
		ui.KeyS:         ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
		ui.KeyF:         ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
		ui.KeyW:         ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
		tcell.KeyCtrlS:  ui.NewKeyAction("Save", l.SaveCmd, true),
	})
}

func (l *Log) doLoad() error {
	l.logs.Clear()
	l.setTitle(l.path, l.container)

	var ctx context.Context
	ctx = context.WithValue(context.Background(), internal.KeyFactory, l.app.factory)
	ctx, l.cancelFn = context.WithCancel(ctx)

	c := make(chan string, 10)
	go l.updateLogs(ctx, c, logBuffSize)

	accessor, err := dao.AccessorFor(l.app.factory, l.gvr)
	if err != nil {
		return err
	}
	logger, ok := accessor.(dao.Loggable)
	if !ok {
		return fmt.Errorf("Resource %s is not tailable", l.gvr)
	}

	if err := logger.TailLogs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil {
		l.cancelFn()
		close(c)
		return err
	}

	return nil
}

func (l *Log) logOpts(path, co string, prevLogs bool) dao.LogOptions {
	return dao.LogOptions{
		Path:      path,
		Container: co,
		Lines:     int64(l.app.Config.K9s.LogRequestSize),
		Previous:  prevLogs,
	}
}

func (l *Log) updateLogs(ctx context.Context, c <-chan string, buffSize int) {
	defer func() {
		log.Debug().Msgf("updateLogs view bailing out!")
	}()
	buff, index := make([]string, buffSize), 0
	for {
		select {
		case line, ok := <-c:
			if !ok {
				log.Debug().Msgf("Closed channel detected. Bailing out...")
				l.Flush(index, buff)
				return
			}
			if index < buffSize {
				buff[index] = line
				index++
				continue
			}
			l.Flush(index, buff)
			index = 0
			buff[index] = line
			index++
		case <-time.After(FlushTimeout):
			l.Flush(index, buff)
			index = 0
		case <-ctx.Done():
			return
		}
	}
}

// Indicator returns the scroll mode viewer.
func (l *Log) Indicator() *LogIndicator {
	return l.indicator
}

func (l *Log) setTitle(path, co string) {
	var fmat string
	if co == "" {
		fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame())
	} else {
		fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame())
	}
	l.path = path
	l.SetTitle(fmat)
}

func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
	key := evt.Key()
	if key == tcell.KeyRune {
		key = tcell.Key(evt.Rune())
	}
	if m, ok := l.logs.Actions()[key]; ok {
		log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
		return m.Action(evt)
	}

	return evt
}

// Logs returns the log viewer.
func (l *Log) Logs() *Details {
	return l.logs
}

func (l *Log) log(lines string) {
	fmt.Fprintln(l.ansiWriter, tview.Escape(lines))
	log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
}

// Flush write logs to viewer.
func (l *Log) Flush(index int, buff []string) {
	if index == 0 || !l.indicator.AutoScroll() {
		return
	}
	l.log(strings.Join(buff[:index], "\n"))
	l.app.QueueUpdateDraw(func() {
		l.indicator.Refresh()
		l.logs.ScrollToEnd()
	})
}

// ----------------------------------------------------------------------------
// Actions()...

// SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey {
	if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil {
		l.app.Flash().Err(err)
	} else {
		l.app.Flash().Infof("Log %s saved successfully!", path)
	}
	return nil
}

func ensureDir(dir string) error {
	return os.MkdirAll(dir, 0744)
}

func saveData(cluster, name, data string) (string, error) {
	dir := filepath.Join(config.K9sDumpDir, cluster)
	if err := ensureDir(dir); err != nil {
		return "", err
	}

	now := time.Now().UnixNano()
	fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now)

	path := filepath.Join(dir, fName)
	mod := os.O_CREATE | os.O_WRONLY
	file, err := os.OpenFile(path, mod, 0600)
	if err != nil {
		log.Error().Err(err).Msgf("LogFile create %s", path)
		return "", nil
	}
	defer func() {
		if err := file.Close(); err != nil {
			log.Error().Err(err).Msg("Closing Log file")
		}
	}()
	if _, err := file.Write([]byte(data)); err != nil {
		return "", err
	}

	return path, nil
}

func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
	l.app.Flash().Info("Clearing logs...")
	l.logs.Clear()
	l.logs.ScrollTo(0, 0)
	return nil
}

func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
	l.indicator.ToggleTextWrap()
	l.logs.SetWrap(l.indicator.textWrap)
	return nil
}

func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
	l.indicator.ToggleAutoScroll()
	return nil
}

func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey {
	l.indicator.ToggleFullScreen()
	sidePadding := 1
	if l.indicator.FullScreen() {
		sidePadding = 0
	}
	l.SetFullScreen(l.indicator.FullScreen())
	l.Box.SetBorder(!l.indicator.FullScreen())
	l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding)

	return nil
}
