package privatelinkservicemigration

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/samber/lo"
	clusterdedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"google.golang.org/protobuf/types/known/timestamppb"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/privatelinkservicemigration"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/randstr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

func (svc *service) Create(
	ctx context.Context,
	req *globaldedicatedv1.CreatePrivateLinkServiceMigrationRequest,
) (*globaldedicatedv1.PrivateLinkServiceMigration, error) {
	_, span := tracer.Start(ctx, "Domain.CreatePrivateLinkServiceMigrationSvc")
	defer span.End()

	// validate the migration
	// check the source and target cluster is exist and the cluster is available
	// check the source and target cluster is in the same region and same project
	// check source tidbgroups belong to source cluster
	// check target tidbgroups belong to target cluster
	// check the source tidbgroup and target tidbgroup is exist
	// check the existing migration with the same source and target tidbgroup is exist
	// check source tidbgroup private link service is exist
	// if validate_only is true, return success
	// store the migration in the database
	// create migration workflow
	// workflow for one migration tidbgroup
	// * delete target tidbgroup private link service
	// * disable dns on target tidbgroup
	// * disable dns on source tidbgroup
	// * set target tidbgroup dns identity(use source target tidbgroup dns identity) and enable target tidbgroup dns
	// * cutover private link service to target tidbgroup
	// * set new dns identity on source tidbgroup and enable dns
	// * create new private link service on source tidbgroup with new private link service id

	migration := req.GetMigration()
	if err := svc.validateMigration(migration); err != nil {
		return nil, err
	}

	sourceCluster, err := svc.clusterRepo.Get(ctx, migration.GetSourceClusterId(), false)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to get source cluster")
	}
	targetCluster, err := svc.clusterRepo.Get(ctx, migration.GetTargetClusterId(), false)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to get target cluster")
	}
	if err := svc.validateSourceClusterAndTargetCluster(sourceCluster, targetCluster); err != nil {
		return nil, err
	}
	if migration.GetMigrationTidbGroups() == nil || len(migration.GetMigrationTidbGroups()) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "migration tidb groups is required")
	}
	if err := svc.validateMigrationTidbGroups(ctx, migration.GetMigrationTidbGroups(), sourceCluster, targetCluster); err != nil {
		return nil, err
	}
	region, err := svc.regionRepo.SelectRegionById(ctx, sourceCluster.RegionId, sourceCluster.OrganizationId)
	if err != nil {
		return nil, err
	}

	// make source privatelink is ready
	for _, migrationTidbGroup := range migration.GetMigrationTidbGroups() {
		pls, err := svc.regionserverCli.GetPrivateLinkService(ctx, region, &regionserver.GetPrivateLinkServiceRequest{
			ClusterId:   strconv.FormatUint(sourceCluster.ClusterId, 10),
			TidbGroupId: strconv.FormatUint(migrationTidbGroup.GetSourceTidbGroupId(), 10),
		})
		if err != nil {
			return nil, err
		}
		if pls == nil {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"%s",
				fmt.Sprintf("source private link service is not found, tidb group id: %d", migrationTidbGroup.GetSourceTidbGroupId()),
			)
		}
		if pls.Status != regionaldedicatedv1.PrivateLinkService_ACTIVE {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"%s",
				fmt.Sprintf(
					"source private link service is not active, status: %s, tidb group id: %d",
					pls.Status.String(),
					migrationTidbGroup.GetSourceTidbGroupId(),
				),
			)
		}
	}

	if err := svc.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check if the migration tidb groups is exist
		allGroupIds := lo.Map(
			migration.GetMigrationTidbGroups(),
			func(each *globaldedicatedv1.PrivateLinkServiceMigration_MigrationTidbGroup, _ int) uint64 {
				return each.GetSourceTidbGroupId()
			},
		)
		allGroupIds = append(
			allGroupIds,
			lo.Map(migration.GetMigrationTidbGroups(), func(each *globaldedicatedv1.PrivateLinkServiceMigration_MigrationTidbGroup, _ int) uint64 {
				return each.GetTargetTidbGroupId()
			})...)
		existingMigrationTidbGroups, err := svc.privatelinkservicemigrationRepo.ListMigrationTidbGroups(
			ctx,
			&privatelinkservicemigration.MigrationTidbGroupFilter{
				SourceTidbGroupIDs: allGroupIds,
				TargetTidbGroupIDs: allGroupIds,
				States: []globaldedicatedv1.PrivateLinkServiceMigration_State{
					globaldedicatedv1.PrivateLinkServiceMigration_STATE_UNSPECIFIED,
					globaldedicatedv1.PrivateLinkServiceMigration_PENDING,
					globaldedicatedv1.PrivateLinkServiceMigration_RUNNING,
				},
			},
		)
		if err != nil {
			return err
		}
		if len(existingMigrationTidbGroups) > 0 {
			return bizerr.New(bizerr.ErrCodeParams, "migration tidb groups are already exist")
		}
		if req.GetValidateOnly() {
			return nil
		}

		migration.Id = svc.idGeneratorFacade.MustNew()
		migration.OrganizationId = sourceCluster.OrganizationId
		migration.ProjectId = sourceCluster.ProjectId
		migration.CreateTime = timestamppb.Now()
		migration.UpdateTime = timestamppb.Now()
		_, err = svc.privatelinkservicemigrationRepo.Create(ctx, migration)
		if err != nil {
			return err
		}
		// update migration state
		if _, err := svc.privatelinkservicemigrationRepo.UpdateState(
			ctx,
			migration.Id,
			globaldedicatedv1.PrivateLinkServiceMigration_RUNNING,
			"migration started",
		); err != nil {
			return err
		}
		for _, migrationTidbGroup := range migration.GetMigrationTidbGroups() {
			// update migration tidb group state
			migrationTidbGroup.State = globaldedicatedv1.PrivateLinkServiceMigration_RUNNING
			migrationTidbGroup.StartTime = timestamppb.New(time.Now())
			if _, err := svc.privatelinkservicemigrationRepo.UpdateMigrationTidbGroup(ctx, migration.Id, migrationTidbGroup); err != nil {
				return err
			}
			sourceTidbGroup, err := svc.regionserverCli.GetTidbGroup(ctx, region, &clusterdedicatedv1.GetTidbGroupRequest{
				ClusterId:   strconv.FormatUint(sourceCluster.ClusterId, 10),
				TidbGroupId: strconv.FormatUint(migrationTidbGroup.GetSourceTidbGroupId(), 10),
			})
			if err != nil {
				return err
			}
			if sourceTidbGroup == nil {
				return bizerr.New(bizerr.ErrCodeRecordNotFound, "source tidb group is not found")
			}

			newPrivateLinkServiceId := svc.idGeneratorFacade.MustNew()
			werr := svc.workflow.RunDagWithBizId(ctx,
				common.CutoverPrivateLinkService,
				strconv.FormatUint(sourceCluster.OrganizationId, 10),
				strconv.FormatUint(sourceCluster.ProjectId, 10),
				strconv.FormatUint(migration.Id, 10),
				map[string]string{
					workflow.KeyTraceId:                     workflow.GetTraceId(ctx),
					common.KeyPrivateLinkServiceMigrationId: strconv.FormatUint(migration.Id, 10),
					common.KeySourceClusterId:               strconv.FormatUint(sourceCluster.ClusterId, 10),
					common.KeySourceTidbGroupId:             strconv.FormatUint(migrationTidbGroup.GetSourceTidbGroupId(), 10),
					common.KeyTargetClusterId:               strconv.FormatUint(targetCluster.ClusterId, 10),
					common.KeyTargetTidbGroupId:             strconv.FormatUint(migrationTidbGroup.GetTargetTidbGroupId(), 10),
					common.KeyDNSIdentity:                   sourceTidbGroup.GetClusterIdentifierHash(),
					common.KeyNewDNSIdentity:                randstr.GenDnsIdentity(), // new dns identity for source tidb group after cutover
					common.KeyNewPrivateLinkServiceId:       strconv.FormatUint(newPrivateLinkServiceId, 10),
				})
			if werr != nil {
				return werr
			}
		}

		// poll private link service migration status
		// create new dag to poll private link service migration status
		if err := svc.workflow.RunDagWithBizId(ctx,
			common.PollPrivateLinkServiceMigrationStatus,
			strconv.FormatUint(sourceCluster.OrganizationId, 10),
			strconv.FormatUint(sourceCluster.ProjectId, 10),
			strconv.FormatUint(migration.Id, 10),
			map[string]string{
				workflow.KeyTraceId:                     workflow.GetTraceId(ctx),
				common.KeyPrivateLinkServiceMigrationId: strconv.FormatUint(migration.Id, 10),
			},
		); err != nil {
			return bizerr.WrapError(err, "failed to run poll private link service migration status workflow")
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return migration, nil
}

