package br

import (
	"context"
	"fmt"
	"strconv"

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

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	brrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	clusterrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"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"
	wf "github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

type restoreFromManualAdapter struct {
	workflow         wf.Workflow
	transaction      db.Transactioner
	backupRepo       brrepo.BackupRepo
	restoreRepo      brrepo.RestoreRepo
	clusterRepo      clusterrepo.Repo
	regionRepo       regionrepo.Repo
	regionalCli      regionalserver.Client
	eventCommitter   eventbus.WALCommitter
	resourcePoolRepo resourcepoolrepo.Repo
}

func (a *restoreFromManualAdapter) PrepareRestore(
	ctx context.Context,
	req *globalngv1.RestoreClusterRequest,
) (submitRestore, bizerr.BizError) {
	backupId, err := strconv.ParseUint(req.GetBackupId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse backup id")
	}

	sourceClusterId, err := strconv.ParseUint(req.GetSourceClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse source cluster id")
	}

	targetClusterId, err := strconv.ParseUint(req.GetTargetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse target cluster id")
	}

	targetCluster, berr := a.clusterRepo.Get(ctx, targetClusterId, true)
	if berr != nil {
		return nil, berr.Chain("failed to get target cluster")
	}

	targetSharedPoolId, err := strconv.ParseUint(targetCluster.GetSharedPoolId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse target shared pool id")
	}

	restoreEntity := &entity.Restore{
		Restore: &schema.Restore{
			State:           "pending",
			RestoreMode:     schema.RestoreModeSnapshot,
			BackupId:        backupId,
			TargetClusterId: targetClusterId,
			TenantId:        targetCluster.GetOrganizationId(),

			TargetSharedPoolId:  targetSharedPoolId,
			TargetRegionId:      targetCluster.GetRegionId().String(),
			TargetCloudProvider: targetCluster.GetCloudProviderId().String(),
			TargetServicePlan:   targetCluster.GetServicePlan().String(),
			TargetMinRcu:        targetCluster.GetMinRcu(),
			TargetMaxRcu:        targetCluster.GetMaxRcu(),
			SourceClusterId:     sourceClusterId,
			SourceRegionId:      targetCluster.GetRegionId().String(),
			SourceCloudProvider: targetCluster.GetCloudProviderId().String(),
			SourceServicePlan:   targetCluster.GetServicePlan().String(),
		},
	}

	regionalReq := &clusterngv1beta.CreateManualBackupRestoreRequest{
		RestoreId:          fmt.Sprintf("%d", targetClusterId),
		SourceClusterId:    fmt.Sprintf("%d", sourceClusterId),
		BackupId:           fmt.Sprintf("%d", backupId),
		TargetSharedPoolId: targetCluster.GetSharedPoolId(),
		TargetClusterId:    fmt.Sprintf("%d", targetClusterId),
	}

	tilog.Info(ctx, "creating manual backup restore",
		zap.Uint64("backup_id", backupId),
		zap.Uint64("source_cluster_id", sourceClusterId),
		zap.Uint64("target_cluster_id", targetClusterId),
	)

	requestJson, err := protojson.Marshal(regionalReq)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to marshal create manual backup restore request")
	}

	metadataJson, err := metadata.MarshalRequestMetaFromCtx(ctx)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to marshal metadata from context")
	}

	return func(ctx context.Context) bizerr.BizError {
		txErr := a.transaction.Transaction(ctx, func(ctx context.Context) error {
			if berr := lockResourcePoolForRestore(
				ctx,
				a.resourcePoolRepo,
				a.clusterRepo,
				targetSharedPoolId,
				targetClusterId,
			); berr != nil {
				return berr
			}
			berr := a.restoreRepo.Create(ctx, restoreEntity)
			if berr != nil {
				return berr.Chain("failed to create restore record")
			}

			workflowErr := a.workflow.RunDagWithBizId(
				ctx,
				common.RestoreFromManualBackup,
				fmt.Sprintf("%d", targetCluster.GetOrganizationId()),
				"0",
				fmt.Sprintf("%d", targetClusterId),
				map[string]string{
					common.KeyRequest: string(requestJson),
					wf.KeyTraceId:     wf.GetTraceId(ctx),
					wf.KeyRequestMeta: string(metadataJson),
				},
			)
			if workflowErr != nil {
				return bizerr.WrapErrorWithCode(workflowErr, bizerr.ErrCodeUnknown, "create async workflow failed")
			}
			displayEv := newRestoreDisplayEvent(ctx, restoreEntity, eventdata.EventStatusStarted, "Restore started.")
			displayevent.Write(ctx, a.eventCommitter, displayEv)
			return nil
		})
		if txErr != nil {
			return bizerr.WrapError(txErr, "failed to create restore record or run restore dag")
		}
		return nil
	}, nil
}

