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

package view

import (
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"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/perf"
	"github.com/derailed/k9s/internal/render"
	"github.com/derailed/k9s/internal/slogs"
	"github.com/derailed/k9s/internal/ui"
	"github.com/derailed/tcell/v2"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
)

// Service represents a service viewer.
type Service struct {
	ResourceViewer

	bench *perf.Benchmark
}

// NewService returns a new viewer.
func NewService(gvr *client.GVR) ResourceViewer {
	s := Service{
		ResourceViewer: NewPortForwardExtender(
			NewOwnerExtender(
				NewLogsExtender(NewBrowser(gvr), nil),
			),
		),
	}
	s.AddBindKeysFn(s.bindKeys)
	s.GetTable().SetEnterFn(s.showPods)

	return &s
}

// Protocol...

func (s *Service) bindKeys(aa *ui.KeyActions) {
	aa.Bulk(ui.KeyMap{
		ui.KeyB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true),
	})
}

func (s *Service) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) {
	var res dao.Service
	res.Init(a.factory, s.GVR())

	svc, err := res.GetInstance(path)
	if err != nil {
		a.Flash().Err(err)
		return
	}
	if svc.Spec.Type == v1.ServiceTypeExternalName {
		a.Flash().Warnf("No matching pods. Service %s is an external service.", path)
		return
	}
	if svc.Spec.Selector == nil {
		a.Flash().Warnf("No matching pods. Service %s does not provide any selectors", path)
		return
	}

	showPods(a, path, labels.SelectorFromSet(svc.Spec.Selector), "")
}

func (*Service) checkSvc(svc *v1.Service) error {
	if svc.Spec.Type != "NodePort" && svc.Spec.Type != "LoadBalancer" {
		return errors.New("you must select a reachable service")
	}
	return nil
}

func (*Service) getExternalPort(svc *v1.Service) (string, error) {
	if svc.Spec.Type == "LoadBalancer" {
		return "", nil
	}
	ports := render.ToPorts(svc.Spec.Ports)
	pp := strings.Split(ports, " ")
	// Grab the first port pair for now...
	tokens := strings.Split(pp[0], "►")
	if len(tokens) < 2 {
		return "", errors.New("no ports pair found")
	}

	return tokens[1], nil
}

func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
	if s.bench != nil {
		slog.Debug(">>> Benchmark canceled!!")
		s.App().Status(model.FlashErr, "Benchmark Canceled!")
		s.bench.Cancel()
		s.App().ClearStatus(true)
		return nil
	}

	path := s.GetTable().GetSelectedItem()
	if path == "" || s.bench != nil {
		return evt
	}

	cust, err := config.NewBench(s.App().BenchFile)
	if err != nil {
		slog.Debug("No bench config file found", slogs.FileName, s.App().BenchFile)
	}

	cfg, ok := cust.Benchmarks.Services[path]
	if !ok {
		s.App().Flash().Errf("No bench config found for service %s in %s", path, s.App().BenchFile)
		return nil
	}
	cfg.Name = path
	slog.Debug("Benchmark config", slogs.Config, cfg)

	svc, err := fetchService(s.App().factory, path)
	if err != nil {
		s.App().Flash().Err(err)
		return nil
	}
	if e := s.checkSvc(svc); e != nil {
		s.App().Flash().Err(e)
		return nil
	}
	port, err := s.getExternalPort(svc)
	if err != nil {
		s.App().Flash().Err(err)
		return nil
	}
	if err := s.runBenchmark(port, &cfg); err != nil {
		s.App().Flash().Errf("Benchmark failed %v", err)
		s.App().ClearStatus(false)
		s.bench = nil
	}

	return nil
}

// BOZO!! Refactor used by forwards.
func (s *Service) runBenchmark(port string, cfg *config.BenchConfig) error {
	if cfg.HTTP.Host == "" {
		return fmt.Errorf("invalid benchmark host %q", cfg.HTTP.Host)
	}

	var err error
	base := cfg.HTTP.Host
	if !strings.Contains(base, ":") {
		base += ":" + port + cfg.HTTP.Path
	} else {
		base += cfg.HTTP.Path
	}
	if strings.Index(base, "http") != 0 {
		base = "http://" + base
	}

	if s.bench, err = perf.NewBenchmark(base, s.App().version, cfg); err != nil {
		return err
	}

	s.App().Status(model.FlashWarn, "Benchmark in progress...")
	slog.Debug("Benchmark starting...")

	ct, err := s.App().Config.K9s.ActiveContext()
	if err != nil {
		return err
	}
	name := s.App().Config.K9s.ActiveContextName()

	go s.bench.Run(ct.ClusterName, name, s.benchDone)

	return nil
}

func (s *Service) benchDone() {
	slog.Debug("Bench Completed!")
	s.App().QueueUpdate(func() {
		if s.bench.Canceled() {
			s.App().Status(model.FlashInfo, "Benchmark canceled")
		} else {
			s.App().Status(model.FlashInfo, "Benchmark Completed!")
			s.bench.Cancel()
		}
		s.bench = nil
		go clearStatus(s.App())
	})
}

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

func clearStatus(app *App) {
	<-time.After(2 * time.Second)
	app.QueueUpdate(func() {
		app.ClearStatus(true)
	})
}

func fetchService(f dao.Factory, path string) (*v1.Service, error) {
	o, err := f.Get(client.SvcGVR, path, true, labels.Everything())
	if err != nil {
		return nil, err
	}

	var svc v1.Service
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)
	if err != nil {
		return nil, err
	}

	return &svc, nil
}
