package br

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

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	eventdata "github.com/tidbcloud/eventbus-datastruct/event"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	brrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/displayevent"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
	wf "github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

func (s *serviceImpl) CreateManualBackup(
	ctx context.Context,
	req *globalngv1.CreateManualBackupRequest,
) (*globalngv1.CreateManualBackupResponse, bizerr.BizError) {
	clusterID, err := strconv.ParseUint(req.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse cluster id")
	}
	if req.GetName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "name is required")
	}
	return s.createManualBackup(ctx, clusterID, req.GetName(), resourcepoolpolicy.OperationManualBackup, "", "")
}

func (s *serviceImpl) CreateFinalBackup(
	ctx context.Context,
	req *globalngv1.CreateFinalBackupRequest,
) (*globalngv1.CreateFinalBackupResponse, bizerr.BizError) {
	if req == nil || req.GetClusterId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is required")
	}

	response, berr := s.createManualBackup(
		ctx,
		req.GetClusterId(),
		"Final backup",
		resourcepoolpolicy.OperationManualBackup,
		"",
		schema.BackupPurposeFinal,
	)
	if berr != nil {
		return nil, berr
	}
	return &globalngv1.CreateFinalBackupResponse{BackupId: response.GetBackupId()}, nil
}

func (s *serviceImpl) CreatePauseManualBackup(
	ctx context.Context,
	clusterID uint64,
	operationID string,
) (*globalngv1.CreateManualBackupResponse, bizerr.BizError) {
	name := fmt.Sprintf("Pre-pause backup %s", operationID)
	if len(name) > 64 {
		name = name[:64]
	}
	return s.createManualBackup(
		ctx,
		clusterID,
		name,
		resourcepoolpolicy.OperationPausePreparationBackup,
		operationID,
		"",
	)
}

func (s *serviceImpl) createManualBackup(
	ctx context.Context,
	clusterID uint64,
	displayName string,
	operation resourcepoolpolicy.Operation,
	pauseOperationID string,
	purpose schema.BackupPurpose,
) (*globalngv1.CreateManualBackupResponse, bizerr.BizError) {
	clusterIDString := strconv.FormatUint(clusterID, 10)
	cluster, berr := s.clusterRepo.Get(ctx, clusterID, false)
	if berr != nil {
		return nil, berr.Chain("failed to get cluster")
	}
	if _, berr = s.regionRepo.Get(ctx, cluster.RegionId); berr != nil {
		return nil, berr.Chain("failed to get region")
	}
	sharedPoolID, err := strconv.ParseUint(cluster.GetSharedPoolId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse shared pool id")
	}
	if berr := s.checkResourcePoolForBRMutation(ctx, sharedPoolID, operation); berr != nil {
		return nil, berr
	}

	encryptionSnapshot, berr := s.backupEncryptionResolver.ResolveByCluster(ctx, cluster)
	if berr != nil {
		return nil, berr.Chain("failed to resolve backup encryption snapshot")
	}
	backup := &entity.Backup{Backup: &schema.Backup{
		State:           schema.BackupStatePending,
		TriggerType:     schema.BackupTriggerTypeManual,
		Type:            schema.BackupTypeSnapshot,
		DisplayName:     displayName,
		ClusterId:       clusterID,
		SharedPoolId:    sharedPoolID,
		TenantId:        cluster.GetOrganizationId(),
		RegionId:        globalcommonv1.Region_Name_name[int32(cluster.GetRegionId())],
		CloudProvider:   globalcommonv1.CloudProvider_Name_name[int32(cluster.GetCloudProviderId())],
		ServicePlan:     globalcommonv1.ServicePlan_name[int32(cluster.GetServicePlan())],
		ClusterVersion:  cluster.GetVersion(),
		CustomerManaged: encryptionSnapshot.CustomerManaged,
		EncryptionKeyId: encryptionSnapshot.EncryptionKeyId,
		ExpirationTime:  time.Time{},
	}}
	if pauseOperationID != "" {
		backup.PauseOperationId = &pauseOperationID
	}
	if purpose != "" {
		backup.Purpose = &purpose
	}

	metadataJSON, err := metadata.MarshalRequestMetaFromCtx(ctx)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to marshal metadata from context")
	}
	var createdBackupID uint64
	err = s.transaction.Transaction(ctx, func(ctx context.Context) error {
		if berr := s.lockResourcePoolForBRMutationWithOperation(ctx, sharedPoolID, operation); berr != nil {
			return berr
		}
		if purpose != "" {
			isDeleted := false
			existing, berr := s.backupRepo.UnscopedQuery(ctx, &brrepo.QueryBackupsFilter{
				ClusterIds: []uint64{clusterID},
				Purposes:   []schema.BackupPurpose{purpose},
				Deleted:    &isDeleted,
			}, &pagination.Page{PageSize: 1})
			if berr != nil {
				return berr
			}
			if len(existing) > 0 {
				createdBackupID = existing[0].ID
				return nil
			}
		} else if pauseOperationID != "" {
			isDeleted := false
			existing, berr := s.backupRepo.UnscopedQuery(ctx, &brrepo.QueryBackupsFilter{
				ClusterIds:        []uint64{clusterID},
				PauseOperationIds: []string{pauseOperationID},
				Deleted:           &isDeleted,
			}, &pagination.Page{PageSize: 1})
			if berr != nil {
				return berr
			}
			if len(existing) > 0 {
				createdBackupID = existing[0].ID
				return nil
			}
		}
		if berr := s.backupRepo.Create(ctx, backup); berr != nil {
			return berr.Chain("failed to create backup record")
		}
		createdBackupID = backup.ID
		regionalRequest := &clusterngv1beta.CreateManualBackupRequest{
			BackupId:  strconv.FormatUint(backup.ID, 10),
			ClusterId: clusterIDString,
		}
		requestJSON, err := protojson.Marshal(regionalRequest)
		if err != nil {
			return bizerr.WrapError(err, "failed to marshal create manual backup request")
		}
		if err := s.workflow.RunDagWithBizId(
			ctx,
			common.CreateManualBackup,
			strconv.FormatUint(cluster.GetOrganizationId(), 10),
			"0",
			clusterIDString,
			map[string]string{
				common.KeyRequest: string(requestJSON),
				wf.KeyTraceId:     wf.GetTraceId(ctx),
				wf.KeyRequestMeta: string(metadataJSON),
			},
		); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "create async workflow failed")
		}
		displayevent.Write(ctx, s.eventCommitter, newBackupDisplayEvent(
			ctx,
			backup,
			displayevent.BackupCluster,
			eventdata.EventStatusStarted,
			"Manual backup started.",
		))
		return nil
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to create backup record or run create manual backup dag")
	}

	tilog.Info(ctx, "manual backup created",
		zap.Uint64("backup_id", createdBackupID),
		zap.Uint64("cluster_id", clusterID),
		zap.String("pause_operation_id", pauseOperationID),
		zap.String("purpose", string(purpose)),
	)
	return &globalngv1.CreateManualBackupResponse{BackupId: strconv.FormatUint(createdBackupID, 10)}, nil
}
