package action

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

	"github.com/tidbcloud/fastflow/pkg/entity/run"
	"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/tidb-mgmt-service/domain/adapter/nextgenglobal"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/constant"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/service/workflow/metric"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

type NextGenFinalBackupAction struct {
	nextgenCli nextgenglobal.NextGenGlobal
}

func NewNextGenFinalBackupAction(nextgenCli nextgenglobal.NextGenGlobal) *NextGenFinalBackupAction {
	return &NextGenFinalBackupAction{nextgenCli: nextgenCli}
}

func (a *NextGenFinalBackupAction) Name() string {
	return constant.TaskNextGenFinalBackup
}

func (a *NextGenFinalBackupAction) Run(ctx run.ExecuteContext, _ any) error {
	cctx, span := workflow.GenerateContext(ctx)
	defer span.End()

	needFinalBackup, ok := ctx.GetVar(constant.KeyFinalBackup)
	if !ok || needFinalBackup != "true" {
		tilog.Info(cctx, "skip nextgen final backup")
		return nil
	}

	requestJSON, ok := ctx.GetVar(constant.KeyRequest)
	if !ok {
		err := errors.New("delete nextgen request is not found")
		tilog.Error(cctx, "nextgen final backup request is missing", zap.Error(err))
		return err
	}
	var request globalngv1.DeleteClusterRequest
	if err := protojson.Unmarshal([]byte(requestJSON), &request); err != nil {
		tilog.Error(cctx, "failed to unmarshal nextgen final backup request", zap.Error(err))
		return fmt.Errorf("unmarshal delete nextgen request: %w", err)
	}

	cluster, err := a.nextgenCli.GetCluster(cctx, &globalngv1.GetClusterRequest{ClusterId: request.GetClusterId()})
	if err != nil {
		if hasBizErrorCode(err, bizerr.ErrCodeRecordNotFound) {
			tilog.Warn(cctx, "skip final backup because nextgen cluster is not found", zap.Uint64("cluster_id", request.GetClusterId()))
			return nil
		}
		tilog.Error(cctx, "failed to get nextgen cluster for final backup", zap.Uint64("cluster_id", request.GetClusterId()), zap.Error(err))
		return fmt.Errorf("get nextgen cluster for final backup: %w", err)
	}

	switch cluster.GetState() {
	case globalngv1.Cluster_CREATING, globalngv1.Cluster_DELETED, globalngv1.Cluster_UNAVAILABLE:
		tilog.Info(cctx, "skip nextgen final backup for cluster state",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("state", cluster.GetState().String()),
		)
		return nil
	case globalngv1.Cluster_AVAILABLE, globalngv1.Cluster_MODIFYING:
	default:
		err := fmt.Errorf("nextgen cluster state %s is not ready for final backup", cluster.GetState().String())
		tilog.Error(cctx, "nextgen cluster is not ready for final backup", zap.Uint64("cluster_id", request.GetClusterId()), zap.Error(err))
		return err
	}

	resourcePoolID, err := strconv.ParseUint(cluster.GetSharedPoolId(), 10, 64)
	if err != nil || resourcePoolID == 0 {
		parseErr := fmt.Errorf("invalid resource pool id %q", cluster.GetSharedPoolId())
		if err != nil {
			parseErr = fmt.Errorf("parse resource pool id %q: %w", cluster.GetSharedPoolId(), err)
		}
		tilog.Error(cctx, "failed to resolve resource pool for final backup", zap.Uint64("cluster_id", request.GetClusterId()), zap.Error(parseErr))
		return parseErr
	}
	if err := a.ensureResourcePoolReady(cctx, request.GetClusterId(), resourcePoolID); err != nil {
		return err
	}

	backup, err := a.nextgenCli.CreateFinalBackup(cctx, &globalngv1.CreateFinalBackupRequest{
		ClusterId: request.GetClusterId(),
	})
	if err != nil {
		tilog.Error(cctx, "failed to create nextgen final backup", zap.Uint64("cluster_id", request.GetClusterId()), zap.Error(err))
		return fmt.Errorf("create nextgen final backup: %w", err)
	}
	if backup.GetBackupId() == "" {
		err := errors.New("created final backup id is empty")
		tilog.Error(cctx, "nextgen final backup id is empty", zap.Uint64("cluster_id", request.GetClusterId()), zap.Error(err))
		return err
	}

	backups, err := a.nextgenCli.ListBackups(cctx, &globalngv1.ListBackupsRequest{
		BackupIds: []string{backup.GetBackupId()},
		PageSize:  1,
	})
	if err != nil {
		tilog.Error(
			cctx,
			"failed to get nextgen final backup",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("backup_id", backup.GetBackupId()),
			zap.Error(err),
		)
		return fmt.Errorf("get nextgen final backup: %w", err)
	}
	if len(backups.GetBackups()) == 0 {
		err := errors.New("nextgen final backup is not visible yet")
		tilog.Error(
			cctx,
			"nextgen final backup is not visible yet",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("backup_id", backup.GetBackupId()),
			zap.Error(err),
		)
		return err
	}

	finalBackup := backups.GetBackups()[0]
	switch finalBackup.GetState() {
	case globalngv1.Backup_STATE_SUCCESS:
		tilog.Info(
			cctx,
			"nextgen final backup succeeded",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("backup_id", backup.GetBackupId()),
		)
		return nil
	case globalngv1.Backup_STATE_FAILED, globalngv1.Backup_STATE_CANCELLED:
		metric.NextGenFinalBackupFailuresTotal.WithLabelValues(finalBackup.GetState().String()).Inc()
		tilog.Error(cctx, "nextgen final backup finished without success; continue cluster deletion",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("backup_id", backup.GetBackupId()),
			zap.String("backup_state", finalBackup.GetState().String()),
		)
		return nil
	default:
		err := fmt.Errorf("nextgen final backup is not finished, state: %s", finalBackup.GetState().String())
		tilog.Error(
			cctx,
			"nextgen final backup is not finished",
			zap.Uint64("cluster_id", request.GetClusterId()),
			zap.String("backup_id", backup.GetBackupId()),
			zap.Error(err),
		)
		return err
	}
}

