package br

import (
	"context"
	"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"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

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

const CreateTiBRRestoreActionName = "create-tibr-restore"

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

func (a *createTiBRRestoreAction) Name() string {
	return CreateTiBRRestoreActionName
}

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

	targetSharedPool, remoteCli, lwRestore, err := a.getActionContextInfos(args)
	if err != nil {
		return fmt.Errorf("failed to get action context infos: %w", err)
	}

	err = AddActionForLWRestore(ctx, a.lwRestoreRepo, lwRestore.RestoreID, schema.LightweightRestoreActionCreateTiBR)
	if err != nil {
		return fmt.Errorf("failed to add create tibr action to restore: %w", err)
	}

	cert := cert2.TiBRCert(tioperator.TiBRCRNameOfRestore(lwRestore.RestoreID), 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, lwRestore, args)
	if err != nil {
		return fmt.Errorf("create tibr CR failed: %s", err.Error())
	}

	return nil
}

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

	targetSharedPool, remoteCli, lwRestore, err := a.getActionContextInfos(args)
	if err != nil {
		return false, fmt.Errorf("failed to get create tibr context infos: %w", err)
	}

	tibrCR := &v1alpha1br.TiBR{}
	tibrName := tioperator.TiBRCRNameOfRestore(lwRestore.RestoreID)
	namespace := targetSharedPool.GetDataPlaneNamespace()
	err = remoteCli.Get(ctx, types.NamespacedName{
		Name:      tibrName,
		Namespace: namespace,
	}, tibrCR)
	if err != nil {
		tilog.Error(ctx, "failed to get tibr", zap.String("name", tibrName), zap.String("namespace", namespace), zap.Error(err))
		return false, fmt.Errorf("failed to get tibr: %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
}

//nolint:nakedret // make it easy for return multiple values
func (a *createTiBRRestoreAction) getActionContextInfos(args actions.ActionArgs) (targetSharedPool *entity.SharedPool, remoteCli client.Client, lwRestore *entity.LightweightRestore, err error) {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "get create tibr context infos...")

	var targetSharedPoolId string
	restoreID, existed := GetRestoreID(args)
	if !existed {
		err = fmt.Errorf("restore id is required, but not found in args: %v", args)
		return
	}

	lwRestore, err = a.lwRestoreRepo.Load(ctx, restoreID, false)
	if err != nil {
		err = fmt.Errorf("failed to load lw restore: %w", err)
		return
	}
	targetSharedPoolId = lwRestore.TargetSharedPoolID

	targetSharedPool, err = a.sharedPoolRepo.Load(ctx, targetSharedPoolId, false)
	if err != nil {
		err = fmt.Errorf("failed to load target sharedPool: %w", err)
		return
	}

	remoteCli, err = a.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 (a *createTiBRRestoreAction) createTiBRCRIfNotExist(ctx context.Context, targetSharedPool *entity.SharedPool, lwRestore *entity.LightweightRestore, args actions.ActionArgs) error {
	cpu, memory, storage, err := getResourcesForTiBRRestore(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 restore: %w", err)
	}

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

	yamlStr, err := a.renderTiBRCR(ctx, targetSharedPool, lwRestore, 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 *createTiBRRestoreAction) renderTiBRCR(
	ctx context.Context,
	targetSharedPool *entity.SharedPool,
	lwRestore *entity.LightweightRestore,
	tenant, image, cfg, cpu, memory, storage string,
	placement TiBRNodePlacement,
) (string, error) {
	tibrName := tioperator.TiBRCRNameOfRestore(lwRestore.RestoreID)

	// 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":      lwRestore.TargetClusterID,
		"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_restore", "", tplArgs)
	if err != nil {
		return "", fmt.Errorf("failed to render template: %w", err)
	}
	return yamlStr, nil
}

func getResourcesForTiBRRestore(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 *createTiBRRestoreAction) getImageAndConfig(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string) (string, string, error) {
	if a.bizConfig.DefaultTiBRConfig.IntegrateConfigCenter {
		return a.getImageAndConfigFromConfigCenter(ctx, targetSharedPool, tenantId)
	}

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

	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)
	}

	return image, string(configFile), nil
}

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