package cluster

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (svc *service) RestoreStatus(ctx context.Context, clusterId string) (*entity.RestoreClusterStatus, error) {
	ctx, span := tracer.Start(ctx, "ClusterService.RestoreStatus")
	defer span.End()

	customeRestoreStatus, err := svc.customStatusRepo.GetRestoreStatusByName(ctx, fmt.Sprintf("restore-%s", clusterId))
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			tilog.Error(ctx, "failed to get restore status", zap.Error(err), zap.String("cluster_id", clusterId))
			return nil, bizerr.WrapError(err, "failed to get restore status")
		}
		tilog.Info(
			ctx,
			"customer restoring status not found, restore may be already done",
			zap.String("cluster_id", clusterId),
		)
	}

	restoreStatusInNextGen, err := svc.nextGenCli.RestoreStatus(ctx, &globalngv1.RestoreStatusRequest{
		ClusterId: clusterId,
	})
	if err != nil {
		var be bizerr.BizError
		ok := errors.As(err, &be)
		if !ok {
			tilog.Error(
				ctx,
				"next gen service return unexpected error, expect bizerr.BizError",
				zap.Error(err),
				zap.String("cluster_id", clusterId),
			)
			return nil, bizerr.WrapError(err, "next gen service return unexpected error, expect bizerr.BizError")
		}
		if be.ErrCode() != bizerr.ErrCodeRecordNotFound {
			tilog.Error(
				ctx,
				"failed to get restore status in next gen service",
				zap.Error(err),
				zap.String("cluster_id", clusterId),
			)
			return nil, bizerr.WrapError(err, "failed to get restore status in next gen service")
		}
		tilog.Info(
			ctx,
			"restore status not found in next gen service, may be not start in next gen service yet",
			zap.String("cluster_id", clusterId),
		)
	}

	// cluster is not in restoring && no restore status in next gen service, so cluster haven't been restored
	if customeRestoreStatus == nil && restoreStatusInNextGen == nil {
		tilog.Info(
			ctx,
			"cluster doesn't has customer restoring status, and no restore status found in next gen service, so there is not restore for this cluster",
			zap.String("cluster_id", clusterId),
		)
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "restore not found, cluster haven't been restored")
	}

	// the customer restore status is cleared, restore should be success
	if customeRestoreStatus == nil && restoreStatusInNextGen != nil {
		// restore should be success
		if restoreStatusInNextGen.GetState() != globalngv1.RestoreState_RESTORE_STATE_SUCCESS {
			tilog.Error(ctx, "restore should be success, but not",
				zap.String("cluster_id", clusterId),
				zap.String("restore_state", restoreStatusInNextGen.GetState().String()))
			return &entity.RestoreClusterStatus{
				RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
					State: openapingv1beta2.RestoreState_UNKNOWN,
					Message: fmt.Sprintf(
						"Restore should be success, but: %s",
						restoreStatusInNextGen.GetState().String(),
					),
				},
			}, nil
		}
		return &entity.RestoreClusterStatus{
			RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
				State:    openapingv1beta2.RestoreState_SUCCEEDED,
				Progress: 100,
			},
		}, nil
	}

	// following cases is that cluster customer restore status is not nil, but no restore status in next gen service,
	// so cluster is not available yet or deleted
	if restoreStatusInNextGen == nil {
		clusterIdUint64, _ := strconv.ParseUint(clusterId, 10, 64)
		cluster, err := svc.clusterRepo.GetCluster(ctx, clusterIdUint64, true)
		if err != nil {
			tilog.Error(ctx, "failed to get cluster", zap.Error(err), zap.String("cluster_id", clusterId))
			return nil, bizerr.WrapError(err, "failed to get cluster")
		}
		if cluster.DeletedAt != nil {
			tilog.Info(
				ctx,
				"cluster is deleted, restore is cancelled",
				zap.String("cluster_id", clusterId),
			)
			return &entity.RestoreClusterStatus{
				RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
					State:   openapingv1beta2.RestoreState_CANCELLED,
					Message: "Cluster is deleted, restore is cancelled",
				},
			}, nil
		}
		return &entity.RestoreClusterStatus{
			RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
				State:    openapingv1beta2.RestoreState_PENDING,
				Message:  "Cluster is provisioning, please wait for a moment, restore can be started after cluster is available",
				Progress: 0,
			},
		}, nil
	}
	// for the cases create request already send to next gen service
	if !lo.Contains([]globalngv1.RestoreState{
		globalngv1.RestoreState_RESTORE_STATE_PENDING,
		globalngv1.RestoreState_RESTORE_STATE_RUNNING,
		globalngv1.RestoreState_RESTORE_STATE_SUCCESS,
		globalngv1.RestoreState_RESTORE_STATE_FAILED,
		globalngv1.RestoreState_RESTORE_STATE_CANCELLED,
	}, restoreStatusInNextGen.GetState()) {
		tilog.Error(
			ctx,
			"restore state is not valid in next gen service",
			zap.String("cluster_id", clusterId),
			zap.String("restore_state", restoreStatusInNextGen.GetState().String()),
		)
		return &entity.RestoreClusterStatus{
			RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
				State:   openapingv1beta2.RestoreState_UNKNOWN,
				Message: fmt.Sprintf("restore state is not valid: %s", restoreStatusInNextGen.GetState().String()),
			},
		}, nil
	}

	// restore started in next gen service
	return &entity.RestoreClusterStatus{
		RestoreStatusResponse: &openapingv1beta2.RestoreStatusResponse{
			State:    convertNextGenRestoreStateToOpenApiRestoreState(restoreStatusInNextGen.GetState()),
			Message:  restoreStatusInNextGen.GetMessage(),
			Progress: restoreStatusInNextGen.GetProgress(),
		},
	}, nil
}

func convertNextGenRestoreStateToOpenApiRestoreState(state globalngv1.RestoreState) openapingv1beta2.RestoreState {
	switch state {
	case globalngv1.RestoreState_RESTORE_STATE_PENDING:
		return openapingv1beta2.RestoreState_PENDING
	case globalngv1.RestoreState_RESTORE_STATE_RUNNING:
		return openapingv1beta2.RestoreState_RUNNING
	case globalngv1.RestoreState_RESTORE_STATE_SUCCESS:
		return openapingv1beta2.RestoreState_SUCCEEDED
	case globalngv1.RestoreState_RESTORE_STATE_FAILED:
		return openapingv1beta2.RestoreState_FAILED
	case globalngv1.RestoreState_RESTORE_STATE_CANCELLED:
		return openapingv1beta2.RestoreState_CANCELLED
	default:
		return openapingv1beta2.RestoreState_UNKNOWN
	}
}
