package common

import (
	"fmt"

	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"golang.org/x/exp/slices"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/builder"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cmpver"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	// StorageClass should be defined at provider.Spec
	addIndexAccelerateStorageClassName = builder.AddIndexAccelerateStorageClassName
	addIndexAccelerateVolumeName       = builder.AddIndexAccelerateVolumeName
	addIndexAccelerateMountPath        = builder.AddIndexAccelerateMountPath

	tikvDedicatedRaftEngineStorageMountPath = "/var/lib/tikv-raft-engine"
	tikvDedicatedRaftEngineDir              = tikvDedicatedRaftEngineStorageMountPath + "/raft-engine"
	tiKVDedicatedRaftEngineVolumeName       = "raftengine"
)

var (
	minAddIndexAccelerateVersion = cmpver.MustNewConstraint(cmpver.GreaterOrEqual, "v6.3.0")
	// TODO: it should be allowed to be configured
	addIndexAccelerateVolumeSize = resource.MustParse("100Gi")
)

// overlaySuspendActions applies the suspend actions to the tidbcluster resource
//
// The flow for the suspending the cluster is as below:
//   - If `suspendAction` is set in the spec, mark the cluster as
//     suspending first by adding the `ComponentSuspended` condition in `resolveStatus`.
//   - Update TC with the `suspendAction` in the next reconcile,
//     in case that the user may resume the cluster immediately
//     after pausing the cluster
//   - Mark the cluster as suspended by updating the `ComponentSuspended`
//     condition to `True` after the cluster is suspended
//   - Remove the `ComponentSuspended` condition when the user resumes the cluster
func overlaySuspendActions(t *TiDBCluster, tc *opv1alpha1.TidbCluster) error {
	cluster := t.Cluster
	topo := cluster.Spec.Cluster

	if tc.Spec.TiDB != nil && !cluster.TiDBStatusNil() &&
		(cluster.Status.DB.TiDB.Suspending() || cluster.Status.DB.TiDB.Suspended()) {
		tc.Spec.TiDB.SuspendAction = convertSuspendActions(topo.TiDB.GetSuspendActions(topo.SuspendAction))
	}
	if tc.Spec.PD != nil && !cluster.PDStatusNil() &&
		(cluster.Status.DB.PD.Suspending() || cluster.Status.DB.PD.Suspended()) {
		tc.Spec.PD.SuspendAction = convertSuspendActions(topo.PD.GetSuspendActions(topo.SuspendAction))
	}
	if tc.Spec.TiKV != nil && !cluster.TiKVStatusNil() &&
		(cluster.Status.DB.TiKV.Suspending() || cluster.Status.DB.TiKV.Suspended()) {
		tc.Spec.TiKV.SuspendAction = convertSuspendActions(topo.TiKV.GetSuspendActions(topo.SuspendAction))
	}
	if topo.TiFlash.Replicas > 0 && tc.Spec.TiFlash != nil && !cluster.TiFlashStatusNil() &&
		(cluster.Status.DB.TiFlash.Suspending() || cluster.Status.DB.TiFlash.Suspended()) {
		tc.Spec.TiFlash.SuspendAction = convertSuspendActions(topo.TiFlash.GetSuspendActions(topo.SuspendAction))
	}
	if topo.TiProxy.Replicas > 0 && tc.Spec.TiProxy != nil && !cluster.TiProxyStatusNil() &&
		(cluster.Status.DB.TiProxy.Suspending() || cluster.Status.DB.TiProxy.Suspended()) {
		tc.Spec.TiProxy.SuspendAction = convertSuspendActions(topo.TiProxy.GetSuspendActions(topo.SuspendAction))
	}
	return nil
}

func overlayTiDBGroupSuspendActions(t *TiDBGroup, tc *opv1alpha1.TidbCluster) error {
	cluster := t.Cluster
	topo := cluster.Spec.Cluster

	if tc.Spec.TiDB != nil && !cluster.TiDBStatusNil() &&
		(cluster.Status.DB.TiDB.Suspending() || cluster.Status.DB.TiDB.Suspended()) {
		tc.Spec.TiDB.SuspendAction = convertSuspendActions(topo.TiDB.GetSuspendActions(topo.SuspendAction))
	}
	return nil
}

// convertSuspendActions converts the cloudv1.SuspendAction to v1alpha1.SuspendAction
func convertSuspendActions(action *cloudv1.SuspendAction) *opv1alpha1.SuspendAction {
	if action == nil {
		return nil
	}
	return &opv1alpha1.SuspendAction{SuspendStatefulSet: action.SuspendCompute}
}

