package br

import (
	"context"
	"errors"
	"fmt"
	"path"
	"time"

	v1alpha1br "github.com/pingcap/tidb-operator/api/v2/br/v1alpha1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	cert2 "github.com/tidbcloud/cluster-service-ng/pkg/cert"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type createTiBRManualBackupAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	manualBackupRepo    repo.ManualBackupRepo
	templateVersionSvc  *templateversion.Service
	imageConfig         *config.ImageConfig
	bizConfig           *config.BizConfig
	dpServiceCli        dplanemgrv1.DataPlaneServiceClient
	clusterConfigClient clusterconfig.Client
}

type skipManualBackupActionError struct {
	cause error
}

func (e *skipManualBackupActionError) Error() string {
	return e.cause.Error()
}

func (e *skipManualBackupActionError) Unwrap() error {
	return e.cause
}

const CreateTiBRManualBackupActionName = "create-tibr-manual-backup"

func NewCreateTiBRManualBackupAction(
	sharedPoolRepo repo.SharedPoolRepo,
	manualBackupRepo repo.ManualBackupRepo,
	dataplaneManager dataplane.Manager,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
	bizConfig *config.BizConfig,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	clusterConfigClient clusterconfig.Client,
) actions.Action {
	return actions.CreateActionFromAsync(&createTiBRManualBackupAction{
		sharedPoolRepo:      sharedPoolRepo,
		manualBackupRepo:    manualBackupRepo,
		dataplaneManager:    dataplaneManager,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
		bizConfig:           bizConfig,
		dpServiceCli:        dpServiceCli,
		clusterConfigClient: clusterConfigClient,
	}, 1*time.Minute, 30*time.Minute)
}

func (a *createTiBRManualBackupAction) Name() string {
	return CreateTiBRManualBackupActionName
}

func (a *createTiBRManualBackupAction) Start(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "create tibr for manual backup...")

	targetSharedPool, remoteCli, manualBackup, err := getManualBackupActionContextInfos(ctx, args, a.manualBackupRepo, a.sharedPoolRepo, a.dataplaneManager)
	if err != nil {
		if skip, skipErr := skipManualBackupActionForMissingSharedPool(ctx, a.manualBackupRepo, manualBackup, err, a.Name()); skip {
			return skipErr
		}
		return fmt.Errorf("failed to get manual backup action context infos: %w", err)
	}

	if err := AddActionForManualBackup(ctx, a.manualBackupRepo, manualBackup, schema.ManualBackupActionCreateTiBR); err != nil {
		return fmt.Errorf("failed to add create tibr action to manual backup: %w", err)
	}

	cert := cert2.TiBRCert(tioperator.TiBRCRNameOfManualBackup(manualBackup.ClusterID, manualBackup.BackupID), targetSharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, cert); err != nil {
		tilog.Error(ctx, "failed to create tibr cert", zap.Any("cert", cert), zap.Error(err))
		return err
	}

	err = a.createTiBRCRIfNotExist(ctx, targetSharedPool, manualBackup, args)
	if err != nil {
		return fmt.Errorf("create tibr CR failed: %s", err.Error())
	}

	return nil
}

func (a *createTiBRManualBackupAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "check tibr creation finished...")

	targetSharedPool, remoteCli, manualBackup, err := getManualBackupActionContextInfos(ctx, args, a.manualBackupRepo, a.sharedPoolRepo, a.dataplaneManager)
	if err != nil {
		if skip, skipErr := skipManualBackupActionForMissingSharedPool(ctx, a.manualBackupRepo, manualBackup, err, a.Name()); skip {
			return true, skipErr
		}
		return false, fmt.Errorf("failed to get manual backup action context infos: %w", err)
	}

	tibrCR := &v1alpha1br.TiBR{}
	tibrName := tioperator.TiBRCRNameOfManualBackup(manualBackup.ClusterID, manualBackup.BackupID)
	namespace := targetSharedPool.GetDataPlaneNamespace()
	err = remoteCli.Get(ctx, types.NamespacedName{
		Name:      tibrName,
		Namespace: namespace,
	}, tibrCR)
	if err != nil {
		tilog.Error(ctx, "failed to get tibr for manual backup", zap.String("name", tibrName), zap.String("namespace", namespace), zap.Error(err))
		return false, fmt.Errorf("failed to get tibr for manual backup: %w", err)
	}

	if !tioperator.IsTiBRSyncedReady(tibrCR) {
		tilog.Error(ctx, "tibr is not ready and synced", zap.String("name", tibrName), zap.String("namespace", namespace))
		return false, nil
	}

	tilog.Info(ctx, "tibr is ready and synced", zap.String("name", tibrName), zap.String("namespace", namespace))

	return true, nil
}

func getManualBackupActionContextInfos(ctx context.Context, args actions.ActionArgs, manualBackupRepo repo.ManualBackupRepo, sharedPoolRepo repo.SharedPoolRepo, dataplaneManager dataplane.Manager) (targetSharedPool *entity.SharedPool, remoteCli client.Client, manualBackup *entity.ManualBackup, err error) {
	return getManualBackupActionContextInfosWithDeletedPool(
		ctx, args, manualBackupRepo, sharedPoolRepo, dataplaneManager, false,
	)
}

