package br

import (
	"context"
	"fmt"
	"path"
	"slices"
	"strings"
	"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"
	"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/controller/controllerutil"
)

// this action
// 1. create tibrgc when sharedpool is creating
// 2. update tibrgc if backup gc setting is changed as needed, only when sharedpool is not deleted
const ApplyTiBRGCActionName = "apply-tibrgc"

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

func NewApplyTiBRGCAction(
	gcRepo repo.BackupGcSettingRepo,
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
	bizConfig *config.BizConfig,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	clusterConfigClient clusterconfig.Client,
) actions.Action {
	return actions.CreateActionFromAsync(&applyTiBRGCAction{
		gcRepo:              gcRepo,
		sharedPoolRepo:      sharedPoolRepo,
		dataplaneManager:    dataplaneManager,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
		bizConfig:           bizConfig,
		dpServiceCli:        dpServiceCli,
		clusterConfigClient: clusterConfigClient,
	}, time.Minute, time.Minute*20)
}

func (a *applyTiBRGCAction) Name() string {
	return ApplyTiBRGCActionName
}

// if backup gc setting is not exist, create it
// if sharedpool is deleted, skip
// if backup gc setting is exist, apply it
func (a *applyTiBRGCAction) Start(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply tibrgc...")
	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	var backupGcSetting *entity.BackupGcSetting
	backupGcSetting, err = a.gcRepo.Load(ctx, sharedPoolId)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "backup gc setting not found, it should be early created sharedpool without gc setting", zap.String("shared_pool_id", sharedPoolId))
			return nil
		}
		tilog.Error(ctx, "ailed to load backup gc setting", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		return fmt.Errorf("failed to load backup gc setting: %w", err)
	}

	if !slices.Contains(schema.BackupGcStatusesOfLivingSharedpool, backupGcSetting.Status) {
		tilog.Info(ctx, "tibrgc is not applicable, skip apply", zap.String("sharedPoolId", sharedPoolId), zap.String("status", backupGcSetting.Status))
		return nil
	}

	err = a.applyTiBRGC(ctx, backupGcSetting)
	if err != nil {
		tilog.Error(ctx, "failed to apply backup gc setting", zap.Error(err), zap.Any("backupGcSetting", backupGcSetting))
		return fmt.Errorf("failed to apply backup gc setting: %w", err)
	}
	tilog.Info(ctx, "applied backup gc setting", zap.Any("backupGcSetting", backupGcSetting))

	return nil
}

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

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return false, fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	backupGcSetting, err := a.gcRepo.Load(ctx, sharedPoolId)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "backup gc setting not found, it should be early created sharedpool without gc setting", zap.String("shared_pool_id", sharedPoolId))
			return true, nil
		}
		return false, fmt.Errorf("failed to load backup gc setting: %w", err)
	}

	if !slices.Contains(schema.BackupGcStatusesOfLivingSharedpool, backupGcSetting.Status) {
		tilog.Info(ctx, "tibrgc is not applicable, skip apply check finish", zap.String("sharedPoolId", sharedPoolId), zap.String("status", backupGcSetting.Status))
		return true, nil
	}

	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, true)
	if err != nil {
		return false, fmt.Errorf("failed to load sharedPool: %w", err)
	}
	if sharedPool.DeletedAt != nil {
		tilog.Info(ctx, "shared pool is deleted, skip checking TiBRGC",
			zap.String("sharedPoolId", sharedPoolId))
		return true, nil
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return false, fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	tibrgcCR := &v1alpha1br.TiBRGC{}
	namespace := sharedPool.GetDataPlaneNamespace()
	err = remoteCli.Get(ctx, types.NamespacedName{
		Name:      tioperator.DefaultTiBRGCRName,
		Namespace: namespace,
	}, tibrgcCR)
	if err != nil {
		tilog.Error(ctx, "failed to get tibrgc", zap.String("name", tioperator.DefaultTiBRGCRName), zap.String("namespace", namespace), zap.Error(err))
		return false, fmt.Errorf("failed to get tibrgc: %w", err)
	}

	if tibrgcCR.Status.Phase != v1alpha1br.TiBRGCPhaseRunning {
		tilog.Error(ctx, "tibrgc is not running", zap.String("name", tioperator.DefaultTiBRGCRName), zap.String("namespace", namespace))
		return false, nil
	}

	tilog.Info(ctx, "tibrgc is running", zap.String("name", tioperator.DefaultTiBRGCRName), zap.String("namespace", namespace))

	return true, nil
}

