package resources

import (
	"context"
	"math"
	"strconv"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gopkg.in/yaml.v3"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/pkg/diagcontroller/config"
	"github.com/tidbcloud/observability/pkg/diagcontroller/helper"
	diagstorage "github.com/tidbcloud/observability/pkg/diagcontroller/storage"
	"github.com/tidbcloud/observability/pkg/diagcontroller/tasks"
)

type DiagEngine struct {
	kubeCli client.Client
	conf    *config.DiagConfig
	store   diagstorage.Storage
}

func NewDiagEngine(
	kubeCli client.Client,
	conf *config.DiagConfig,
	store diagstorage.Storage,
) Resourcer {
	return &DiagEngine{
		kubeCli: kubeCli,
		conf:    conf,
		store:   store,
	}
}

func (d *DiagEngine) IsExist() bool {
	diagEngine := &appsv1.StatefulSet{}
	err := d.kubeCli.Get(context.Background(), types.NamespacedName{Namespace: d.conf.DiagEngine.Namespace, Name: d.conf.DiagEngine.Name}, diagEngine)
	if err != nil {
		if client.IgnoreNotFound(err) != nil {
			log.Error("failed to get diag-engine StatefulSet", zap.Error(err))
			return false
		}
		log.Info("diag-engine StatefulSet is not exist")
		return false
	}
	return true
}

func (d *DiagEngine) Offline() error {
	offlineEngines, err := helper.ListOfflineDiagEnginePods(d.kubeCli, d.conf.DiagEngine)
	if err != nil {
		return err
	}
	if offlineEngines == nil || len(offlineEngines.Items) == 0 {
		log.Info("no offline diag-engine pod, skip")
		return nil
	}

	diagEngine := &appsv1.StatefulSet{}
	if err := d.kubeCli.Get(context.Background(),
		types.NamespacedName{Namespace: d.conf.DiagEngine.Namespace, Name: d.conf.DiagEngine.Name}, diagEngine); err != nil {
		return errors.Annotate(err, "failed to get diag-engine StatefulSet")
	}
	if diagEngine.Spec.Replicas == nil {
		return errors.New("diag-engine replicas is nil")
	}
	if *diagEngine.Spec.Replicas <= 1 {
		return errors.New("diag-engine replicas is less than or equal to 1, can't offline it")
	}
	lastPodName := diagEngine.Name + "-" + strconv.Itoa(int(*diagEngine.Spec.Replicas-1))

	catalog, err := d.getCatalog()
	if err != nil {
		return err
	}
	canOffline := false
	for _, engine := range offlineEngines.Items {
		if engine.Name != lastPodName {
			log.Info("diag-engine is offline, but it's not the last pod, skip it", zap.String("Engine_Name", engine.Name))
			continue
		}
		if ts, ok := catalog.Tasks[engine.Name]; ok && len(ts) > 0 {
			log.Info("diag-engine is offline, but still has tasks, skip it", zap.String("Engine_Name", engine.Name))
			return nil
		}
		canOffline = true
		log.Info("diag-engine is offline, and it's the last pod, can offline it", zap.String("Engine_Name", engine.Name))
		break
		// TODO: add more checkers to check if the diag-engine has running tasks, such as to check api of diag-engine
	}

	if !canOffline {
		log.Info("no diag-engine can be offline, skip")
		return nil
	}

	if canOffline {
		*diagEngine.Spec.Replicas--
		if err := d.kubeCli.Update(context.Background(), diagEngine); err != nil {
			return errors.Annotate(err, "failed to update diag-engine StatefulSet")
		}
	}

	return nil
}

func (d *DiagEngine) ScaleUp(count int) error {
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		diagEngine := &appsv1.StatefulSet{}
		if err := d.kubeCli.Get(context.Background(), types.NamespacedName{Namespace: d.conf.DiagEngine.Namespace, Name: d.conf.DiagEngine.Name}, diagEngine); err != nil {
			return err
		}
		if diagEngine.Spec.Replicas == nil {
			return errors.New("diag-engine replicas is nil")
		}
		//nolint:gosec
		*diagEngine.Spec.Replicas += int32(count)

		if err := d.kubeCli.Update(context.Background(), diagEngine); err != nil {
			return err
		}
		//nolint:gosec
		log.Info("scale up diag-engine from 'Former_Replicas_Number' to 'Current_Replicas_Number' successfully",
			zap.Int32("Former_Replicas_Number", *diagEngine.Spec.Replicas-int32(count)),
			zap.Int32("Current_Replicas_Number", *diagEngine.Spec.Replicas),
		)
		return nil
	})
}

func (d *DiagEngine) ScaleDown(count int) error {
	engines, err := helper.ListAvailableEnginePods(d.kubeCli, d.conf.DiagEngine)
	if err != nil {
		return err
	}
	if len(engines.Items) <= 1 {
		log.Info("available diag-engine pods is less than 2, skill this scale down action")
		return nil
	}

	for idx := len(engines.Items) - 1; idx > len(engines.Items)-count-1; idx-- {
		if idx > 0 {
			if err := helper.MarkDiagEngineOffline(d.kubeCli, &engines.Items[idx]); err != nil {
				return err
			}
			log.Info("mark diag-engine offline successfully", zap.String("Engine_Name", engines.Items[idx].Name))
		}
	}

	return nil
}

func (d *DiagEngine) Interval() time.Duration {
	return d.conf.DiagEngine.Interval
}

func (d *DiagEngine) GetScaleCount() (int, error) {
	catalog, err := d.getCatalog()
	if err != nil {
		return 0, err
	}

	available, err := helper.ListAvailableEnginePods(d.kubeCli, d.conf.DiagEngine)
	if err != nil {
		return 0, err
	}

	tasksCount := catalog.TasksCount()
	availableEngineCount := len(available.Items)
	scaleDownCount := countScaleDownNumber(tasksCount, d.conf.DiagEngine.MinTasksPerEngine, availableEngineCount)
	if scaleDownCount > 0 {
		return -scaleDownCount, nil
	}

	scaleUpCount := countScaleUpNumber(tasksCount, d.conf.DiagEngine.MaxTasksPerEngine, availableEngineCount)
	if scaleUpCount > 0 {
		return scaleUpCount, nil
	}
	return 0, nil
}

func (d *DiagEngine) Name() string {
	return d.conf.DiagEngine.Name
}

func countScaleDownNumber(tasksCount int, minTasksPerEngine int, availableEngine int) int {
	scaleDownCount := 0
	if tasksCount < minTasksPerEngine*availableEngine {
		scaleDownCount = availableEngine - int(math.Ceil(float64(tasksCount)/float64(minTasksPerEngine)))
	}
	return scaleDownCount
}

func countScaleUpNumber(tasksCount int, maxTasksPerEngine int, availableEngine int) int {
	scaleUpCount := 0
	if tasksCount > maxTasksPerEngine*availableEngine {
		scaleUpCount = int(math.Ceil(float64(tasksCount)/float64(maxTasksPerEngine))) - availableEngine
	}
	return scaleUpCount
}

func (d *DiagEngine) getCatalog() (*tasks.Catalog, error) {
	catalog := &tasks.Catalog{}
	data, err := d.store.LoadObject(d.conf.Task.Bucket, d.conf.Task.CatalogPath)
	if err != nil {
		return nil, err
	}
	if err := yaml.Unmarshal(data, catalog); err != nil {
		return nil, err
	}
	return catalog, nil
}