func (a *NextGenFinalBackupAction) ensureResourcePoolReady(
	ctx context.Context,
	clusterID uint64,
	resourcePoolID uint64,
) error {
	profile, err := a.nextgenCli.GetResourcePool(ctx, &globalngv1.GetResourcePoolRequest{ResourcePoolId: resourcePoolID})
	if err != nil {
		tilog.Error(ctx, "failed to get resource pool for final backup", zap.Uint64("resource_pool_id", resourcePoolID), zap.Error(err))
		return fmt.Errorf("get resource pool for final backup: %w", err)
	}
	if profile == nil || profile.GetResourcePool() == nil {
		err := errors.New("resource pool profile does not contain a resource pool")
		tilog.Error(ctx, "invalid resource pool profile for final backup", zap.Uint64("resource_pool_id", resourcePoolID), zap.Error(err))
		return err
	}
	state := profile.GetResourcePool().GetLifecycleState()
	switch state {
	case globalngv1.ResourcePool_PAUSED:
		operationID := fmt.Sprintf("overdue-cleanup-final-backup-%d", clusterID)
		if _, err := a.nextgenCli.ResumeResourcePool(ctx, &globalngv1.ResumeResourcePoolRequest{
			ResourcePoolId: resourcePoolID,
			OperationId:    operationID,
		}); err != nil {
			tilog.Error(ctx, "failed to resume resource pool for final backup", zap.Uint64("resource_pool_id", resourcePoolID), zap.Error(err))
			return fmt.Errorf("resume resource pool for final backup: %w", err)
		}
		err := errors.New("resource pool resume submitted; wait before final backup")
		tilog.Error(ctx, "resource pool resume submitted for final backup", zap.Uint64("resource_pool_id", resourcePoolID), zap.Error(err))
		return err
	case globalngv1.ResourcePool_PAUSING, globalngv1.ResourcePool_PAUSE_PRECHECKING, globalngv1.ResourcePool_RESUMING:
		err := fmt.Errorf("resource pool state %s is not ready for final backup", state.String())
		tilog.Error(ctx, "resource pool is not ready for final backup", zap.Uint64("resource_pool_id", resourcePoolID), zap.Error(err))
		return err
	default:
		return nil
	}
}

func hasBizErrorCode(err error, code bizerr.ErrCode) bool {
	var target bizerr.BizError
	return errors.As(err, &target) && target.ErrCode() == code
}