func (a *applyTiBRGCAction) applyTiBRGC(ctx context.Context, backupGcSetting *entity.BackupGcSetting) error {
	sharedPool, err := a.sharedPoolRepo.Load(ctx, backupGcSetting.SharedPoolID, true)
	if err != nil {
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}
	if sharedPool.DeletedAt != nil {
		tilog.Info(ctx, "shared pool is deleted, skip applying TiBRGC",
			zap.String("sharedPoolId", backupGcSetting.SharedPoolID))
		return nil
	}

	yamlStr, err := a.rednerTiBRGCR(ctx, backupGcSetting, sharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to render tibrgc CR yaml", zap.Error(err))
		return fmt.Errorf("failed to render tibrgc 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(sharedPool.GetDataplaneName())
	if err != nil {
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	certCR := cert.TiBRGCRCert(tioperator.DefaultTiBRGCRName, sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, certCR); err != nil {
		tilog.Error(ctx, "failed to create tibrgc cert", zap.Any("cert", certCR), zap.Error(err))
		return err
	}

	toCr := resourceCr.DeepCopy()
	opResult, err := controllerutil.CreateOrUpdate(ctx, remoteCli, toCr, func() error {
		toCr.Object["spec"] = resourceCr.Object["spec"]
		toCr.SetAnnotations(resourceCr.GetAnnotations())
		toCr.SetLabels(resourceCr.GetLabels())
		return nil
	})
	if err != nil {
		tilog.Error(ctx, "failed to create or update tibrgc", zap.Any("resourceCr", resourceCr), zap.Error(err))
		return fmt.Errorf("failed to create or update tibrgc: %w", err)
	}

	tilog.Info(ctx, " create or update tibrgc success", zap.Any("opResult", opResult), zap.String("yaml", yamlStr))
	return nil
}

func (a *applyTiBRGCAction) rednerTiBRGCR(ctx context.Context, backupGcSetting *entity.BackupGcSetting, sharedPool *entity.SharedPool) (string, error) {
	cpu, memory, err := GetAvailableTiBRGCCpuMemory(ctx, sharedPool, a.dataplaneManager)
	if err != nil {
		return "", fmt.Errorf("failed to get available tibrgc cpu memory: %w", err)
	}

	tenant, placement, err := getTiBRNodePlacement(
		ctx,
		a.dpServiceCli,
		sharedPool.GetDataplaneName(),
		fmt.Sprintf("%d", cpu),
		memory,
	)
	if err != nil {
		return "", fmt.Errorf("failed to resolve node placement for tibrgc: %w", err)
	}

	t2Image, t2Options, err := a.getT2ImageAndOptions(ctx, sharedPool, tenant, cpu)
	if err != nil {
		return "", fmt.Errorf("failed to get t2 image and options: %w", err)
	}

	t3Image, t3Options, err := a.getT3ImageAndOptions(ctx, sharedPool, tenant, backupGcSetting, cpu)
	if err != nil {
		return "", fmt.Errorf("failed to get t3 image and options: %w", err)
	}

	return a.doRenderTiBRGC(ctx, backupGcSetting, sharedPool, cpu, memory, tenant, placement, t2Image, t2Options, t3Image, t3Options)
}

func (a *applyTiBRGCAction) doRenderTiBRGC(ctx context.Context, backupGcSetting *entity.BackupGcSetting, sharedPool *entity.SharedPool, cpu uint64, memory, tenant string,
	placement TiBRNodePlacement, t2Image string, t2Options map[string]string, t3Image string, t3Options map[string]string) (string, error) {
	tplArgs := map[string]string{
		"Name":      tioperator.DefaultTiBRGCRName,
		"Namespace": sharedPool.GetDataPlaneNamespace(),
		"T2Image":   t2Image,
		"T3Image":   t3Image,
		"DfsPrefix": sharedPool.GetDfsPrefix(),
		"DfsBucket": sharedPool.GetObjectStorage().GetBucket(),
		"DfsRegion": sharedPool.GetObjectStorage().GetRegion(),

		"MinT2DataAgeDays": fmt.Sprintf("%d", getMinT2DataAgeDays(&a.bizConfig.DefaultTiBRConfig, backupGcSetting.MinT2DataAgeDays)),
		"T2Schedule":       "0 0 * * *",
		"T2Options":        genOptionsStr(t2Options),
		"T2ResourceCPU":    fmt.Sprintf("%d", cpu),
		"T2ResourceMemory": memory,

		"MinT3DataAgeDays":      fmt.Sprintf("%d", backupGcSetting.MinT3DataAgeDays),
		"T3Schedule":            "30 0 * * *",
		"T3Options":             genOptionsStr(t3Options),
		"T3ResourceCPU":         fmt.Sprintf("%d", cpu),
		"T3ResourceMemory":      memory,
		"T3ResourceStorageSize": GetArchiveBackupStorageSize(&a.bizConfig.DefaultTiBRConfig),

		"ResourceStorageClass": GetStorageClass(&a.bizConfig.DefaultTiBRConfig, sharedPool.GetProvider().GetType()),

		"Tenant":       tenant,
		"ResourcePool": sharedPool.GetId(),
		"NodeSpec":     placement.NodeSpec,
		"NodeVersion":  placement.NodeVersion,
	}

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

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

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

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

func getMinT2DataAgeDays(cfg *config.TiBRConfig, fallbackDays uint32) uint32 {
	if cfg != nil && cfg.MinT2DataAgeDays > 0 {
		return cfg.MinT2DataAgeDays
	}
	return fallbackDays
}

func (a *applyTiBRGCAction) getT2ImageAndOptions(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string, cpu uint64) (string, map[string]string, error) {
	if a.bizConfig.DefaultTiBRConfig.IntegrateConfigCenter {
		return a.getT2ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, cpu)
	}

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

	options := map[string]string{
		"file-types-whitelist": "sst",
		"concurrency":          fmt.Sprintf("%d", cpu*16),
	}

	return image, options, nil
}

func (a *applyTiBRGCAction) getT2ImageAndOptionsFromConfigCenter(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string, cpu uint64) (string, map[string]string, error) {
	_, cfg, err := a.clusterConfigClient.GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T2_DFSGC)
	if err != nil {
		return "", nil, fmt.Errorf("failed to get %s version and config: %w", apiv1.ComponentType_TIBRGC_T2_DFSGC.String(), err)
	}
	image := fmt.Sprintf("%s/%s/%s:%s",
		a.imageConfig.Registry,
		a.imageConfig.RepoPrefix,
		path.Base(cfg.GetImageRepo()),
		cfg.GetImageTag(),
	)
	tomlConfig := schemav1.NewTomlConfig(map[string]any{})
	if err := tomlConfig.UnmarshalTOML([]byte(cfg.GetConfigurationFileContent())); err != nil {
		return "", nil, fmt.Errorf("failed to unmarshal options: %w", err)
	}
	options := map[string]string{}
	for key, value := range tomlConfig.MP {
		options[key] = fmt.Sprintf("%v", value)
	}
	if options["concurrency"] == "" {
		options["concurrency"] = fmt.Sprintf("%d", cpu*16)
	}
	if options["file-types-whitelist"] == "" {
		options["file-types-whitelist"] = "sst"
	}

	// Notice: no allow user config this in configure center
	delete(options, "start-time-safe-interval")

	return image, options, nil
}

func (a *applyTiBRGCAction) getT3ImageAndOptions(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string, backupGcSetting *entity.BackupGcSetting, cpu uint64) (string, map[string]string, error) {
	if a.bizConfig.DefaultTiBRConfig.IntegrateConfigCenter {
		return a.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, cpu)
	}

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

	options := map[string]string{
		"concurrency":     fmt.Sprintf("%d", cpu*8),
		"expiration-date": backupGcSetting.CreatedAt.Format("20060102"),
	}
	if v := strings.TrimSpace(a.bizConfig.DefaultTiBRConfig.ArchiveMaxThroughput); v != "" {
		options["max-throughput"] = v
	}

	return image, options, nil
}

func (a *applyTiBRGCAction) getT3ImageAndOptionsFromConfigCenter(ctx context.Context, targetSharedPool *entity.SharedPool, tenantId string, backupGcSetting *entity.BackupGcSetting, cpu uint64) (string, map[string]string, error) {
	_, cfg, err := a.clusterConfigClient.GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE)
	if err != nil {
		return "", nil, fmt.Errorf("failed to get %s version and config: %w", apiv1.ComponentType_TIBRGC_T3_ARCHIVE.String(), err)
	}
	image := fmt.Sprintf("%s/%s/%s:%s",
		a.imageConfig.Registry,
		a.imageConfig.RepoPrefix,
		path.Base(cfg.GetImageRepo()),
		cfg.GetImageTag(),
	)
	tomlConfig := schemav1.NewTomlConfig(map[string]any{})
	if err := tomlConfig.UnmarshalTOML([]byte(cfg.GetConfigurationFileContent())); err != nil {
		return "", nil, fmt.Errorf("failed to unmarshal options: %w", err)
	}
	options := map[string]string{}
	for key, value := range tomlConfig.MP {
		options[key] = fmt.Sprintf("%v", value)
	}

	if strings.TrimSpace(options["expiration-date"]) == "" {
		options["expiration-date"] = backupGcSetting.CreatedAt.Format("20060102")
	}
	if options["concurrency"] == "" {
		options["concurrency"] = fmt.Sprintf("%d", cpu*8)
	}

	// config-center has higher priority; only apply local config if not set by config-center
	if options["max-throughput"] == "" && a.bizConfig != nil {
		if v := strings.TrimSpace(a.bizConfig.DefaultTiBRConfig.ArchiveMaxThroughput); v != "" {
			options["max-throughput"] = v
		}
	}

	// Notice: no allow user config this in configure center
	delete(options, "start-archive-duration")
	delete(options, "data-dir")

	return image, options, nil
}
