package configchange

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"time"

	opv2 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	v1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/utils/clinic_event"
	"github.com/tidbcloud/eks-provider/utils/tidb-operator/tidbcluster"
	"github.com/tidbcloud/eks-provider/utils/tidb-operator/v2utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/util/config"
)

var (
	// runningEventSendInterval is the interval to send event of running ConfigChange
	runningEventSendInterval = 2 * time.Minute
)

// compareConfBaseDesired compare conf key-value paris between desired and curr based on desired, to
// return all changed keys
func compareConfBaseDesired(desired, curr *config.GenericConfig) []string {
	flattedCurr := curr.FlatMP()
	flattedDesired := desired.FlatMP()

	var changedKeys []string
	for k, desiredValue := range flattedDesired {
		if !reflect.DeepEqual(desiredValue, flattedCurr[k]) {
			changedKeys = append(changedKeys, k)
		}
	}

	return changedKeys
}

func memberName(clusterName string, memberName cloudv1.ComponentType) string {
	return fmt.Sprintf("%s-%s", clusterName, memberName)
}

// FindConfigMapVolume returns the configmap which's name matches the predicate in a PodSpec, empty indicates not found
func FindConfigMapVolume(podSpec *corev1.PodSpec, pred func(string) bool) string {
	for _, vol := range podSpec.Volumes {
		if vol.ConfigMap != nil && pred(vol.ConfigMap.LocalObjectReference.Name) {
			return vol.ConfigMap.LocalObjectReference.Name
		}
	}
	return ""
}

// getAdvanceStatefulSet
// Note the spec is same, to avoid many issues dependents on k8s repo we DO NOT use the client pkg:
// https://github.com/pingcap/advanced-statefulset/tree/master/client
func getAdvanceStatefulSet(ctx context.Context, cli client.Client, key client.ObjectKey, sts *v1.StatefulSet) error {
	u := &unstructured.Unstructured{}
	u.SetGroupVersionKind(schema.GroupVersionKind{
		Group:   "apps.pingcap.com",
		Version: "v1",
		Kind:    "StatefulSet",
	})

	err := cli.Get(ctx, key, u)
	if err != nil {
		return err
	}

	data, err := u.MarshalJSON()
	if err != nil {
		return err
	}

	err = json.Unmarshal(data, sts)
	if err != nil {
		return err
	}

	return nil
}

func markComponentAndStepDoneForComponentNotDeployed(ctx context.Context, cc *cloudv1.ConfigChange, comp cloudv1.ComponentType, stepName cloudv1.ConfigChangeStepName) error {
	msg := fmt.Sprintf("%s is not deployed", comp)
	compStatus := cc.ReadOnlyComponentStatus(comp)
	compStatus.Done = true
	if err := cc.UpdateComponentStatus(ctx, comp, &compStatus); err != nil {
		return fmt.Errorf("update component(%s) status: %w", comp, err)
	}
	cc.UpdateStep(ctx, stepName, cloudv1.CCStepStatusCompleted, msg)

	return nil
}

func getComponentSpecFromTC(tc *opv1alpha1.TidbCluster, comp cloudv1.ComponentType) (*opv1alpha1.ComponentSpec, bool) {
	var compSpec *opv1alpha1.ComponentSpec

	switch comp {
	case cloudv1.ComponentPD:
		if tc.Spec.PD == nil || tc.Spec.PD.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.PD.ComponentSpec
	case cloudv1.ComponentTiProxy:
		if tc.Spec.TiProxy == nil || tc.Spec.TiProxy.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.TiProxy.ComponentSpec
	case cloudv1.ComponentTiDB:
		if tc.Spec.TiDB == nil || tc.Spec.TiDB.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.TiDB.ComponentSpec
	case cloudv1.ComponentTiKV:
		if tc.Spec.TiKV == nil || tc.Spec.TiKV.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.TiKV.ComponentSpec
	case cloudv1.ComponentTiFlash, cloudv1.ComponentTiFlashLearner: // tiflash and tiflashLearner two configurations but belong to the same component
		if tc.Spec.TiFlash == nil || tc.Spec.TiFlash.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.TiFlash.ComponentSpec
	case cloudv1.ComponentTiCDC:
		if tc.Spec.TiCDC == nil || tc.Spec.TiCDC.Replicas == 0 {
			return nil, false
		}
		compSpec = &tc.Spec.TiCDC.ComponentSpec
	default:
		return nil, false
	}

	return compSpec, true
}

