package cron

import (
	"context"
	"fmt"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/samber/lo"
	csngapiv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/server"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type SyncStandbyTiDBGroupJob struct {
	dataplaneManager dataplane.Manager
	ngService        server.Service
	lockRepo         repo.LockRepo
	expression       string
	defaultReplicas  int
	tenantReplicas   map[string]int
}

func NewSyncStandbyTiDBGroupJob(
	dataplaneManager dataplane.Manager,
	ngService server.Service,
	lockRepo repo.LockRepo,
	expression string,
	defaultReplicas int,
	tenantReplicas map[string]int,
) *SyncStandbyTiDBGroupJob {
	return &SyncStandbyTiDBGroupJob{
		dataplaneManager: dataplaneManager,
		ngService:        ngService,
		lockRepo:         lockRepo,
		expression:       expression,
		defaultReplicas:  defaultReplicas,
		tenantReplicas:   tenantReplicas,
	}
}

func (j *SyncStandbyTiDBGroupJob) Name() string {
	return "SyncStandbyTiDBGroupJob"
}

func (j *SyncStandbyTiDBGroupJob) CronExpression() string {
	return j.expression
}

func (j *SyncStandbyTiDBGroupJob) Execute(ctx context.Context) error {
	unlockFunc, err := j.lock(ctx)
	if err != nil {
		tilog.Warn(ctx, "failed to acquire lock for sync standby tidbgroup, skip this round", zap.Error(err))
		return nil
	}
	defer func() { _ = unlockFunc() }()

	// list all shared pools
	sharedPools, err := j.ngService.ListSharedPools(ctx, nil, nil)
	if err != nil {
		tilog.Error(ctx, "failed to list shared pools", zap.Error(err))
		return fmt.Errorf("failed to sync standby tidbgroup: %w", err)
	}

	for _, sharedPool := range sharedPools {
		if sharedPool.GetPhase() == csngapiv1beta.SharedPool_DELETING {
			continue
		}
		if !server.SharedPoolAllowsOrdinaryMutation(sharedPool) {
			tilog.Info(ctx, "skip standby tidbgroup sync while shared pool mutation is blocked",
				zap.String("sharedPoolId", sharedPool.GetId()),
				zap.String("pauseResumeState", string(sharedPool.PauseResumeState)))
			continue
		}
		remoteCli, err := j.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
		if err != nil {
			tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
			return fmt.Errorf("failed to get remote client for shared pool: %w", err)
		}
		tidbGroups := new(v1alpha1.TiDBGroupList)
		standbyTiDBgroups := new(v1alpha1.TiDBGroupList)
		{
			opt := client.MatchingLabels{
				"pingcap.com/component": "tidb",
			}
			if err := remoteCli.List(ctx, tidbGroups, opt); err != nil {
				tilog.Error(ctx, "failed to list tidbgroup", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
				return fmt.Errorf("failed to sync standby tidbroup: %w", err)
			}
		}
		{
			opt := client.MatchingLabels{
				"pingcap.com/component": "tidb-standby",
			}
			if err := remoteCli.List(ctx, standbyTiDBgroups, opt); err != nil {
				tilog.Error(ctx, "failed to list standby tidbgroup", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
				return fmt.Errorf("failed to sync standby tidbroup: %w", err)
			}
		}
		// iterate and check matching
		normalMatchCnt := make([]int, len(tidbGroups.Items))
		standbyMatchCnt := make([]int, len(standbyTiDBgroups.Items))
		for i := range tidbGroups.Items {
			for j := range standbyTiDBgroups.Items {
				normal := &(tidbGroups.Items[i])
				standby := &(standbyTiDBgroups.Items[j])
				if isStandbyMatch(normal, standby) {
					normalMatchCnt[i]++
					standbyMatchCnt[j]++
				}
			}
		}
		// delete standby tidbgroup
		for i, cnt := range standbyMatchCnt {
			if cnt > 0 {
				continue
			}
			standby := &standbyTiDBgroups.Items[i]
			if err := client.IgnoreNotFound(remoteCli.Delete(ctx, standby)); err != nil {
				tilog.Error(
					ctx,
					"failed to delete standby tidbgroup",
					zap.String("dataplaneName", sharedPool.GetDataplaneName()),
					zap.String("key", client.ObjectKeyFromObject(standby).String()),
					zap.Error(err),
				)
				return fmt.Errorf("failed to sync standby tidbroup: %w", err)
			}
		}
		// add standby tidbgroup
		for i, cnt := range normalMatchCnt {
			if cnt > 0 {
				continue
			}
			tenant := tidbGroups.Items[0].GetLabels()["tags.tidbcloud.com/tenant"]
			standby := tidbGroups.Items[i].DeepCopy()
			standby.ResourceVersion = ""
			standby.Name += "-standby"
			standby.Labels["pingcap.com/component"] = "tidb-standby"
			standby.Annotations = nil
			standby.Spec.Template.Spec.Server.Labels = nil
			standby.Spec.Template.Spec.Keyspace = ""
			standby.Spec.Template.Spec.Mode = v1alpha1.TiDBModeStandBy
			standby.Spec.Replicas = lo.ToPtr(int32(j.defaultReplicas))
			if replicas, ok := j.tenantReplicas[tenant]; ok {
				standby.Spec.Replicas = lo.ToPtr(int32(replicas))
			}
			if err := remoteCli.Create(ctx, standby); err != nil {
				tilog.Error(
					ctx,
					"failed to create standby tidbgroup",
					zap.String("dataplaneName", sharedPool.GetDataplaneName()),
					zap.String("key", client.ObjectKeyFromObject(standby).String()),
					zap.Error(err),
				)
				return fmt.Errorf("failed to sync standby tidbroup: %w", err)
			}
		}
	}
	return nil
}

func isStandbyMatch(tidbGroup, standbyTiDBGroup *v1alpha1.TiDBGroup) bool {
	normal := normalize(tidbGroup.Spec.Template.Spec.DeepCopy())
	standby := normalize(standbyTiDBGroup.Spec.Template.Spec.DeepCopy())
	opts := []cmp.Option{cmpopts.EquateEmpty(), cmpopts.IgnoreFields(v1alpha1.TiDBTemplateSpec{}, "Mode")}
	return cmp.Equal(normal, standby, opts...)
}

func normalize(spec *v1alpha1.TiDBTemplateSpec) *v1alpha1.TiDBTemplateSpec {
	spec.Keyspace = ""
	spec.Server.Labels = nil
	spec.Overlay.Pod.Annotations = nil
	spec.Overlay.Pod.Labels = nil
	for _, pvc := range spec.Overlay.PersistentVolumeClaims {
		pvc.Name = ""
		pvc.PersistentVolumeClaim.Labels = nil
		pvc.PersistentVolumeClaim.Annotations = nil
	}
	return spec
}

func (s *SyncStandbyTiDBGroupJob) lock(ctx context.Context) (func() error, error) {
	key := "sync_standby_tidb_group"
	lock := &entity.Lock{
		Key:         key,
		TimeoutSecs: 10,
	}

	unlockFunc, err := s.lockRepo.Lock(ctx, lock)
	if err != nil {
		return nil, err
	}
	return unlockFunc, nil
}