//nolint:nakedret // make it easy for return multiple values
func getManualBackupActionContextInfosWithDeletedPool(
	ctx context.Context,
	args actions.ActionArgs,
	manualBackupRepo repo.ManualBackupRepo,
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	includeDeletedSharedPool bool,
) (targetSharedPool *entity.SharedPool, remoteCli client.Client, manualBackup *entity.ManualBackup, err error) {
	tilog.Info(ctx, "get create tibr context infos...")

	tidbClusterID, existed := GetTiDBClusterID(args)
	if !existed {
		err = fmt.Errorf("tidb cluster id is required, but not found in args: %v", args)
		return
	}

	manualBackupID, existed := GetManualBackupID(args)
	if !existed {
		err = fmt.Errorf("manual backup id is required, but not found in args: %v", args)
		return
	}

	manualBackup, err = manualBackupRepo.Load(ctx, tidbClusterID, manualBackupID, true)
	if err != nil {
		err = fmt.Errorf("failed to load manual backup: %w", err)
		return
	}

	targetSharedPool, err = sharedPoolRepo.Load(ctx, manualBackup.SharedPoolID, includeDeletedSharedPool)
	if err != nil {
		err = fmt.Errorf("failed to load target sharedPool: %w", err)
		if status.Code(err) == codes.NotFound {
			err = &skipManualBackupActionError{cause: err}
		}
		return
	}

	remoteCli, err = dataplaneManager.GetClient(targetSharedPool.GetDataplaneName())
	if err != nil {
		err = fmt.Errorf("failed to get remote client for target shared pool: %w", err)
		return
	}
	tilog.Info(ctx, "get create tibr context infos success", zap.Any("args", args))

	return
}

func skipManualBackupActionForMissingSharedPool(
	ctx context.Context,
	manualBackupRepo repo.ManualBackupRepo,
	manualBackup *entity.ManualBackup,
	err error,
	actionName string,
) (bool, error) {
	var skipErr *skipManualBackupActionError
	if !errors.As(err, &skipErr) {
		return false, nil
	}
	if manualBackup == nil {
		return true, nil
	}
	if manualBackup.Status != schema.ManualBackupStatusFailed {
		if setErr := manualBackupRepo.SetStatus(ctx, manualBackup.ClusterID, manualBackup.BackupID, schema.ManualBackupStatusFailed); setErr != nil {
			return true, fmt.Errorf("failed to mark manual backup failed when skipping action %s: %w", actionName, setErr)
		}
	}
	tilog.Warn(ctx, "skip manual backup action because target sharedPool is not found",
		zap.String("action", actionName),
		zap.String("backupId", manualBackup.BackupID),
		zap.String("clusterId", manualBackup.ClusterID),
		zap.String("sharedPoolId", manualBackup.SharedPoolID),
		zap.Error(err))
	return true, nil
}

func (a *createTiBRManualBackupAction) createTiBRCRIfNotExist(ctx context.Context, targetSharedPool *entity.SharedPool, manualBackup *entity.ManualBackup, args actions.ActionArgs) error {
	cpu, memory, storage, err := getResourcesForTiBRManualBackup(args)
	if err != nil {
		return fmt.Errorf("failed to get resources for tibr: %w", err)
	}

	// FIXME: only support one tenant per dataplane now
	tenant, placement, err := getTiBRNodePlacement(ctx, a.dpServiceCli, targetSharedPool.GetDataplaneName(), cpu, memory)
	if err != nil {
		return fmt.Errorf("failed to resolve node placement for tibr manual backup: %w", err)
	}

	image, cfg, err := a.getImageAndConfig(ctx, targetSharedPool, tenant)
	if err != nil {
		return fmt.Errorf("failed to get image and config for tibr manual backup: %w", err)
	}

	yamlStr, err := a.renderTiBRCR(ctx, targetSharedPool, manualBackup, tenant, image, cfg, cpu, memory, storage, placement)
	if err != nil {
		tilog.Error(ctx, "failed to render tibr CR yaml", zap.Error(err))
		return fmt.Errorf("failed to render tibr CR yaml: %s", err.Error())
	}

	resourceCr, _, err := k8s.GenerateUnstructuredFromYaml(yamlStr)
	if err != nil {
		tilog.Error(ctx, "failed to generate unstructured from yaml", zap.String("yaml", yamlStr), zap.Error(err))
		return fmt.Errorf("failed to generate unstructured from yaml: %s", err.Error())
	}

	remoteCli, err := a.dataplaneManager.GetClient(targetSharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for target shared pool", zap.String("dataplaneName", targetSharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for target shared pool: %w", err)
	}

	err = k8s.CreateIfNotExist(ctx, remoteCli, resourceCr)
	if err != nil {
		tilog.Error(ctx, "failed to create tibr", zap.String("yaml", yamlStr), zap.Error(err))
		return fmt.Errorf("failed to create tibr: %s", err.Error())
	}

	tilog.Info(ctx, " create tibr success", zap.String("yaml", yamlStr))

	return nil
}