func (svc *service) validateSourceClusterAndTargetCluster(
	sourceCluster *entity.Cluster,
	targetCluster *entity.Cluster,
) bizerr.BizError {
	if sourceCluster == nil {
		return bizerr.New(bizerr.ErrCodeParams, "source cluster is not found")
	}
	if sourceCluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(bizerr.ErrCodeParams, "source cluster is not available")
	}
	if targetCluster == nil {
		return bizerr.New(bizerr.ErrCodeParams, "target cluster is not found")
	}
	if targetCluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(bizerr.ErrCodeParams, "target cluster is not available")
	}
	if sourceCluster.RegionId != targetCluster.RegionId {
		return bizerr.New(bizerr.ErrCodeParams, "source cluster and target cluster are not in the same region")
	}
	if sourceCluster.OrganizationId != targetCluster.OrganizationId {
		return bizerr.New(bizerr.ErrCodeParams, "source cluster and target cluster are not in the same organization")
	}
	return nil
}

func (svc *service) validateMigration(migration *globaldedicatedv1.PrivateLinkServiceMigration) bizerr.BizError {
	if migration == nil {
		return bizerr.New(bizerr.ErrCodeParams, "migration is required")
	}
	if migration.GetSourceClusterId() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "source cluster id is required")
	}
	if migration.GetTargetClusterId() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "target cluster id is required")
	}

	return nil
}