func (a *restoreFromManualAdapter) GetRestoreRunningStatus(
	ctx context.Context,
	clusterId uint64,
	restore *entity.Restore,
) (*globalngv1.RestoreStatusResponse, bizerr.BizError) {
	targetCluster, berr := a.clusterRepo.Get(ctx, clusterId, false)
	if berr != nil {
		return nil, berr.Chain("failed to get cluster")
	}

	targetRegion, berr := a.regionRepo.Get(ctx, targetCluster.RegionId)
	if berr != nil {
		return nil, berr.Chain("failed to get region")
	}

	req := &clusterngv1beta.GetManualBackupRestoreRequest{
		SourceClusterId: fmt.Sprintf("%d", restore.SourceClusterId),
		BackupId:        fmt.Sprintf("%d", restore.BackupId),
		RestoreId:       fmt.Sprintf("%d", restore.TargetClusterId),
	}

	restoreResp, berr := a.regionalCli.GetManualBackupRestore(ctx, targetRegion, req)
	if berr != nil {
		tilog.Error(ctx, "failed to get manual backup restore", zap.Error(berr), zap.Any("request", req))
		return nil, berr.Chain("failed to get manual backup restore")
	}

	tilog.Info(ctx, "get manual backup restore", zap.Uint64("cluster_id", clusterId), zap.Any("restore", restoreResp))

	switch restoreResp.Status {
	case clusterngv1beta.ManualBackupRestore_RUNNING:
		return &globalngv1.RestoreStatusResponse{
			State:   globalngv1.RestoreState_RESTORE_STATE_RUNNING,
			Message: "Manual backup restore is running",
		}, nil
	case clusterngv1beta.ManualBackupRestore_SUCCEED:
		return &globalngv1.RestoreStatusResponse{
			State:    globalngv1.RestoreState_RESTORE_STATE_RUNNING,
			Message:  "Restore is done in regional control plane, waiting for global control plane to execute post-restore operations",
			Progress: 99,
		}, nil
	case clusterngv1beta.ManualBackupRestore_FAILED:
		return &globalngv1.RestoreStatusResponse{
			State:   globalngv1.RestoreState_RESTORE_STATE_FAILED,
			Message: "Manual backup restore failed: " + restoreResp.GetMessage(),
		}, nil
	case clusterngv1beta.ManualBackupRestore_CANCELLED:
		return &globalngv1.RestoreStatusResponse{
			State:   globalngv1.RestoreState_RESTORE_STATE_CANCELLED,
			Message: "Manual backup restore was cancelled",
		}, nil
	}

	tilog.Error(ctx, "unexpected manual backup restore status: "+restoreResp.Status.String(),
		zap.Uint64("cluster_id", clusterId),
		zap.Any("restore", restoreResp),
	)
	return nil, bizerr.New(bizerr.GeneralInternalErrorCode, "unexpected manual backup restore status: %s", restoreResp.Status.String())
}

func (a *restoreFromManualAdapter) StopRestore(ctx context.Context, req *globalngv1.StopRestoreClusterRequest) bizerr.BizError {
	err := a.workflow.DeleteDagInstancesWithDagId(ctx, req.TargetClusterId, common.RestoreFromManualBackup)
	if err != nil {
		tilog.Error(ctx, "failed to delete restore from manual backup dag instances", zap.Error(err), zap.String("cluster_id", req.TargetClusterId))
		return bizerr.WrapError(err, "failed to delete restore from manual backup dag instances")
	}
	tilog.Info(ctx, "delete restore from manual backup dag instances success", zap.String("cluster_id", req.TargetClusterId))

	// deleting the global dag instances only stops the global workflow, the
	// regional create-restore dag keeps running (and keeps retrying its
	// actions) until the regional control plane is told to delete the restore:
	// DeleteManualBackupRestore aborts the regional create dag, marks the
	// restore cancelled and submits the regional delete dag.
	clusterId, err := strconv.ParseUint(req.TargetClusterId, 10, 64)
	if err != nil {
		return bizerr.WrapError(err, "failed to parse target cluster id")
	}

	restore, berr := a.restoreRepo.Get(ctx, clusterId)
	if berr != nil {
		return berr.Chain("failed to get restore record")
	}

	targetCluster, berr := a.clusterRepo.Get(ctx, clusterId, true)
	if berr != nil {
		return berr.Chain("failed to get target cluster")
	}

	targetRegion, berr := a.regionRepo.Get(ctx, targetCluster.RegionId)
	if berr != nil {
		return berr.Chain("failed to get region")
	}

	berr = a.regionalCli.DeleteManualBackupRestore(ctx, targetRegion, &clusterngv1beta.DeleteManualBackupRestoreRequest{
		SourceClusterId: fmt.Sprintf("%d", restore.SourceClusterId),
		BackupId:        fmt.Sprintf("%d", restore.BackupId),
		RestoreId:       fmt.Sprintf("%d", restore.TargetClusterId),
	})
	if berr != nil {
		return berr.Chain("failed to delete manual backup restore in cluster service")
	}
	tilog.Info(ctx, "delete manual backup restore in cluster service success",
		zap.String("cluster_id", req.TargetClusterId),
		zap.Uint64("source_cluster_id", restore.SourceClusterId),
		zap.Uint64("backup_id", restore.BackupId),
	)
	return nil
}