func (a *createTiBRManualBackupAction) renderTiBRCR(
	ctx context.Context,
	targetSharedPool *entity.SharedPool,
	manualBackup *entity.ManualBackup,
	tenant, image, cfg, cpu, memory, storage string,
	placement TiBRNodePlacement,
) (string, error) {
	tibrName := tioperator.TiBRCRNameOfManualBackup(manualBackup.ClusterID, manualBackup.BackupID)

	// common args
	tplArgs := map[string]string{
		"Name":                 tibrName,
		"Namespace":            targetSharedPool.GetDataPlaneNamespace(),
		"ResourceCPU":          cpu,
		"ResourceMemory":       memory,
		"ResourceStorageClass": GetStorageClass(&a.bizConfig.DefaultTiBRConfig, targetSharedPool.GetProvider().GetType()),
		"Image":                image,
		"DfsPrefix":            targetSharedPool.GetDfsPrefix(),
		"DfsBucket":            targetSharedPool.GetObjectStorage().GetBucket(),
		"DfsRegion":            targetSharedPool.GetObjectStorage().GetRegion(),
		"ComponentGroup":       getComponentGroupName4TiBR(targetSharedPool),
		"TargetClusterID":      manualBackup.ClusterID,
		"ResourceStorageSize":  storage,
		"Tenant":               tenant,
		"ResourcePool":         targetSharedPool.GetId(),
		"Config":               cfg,
		"NodeSpec":             placement.NodeSpec,
		"NodeVersion":          placement.NodeVersion,
	}

	if targetSharedPool.GetDfxEndpoint() != "" {
		tplArgs["DfsEndpoint"] = targetSharedPool.GetDfxEndpoint()
	}

	if err := workflow.AddAzureDfsArgs(targetSharedPool, tplArgs); err != nil {
		return "", err
	}

	if targetSharedPool.GetProvider().GetType() == apiv1.ProviderType_ALICLOUD {
		tplArgs["AliCloudRegion"] = targetSharedPool.GetProvider().GetRegion()
	}

	yamlStr, _, _, err := templateversion.RenderWithVersion(ctx, a.templateVersionSvc, "tibr_manual_backup", "", tplArgs)
	if err != nil {
		return "", fmt.Errorf("failed to render template: %w", err)
	}
	return yamlStr, nil
}

func getResourcesForTiBRManualBackup(args actions.ActionArgs) (cpu, memory, storage string, err error) {
	cpuCore, ok := args[workflow.TiBRCPUKey]
	if !ok {
		return "", "", "", fmt.Errorf("cpu is required, but not found in args: %v", args)
	}
	milliCPU, err := ParseCPU(cpuCore.String())
	if err != nil {
		return "", "", "", fmt.Errorf("failed to parse cpu %s: %w", cpuCore.String(), err)
	}
	cpu = fmt.Sprintf("%d", ComputeAvailableCPU(uint64(milliCPU)/1000))

	memValue, ok := args[workflow.TiBRMemoryKey]
	if !ok {
		return "", "", "", fmt.Errorf("memory is required, but not found in args: %v", args)
	}
	memoryInMiB, err := ParseToMiB(memValue.String())
	if err != nil {
		return "", "", "", fmt.Errorf("failed to parse memory %s: %w", memValue.String(), err)
	}
	memory = fmt.Sprintf("%dMi", tioperator.ComputeAvailableMemory(memoryInMiB))

	storageValue, ok := args[workflow.TiBRStorageKey]
	if !ok {
		return "", "", "", fmt.Errorf("storage is required, but not found in args: %v", args)
	}
	storage = storageValue.String()

	return cpu, memory, storage, nil
}

func (a *createTiBRManualBackupAction) getImageAndConfig(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string) (string, string, error) {
	defaultConfig := schemav1.NewTomlConfig(map[string]any{})
	defaultConfig.Set("native-br.lower-memory", true)
	configFile, err := defaultConfig.MarshalTOML()
	if err != nil {
		return "", "", fmt.Errorf("failed to marshal default config: %w", err)
	}

	image, err := GetImage(targetSharedPool, a.imageConfig)
	if err != nil {
		return "", "", fmt.Errorf("failed to get image for tibr: %w", err)
	}

	if a.bizConfig.DefaultTiBRConfig.IntegrateConfigCenter {
		image, err = a.getImageFromConfigCenter(ctx, targetSharedPool, tenantId)
		if err != nil {
			return "", "", fmt.Errorf("failed to get image from config center: %w", err)
		}
	}

	return image, string(configFile), nil
}

// reuse the image from tibr auto backup
func (a *createTiBRManualBackupAction) getImageFromConfigCenter(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string) (string, error) {
	_, cfg, err := a.clusterConfigClient.GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBR_AUTOBACKUP)
	if err != nil {
		return "", fmt.Errorf("failed to get %s version and config: %w", apiv1.ComponentType_TIBR_AUTOBACKUP.String(), err)
	}
	image := fmt.Sprintf("%s/%s/%s:%s",
		a.imageConfig.Registry,
		a.imageConfig.RepoPrefix,
		path.Base(cfg.GetImageRepo()),
		cfg.GetImageTag(),
	)
	return image, nil
}