func overlayTiDBAddIndex(ctx *ClusterContext, tc *opv1alpha1.TidbCluster) error {
	if addIndexAccelerateSupported(tc.TiDBVersion(), tc.Spec.TiDB.Requests.Cpu()) {
		if tc.Spec.TiDB.Config == nil {
			tc.Spec.TiDB.Config = opv1alpha1.NewTiDBConfig()
		}
		tc.Spec.TiDB.Config.Set("temp-dir", addIndexAccelerateMountPath)

		vol, mount := getIndexAccelerateVolumeAndVolumeMount(ctx.Target.Spec.Cluster.TiDB.InstanceType)
		found := false
		for i := range tc.Spec.TiDB.AdditionalVolumes {
			cur := &tc.Spec.TiDB.AdditionalVolumes[i]
			if cur.Name == vol.Name {
				tc.Spec.TiDB.AdditionalVolumes[i] = vol
				found = true
			}
		}
		if !found {
			tc.Spec.TiDB.AdditionalVolumes = append(tc.Spec.TiDB.AdditionalVolumes, vol)
		}

		found = false
		for i := range tc.Spec.TiDB.AdditionalVolumeMounts {
			cur := &tc.Spec.TiDB.AdditionalVolumeMounts[i]
			if cur.Name == mount.Name {
				tc.Spec.TiDB.AdditionalVolumeMounts[i] = mount
				found = true
			}
		}
		if !found {
			tc.Spec.TiDB.AdditionalVolumeMounts = append(tc.Spec.TiDB.AdditionalVolumeMounts, mount)
		}
	}

	return nil
}

func getIndexAccelerateVolumeAndVolumeMount(instanceType string) (corev1.Volume, corev1.VolumeMount) {
	return corev1.Volume{
			Name: addIndexAccelerateVolumeName,
			VolumeSource: corev1.VolumeSource{
				Ephemeral: &corev1.EphemeralVolumeSource{
					VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{
						ObjectMeta: metav1.ObjectMeta{},
						Spec: corev1.PersistentVolumeClaimSpec{
							AccessModes: []corev1.PersistentVolumeAccessMode{
								corev1.ReadWriteOnce,
							},
							StorageClassName: ptr.To(builder.GetAddIndexAccelerateStorageClassName(instanceType)),
							Resources: corev1.VolumeResourceRequirements{
								Requests: corev1.ResourceList{
									corev1.ResourceStorage: addIndexAccelerateVolumeSize,
								},
							},
						},
					},
				},
			},
		}, corev1.VolumeMount{
			Name:      addIndexAccelerateVolumeName,
			MountPath: addIndexAccelerateMountPath,
		}
}

// TODO: remove the condition of CPU
func addIndexAccelerateSupported(version string, cpu *resource.Quantity) bool {
	ok, err := minAddIndexAccelerateVersion.Check(version)
	if err != nil {
		return false
	}

	return ok && cpu.CmpInt64(4) > 0
}

func AddIndexAccelerateSupported(version string) bool {
	ok, err := minAddIndexAccelerateVersion.Check(version)
	if err != nil {
		return false
	}
	return ok
}

func overlayTiKVStorageVolumes(t *TiDBCluster, tc *opv1alpha1.TidbCluster) error {
	for _, vol := range t.Cluster.Spec.Cluster.TiKV.StorageVolumes {
		storageClassName := vol.StorageClassName
		tc.Spec.TiKV.StorageVolumes = append(tc.Spec.TiKV.StorageVolumes, opv1alpha1.StorageVolume{
			Name:             vol.Name,
			StorageClassName: &storageClassName,
			StorageSize:      vol.StorageSize.String(),
			MountPath:        vol.MountPath,
		})
	}
	return nil
}

func overlayTiKVForDedicatedRaftEngineStorage(t *TiDBCluster, tc *opv1alpha1.TidbCluster) error {
	storage := t.Cluster.Spec.Cluster.TiKV.DedicatedRaftEngineStorage
	if storage == nil {
		return nil
	}
	if storage.StorageSize.IsZero() {
		return fmt.Errorf("dedicated raft engine storage size must be set")
	}

	tc.Spec.TiKV.Config.Set("raft-engine.dir", tikvDedicatedRaftEngineDir)
	tc.Spec.TiKV.StorageVolumes = append(tc.Spec.TiKV.StorageVolumes, opv1alpha1.StorageVolume{
		Name:             tiKVDedicatedRaftEngineVolumeName,
		StorageSize:      storage.StorageSize.String(),
		StorageClassName: storage.StorageClassName,
		MountPath:        tikvDedicatedRaftEngineStorageMountPath,
	})
	return nil
}

func mutateTiKVForDedicatedRaftEngineStorage(curTc, newTc *opv1alpha1.TidbCluster) error {
	find := func(volume opv1alpha1.StorageVolume) bool {
		return volume.Name == tiKVDedicatedRaftEngineVolumeName
	}
	indexInCurTC := slices.IndexFunc(curTc.Spec.TiKV.StorageVolumes, find)
	indexInNewTC := slices.IndexFunc(newTc.Spec.TiKV.StorageVolumes, find)
	if indexInCurTC != indexInNewTC {
		return fmt.Errorf("dedicated raft engine storage is not allowed to be modified")
	}
	if indexInCurTC == -1 {
		return nil
	}

	// curTc don't contain this field because the config is overwritten by the config manager,
	// so that we need to set it always.
	curTc.Spec.TiKV.Config.Set("raft-engine.dir", tikvDedicatedRaftEngineDir)

	// support to scale up the storage
	size, err := util.MaxQuantityStr(
		curTc.Spec.TiKV.StorageVolumes[indexInCurTC].StorageSize,
		newTc.Spec.TiKV.StorageVolumes[indexInNewTC].StorageSize,
	)
	if err != nil {
		return fmt.Errorf("parse dedicated raft engine storage size failed: %v", err)
	}
	curTc.Spec.TiKV.StorageVolumes[indexInCurTC].StorageSize = size

	return nil
}