func componentType2MemberType(comp cloudv1.ComponentType) opv1alpha1.MemberType {
	switch comp {
	case cloudv1.ComponentPD:
		return opv1alpha1.PDMemberType
	case cloudv1.ComponentTiProxy:
		return opv1alpha1.TiProxyMemberType
	case cloudv1.ComponentTiDB:
		return opv1alpha1.TiDBMemberType
	case cloudv1.ComponentTiKV:
		return opv1alpha1.TiKVMemberType
	case cloudv1.ComponentTiFlash, cloudv1.ComponentTiFlashLearner:
		return opv1alpha1.TiFlashMemberType
	case cloudv1.ComponentTiCDC:
		return opv1alpha1.TiCDCMemberType
	default:
		panic(fmt.Sprintf("unsupported compo: %s", comp))
	}
}

func sendConfigChangeStartEv(ctx context.Context, cc *cloudv1.ConfigChange, cluster *cloudv1.Cluster) error {
	contextId := fmt.Sprintf("%s-%s", clinic_event.EventNamePrefix, cc.Name)
	name := fmt.Sprintf("%s:%s", clinic_event.EventNamePrefix, clinic_event.ActionStart)
	ev := clinic_event.BuildGitopsEventTemplate(contextId, name, cluster)
	ev.Payload["changedComponents"] = cc.Status.ChangedComponents
	ev.Payload["changedCategories"] = cc.Status.ChangedCategories
	ev.Payload["db"] = cc.Status.DB
	ev.Payload["steps"] = cc.Status.Steps

	cc.OnEventSent(ctx)
	return clinic_event.SendEvent(ctx, ev)
}

func sendConfigChangeEv(ctx context.Context, oldCC, cc *cloudv1.ConfigChange, cluster *cloudv1.Cluster) error {
	if cc.Done() {
		return sendConfigChangeFinishEv(ctx, cc, cluster)
	} else {
		return sendConfigChangeRunningEv(ctx, oldCC, cc, cluster)
	}
}

func sendConfigChangeFinishEv(ctx context.Context, cc *cloudv1.ConfigChange, cluster *cloudv1.Cluster) error {
	contextId := fmt.Sprintf("%s-%s", clinic_event.EventNamePrefix, cc.Name)
	name := fmt.Sprintf("%s:%s", clinic_event.EventNamePrefix, clinic_event.ActionFinish)
	ev := clinic_event.BuildGitopsEventTemplate(contextId, name, cluster)
	ev.Payload["changedComponents"] = cc.Status.ChangedComponents
	ev.Payload["changedCategories"] = cc.Status.ChangedCategories
	ev.Payload["db"] = cc.Status.DB
	ev.Payload["steps"] = cc.Status.Steps

	cc.OnEventSent(ctx)
	return clinic_event.SendEvent(ctx, ev)
}

// sendConfigChangeRunningEv reports running ConfigChange status to clinic by sending event
func sendConfigChangeRunningEv(ctx context.Context, oldCC, cc *cloudv1.ConfigChange, cluster *cloudv1.Cluster) error {
	logger := log.FromContext(ctx)
	if equality.Semantic.DeepEqual(oldCC.Status, cc.Status) {
		logger.Info("no status changed, skip sending running event")
		return nil
	}

	now := time.Now()
	var lastSendTime time.Time
	if cc.Status.LastEventSendTime != nil {
		lastSendTime = cc.Status.LastEventSendTime.Time
	}
	if now.Sub(lastSendTime) < runningEventSendInterval {
		logger.Info(fmt.Sprintf("last send time is %s, skip sending running event", lastSendTime.String()))
		return nil
	}

	contextId := fmt.Sprintf("%s-%s", clinic_event.EventNamePrefix, cc.Name)
	name := fmt.Sprintf("%s:%s:%d", clinic_event.EventNamePrefix, clinic_event.ActionRun, now.Unix())
	ev := clinic_event.BuildGitopsEventTemplate(contextId, name, cluster)
	ev.Payload["changedComponents"] = cc.Status.ChangedComponents
	ev.Payload["changedCategories"] = cc.Status.ChangedCategories
	ev.Payload["db"] = cc.Status.DB
	ev.Payload["steps"] = cc.Status.Steps

	cc.OnEventSent(ctx)
	return clinic_event.SendEvent(ctx, ev)
}