func (svc *service) validateMigrationTidbGroups(
	ctx context.Context,
	migrationTidbGroups []*globaldedicatedv1.PrivateLinkServiceMigration_MigrationTidbGroup,
	sourceCluster *entity.Cluster,
	targetCluster *entity.Cluster,
) bizerr.BizError {
	for _, migrationTidbGroup := range migrationTidbGroups {
		if migrationTidbGroup.GetSourceTidbGroupId() == 0 {
			return bizerr.New(bizerr.ErrCodeParams, "source tidb group id is required")
		}
		if migrationTidbGroup.GetTargetTidbGroupId() == 0 {
			return bizerr.New(bizerr.ErrCodeParams, "target tidb group id is required")
		}
		if migrationTidbGroup.GetSourceTidbGroupId() == migrationTidbGroup.GetTargetTidbGroupId() {
			return bizerr.New(bizerr.ErrCodeParams, "source tidb group id and target tidb group id cannot be the same")
		}
		sourceTidbGroup, err := svc.tidbGroupRepo.Load(ctx, sourceCluster.ClusterId, migrationTidbGroup.GetSourceTidbGroupId())
		if err != nil {
			return bizerr.WrapError(err, "failed to get source tidb group")
		}
		if sourceTidbGroup == nil {
			return bizerr.New(bizerr.ErrCodeParams, "source tidb group is not found")
		}
		targetTidbGroup, err := svc.tidbGroupRepo.Load(ctx, targetCluster.ClusterId, migrationTidbGroup.GetTargetTidbGroupId())
		if err != nil {
			return bizerr.WrapError(err, "failed to get target tidb group")
		}
		if targetTidbGroup == nil {
			return bizerr.New(bizerr.ErrCodeParams, "target tidb group is not found")
		}
		if sourceTidbGroup.Id == targetTidbGroup.Id {
			return bizerr.New(bizerr.ErrCodeParams, "source tidb group and target tidb group cannot be the same")
		}
	}
	return nil
}