func newTCGetter(ctx context.Context, cluster *cloudv1.Cluster, cli client.Client) (tidbcluster.Getter, error) {
	key := types.NamespacedName{Namespace: cluster.PrefixedId(), Name: tcName}
	if cluster.FeatureEnabled(consts.ClusterFeatureUseTiDBOperatorV2) {
		tcv2 := &opv2.Cluster{}
		if err := cli.Get(ctx, key, tcv2); err != nil {
			return nil, fmt.Errorf("failed to get cluster v2: %w", err)
		}
		var pdg opv2.PDGroup
		if err := cli.Get(ctx, key, &pdg); err != nil {
			return nil, fmt.Errorf("failed to get pd group: %w", err)
		}
		var kvg opv2.TiKVGroup
		if err := cli.Get(ctx, key, &kvg); err != nil {
			return nil, fmt.Errorf("failed to get tikv group: %w", err)
		}
		var dbg opv2.TiDBGroup
		if err := cli.Get(ctx, key, &dbg); err != nil {
			return nil, fmt.Errorf("failed to get tiflash group: %w", err)
		}
		flashg := &opv2.TiFlashGroup{}
		if cluster.Spec.Cluster.TiFlash.Replicas > 0 {
			if err := cli.Get(ctx, key, flashg); err != nil {
				return nil, fmt.Errorf("failed to get tiflash group: %w", err)
			}
		} else {
			flashg = nil
		}

		var extraGroups []*opv2.TiDBGroup
		for _, extraGroup := range cluster.GetExtraGroups() {
			if extraGroup.Spec.TiDB.Replicas > 0 {
				n := namer.NewNamer().WithTiDBGroupName(extraGroup.Name)
				key2 := types.NamespacedName{Namespace: cluster.PrefixedId(), Name: n.GetTiDBName()}
				var extra *opv2.TiDBGroup
				if err := cli.Get(ctx, key2, extra); err != nil {
					return nil, fmt.Errorf("failed to get extra group tidb group %s: %w", key, err)
				}
				extraGroups = append(extraGroups, extra)
			}
		}

		return &tidbcluster.V2Getter{
			Cli:          cli,
			Cluster:      tcv2,
			PDGroup:      &pdg,
			TiDBGroup:    &dbg,
			TiKVGroup:    &kvg,
			TiFlashGroup: flashg,
			ExtraGroups:  extraGroups,
		}, nil
	}

	tc := &opv1alpha1.TidbCluster{}
	if err := cli.Get(ctx, key, tc); err != nil {
		return nil, fmt.Errorf("failed to get tc: %w", err)
	}
	return tidbcluster.NewV1Getter(tc), nil
}

func areComponentGroupsSynced(ctx context.Context, cli client.Client, key client.ObjectKey, comp cloudv1.ComponentType, crName string) (bool, error) {
	switch comp {
	case cloudv1.ComponentPD:
		var list opv2.PDGroupList
		if err := cli.List(ctx, &list, client.InNamespace(key.Namespace)); err != nil {
			return false, fmt.Errorf("failed to list pd groups: %w", err)
		}
		for _, item := range list.Items {
			if !v2utils.IsSynced(item.Status.Conditions) ||
				!v2utils.IsUpToDate(item.GetGeneration(), item.Status.CommonStatus) ||
				item.Annotations[cloudv1.ConfigRevisionIdKey] != crName {
				return false, nil
			}
		}
	case cloudv1.ComponentTiDB:
		var list opv2.TiDBGroupList
		if err := cli.List(ctx, &list, client.InNamespace(key.Namespace)); err != nil {
			return false, fmt.Errorf("failed to list tidb groups: %w", err)
		}
		for _, item := range list.Items {
			if !v2utils.IsSynced(item.Status.Conditions) ||
				!v2utils.IsUpToDate(item.GetGeneration(), item.Status.CommonStatus) ||
				item.Annotations[cloudv1.ConfigRevisionIdKey] != crName {
				return false, nil
			}
		}
	case cloudv1.ComponentTiKV:
		var list opv2.TiKVGroupList
		if err := cli.List(ctx, &list, client.InNamespace(key.Namespace)); err != nil {
			return false, fmt.Errorf("failed to list tikv groups: %w", err)
		}
		for _, item := range list.Items {
			if !v2utils.IsSynced(item.Status.Conditions) ||
				!v2utils.IsUpToDate(item.GetGeneration(), item.Status.CommonStatus) ||
				item.Annotations[cloudv1.ConfigRevisionIdKey] != crName {
				return false, nil
			}
		}
	case cloudv1.ComponentTiFlash:
		var list opv2.TiFlashGroupList
		if err := cli.List(ctx, &list, client.InNamespace(key.Namespace)); err != nil {
			return false, fmt.Errorf("failed to list tiflash groups: %w", err)
		}
		for _, item := range list.Items {
			if !v2utils.IsSynced(item.Status.Conditions) ||
				!v2utils.IsUpToDate(item.GetGeneration(), item.Status.CommonStatus) ||
				item.Annotations[cloudv1.ConfigRevisionIdKey] != crName {
				return false, nil
			}
		}
	default:
		return false, fmt.Errorf("unsupported component type: %s", comp)
	}
	return true, nil
}
