package action

import (
	"fmt"
	"strconv"

	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/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"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/protobuf/encoding/protojson"
)

type ApplyPDGroupAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	templateVersionSvc  *templateversion.Service
	imageConfig         *config.ImageConfig
	clusterConfigClient clusterconfig.Client
	dataplaneClient     dplanemgrv1.DataPlaneServiceClient
	meteringConfig      *config.MeteringConfig
}

const ApplyPDGroupActionName = "apply-pd-group"

func NewApplyPDGroupAction(
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	clusterConfigClient clusterconfig.Client,
	dataplaneClient dplanemgrv1.DataPlaneServiceClient,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
	meteringConfig *config.MeteringConfig,
) actions.AsyncAction {
	return &ApplyPDGroupAction{
		sharedPoolRepo:      sharedPoolRepo,
		dataplaneManager:    dataplaneManager,
		clusterConfigClient: clusterConfigClient,
		dataplaneClient:     dataplaneClient,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
		meteringConfig:      meteringConfig,
	}
}

func (a *ApplyPDGroupAction) Name() string {
	return ApplyPDGroupActionName
}

func (a *ApplyPDGroupAction) Start(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply pd group action")

	_, err := args.GetString(workflow.ComponentIdsKey)
	if err != nil {
		return fmt.Errorf("target component id is required, but not found")
	}

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

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

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

	var tempSharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedPool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedPool: %w", err)
	}

	sharedPool.ComponentGroups = tempSharedPool.GetComponentGroups()
	if sharedPool.GetVersion() == "" {
		sharedPool.Version = tempSharedPool.GetVersion()
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))

		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	nodeVersionMap, err := getNodeVersions(ctx, a.dataplaneClient, sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get node versions for shared pool", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
		return fmt.Errorf("failed to get node versions for shared pool: %w", err)
	}

	for _, comp := range sharedPool.GetComponentGroups() {

		if comp.GetComponentType() != apiv1.ComponentType_PD &&
			comp.GetComponentType() != apiv1.ComponentType_PD_TSO &&
			comp.GetComponentType() != apiv1.ComponentType_PD_SCHEDULING &&
			comp.GetComponentType() != apiv1.ComponentType_RESOURCE_MANAGER {
			tilog.Info(ctx, "only applying pd component group, skip other component types",
				zap.String("componentId", comp.GetId()),
				zap.String("componentType", entity.GetComponentTypeName(comp)),
			)
			continue
		}

		// check if the component is target component
		if !workflow.CheckIsTargetComponent(ctx, comp, args) {
			tilog.Info(ctx, "skip apply component", zap.String("componentId", comp.GetId()), zap.String("componentType", comp.GetComponentType().String()))
			continue
		}

		if comp.GetPhase() == apiv1.Component_AVAILABLE {
			tilog.Info(ctx, "component is already available, skip applying", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)))
			continue
		}

		// full component config from config center
		err = workflow.FullComponentConfigVersion(ctx, a.clusterConfigClient, a.meteringConfig, sharedPool, comp, "")
		if err != nil {
			tilog.Error(ctx, "failed to full component config", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to full component config: %w", err)
		}

		// build template args
		templArgs, err := buildCompTempArgs(a.imageConfig, comp, sharedPool, nodeVersionMap)
		if err != nil {
			tilog.Error(ctx, "failed to build component template args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to build component template args: %w", err)
		}

		if err := workflow.AddAzureDfsArgs(sharedPool, templArgs); err != nil {
			tilog.Error(ctx, "failed to add azure args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to add azure args: %w", err)
		}

		if comp.GetPhase() != apiv1.Component_CREATING && comp.GetComponentType() == apiv1.ComponentType_PD {
			templArgs["Bootstrapped"] = "true"
		}

		for _, storage := range comp.GetStorages() {
			switch storage.GetName() {
			case apiv1.Component_Storage_DATA:
				templArgs["DataStorageSize"] = strconv.FormatUint(uint64(storage.GetSizeGi()), 10)
				templArgs["DataStorageClass"] = workflow.GenStorageClassName(sharedPool, storage)
				templArgs["DataVolumeAttributesClass"] = workflow.GenVolumeAttributesClassName(storage)
			default:
				tilog.Warn(ctx, "unsupported storage type for pd", zap.String("storage", storage.GetName().String()))
				continue
			}
		}

		templateName, err := entity.GetComponentTemplateName(comp)
		if err != nil {
			tilog.Error(ctx, "failed to get component template name", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to get component template name: %w", err)
		}

		// if component phase is deleting, set needDelete to true
		err = templateExecuteWithVersionResolver(
			ctx, remoteCli, templateName, comp.GetTemplateVersion(), templArgs,
			comp.GetPhase() == apiv1.Component_DELETING, a.templateVersionSvc,
		)
		if err != nil {
			tilog.Error(ctx, "failed to execute template", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to execute template: %w", err)
		}
	}

	return nil
}

func (a *ApplyPDGroupAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply pd group check finish")

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

	// load cluster block
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return false, fmt.Errorf("failed to load sharedPool: %w", err)
	}

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

	force, _ := args.GetString(workflow.ForceKey)
	for _, comp := range sharedPool.GetComponentGroups() {

		// if target component is pd, can not skip
		// for other component, if force scale is not enabled, need to wait for pd component ready
		if !workflow.CheckIsTargetComponent(ctx, comp, args) && force == workflow.ForceValue {
			tilog.Info(ctx, "force scale, so skip wait pd comp ready", zap.String("componentId", comp.GetId()), zap.String("componentType", comp.GetComponentType().String()))
			continue
		}

		switch comp.GetComponentType() {
		case apiv1.ComponentType_PD:
			pdGroup, err := tioperator.GetPDGroup(ctx, remoteCli, sharedPoolId, comp.GetId())
			if err != nil {
				tilog.Error(ctx, "failed to get pd group", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
				return false, fmt.Errorf("failed to get pd group: %w", err)
			}

			if pdGroup.GetPhase() != apiv1.Component_AVAILABLE {
				tilog.Info(ctx, "pd group is not available yet", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.String("phase", pdGroup.GetPhase().String()))
				return false, nil
			}
		case apiv1.ComponentType_PD_TSO:
			tsoGroup, err := tioperator.GetTsoGroup(ctx, remoteCli, sharedPoolId, comp.GetId())
			if err != nil {
				tilog.Error(ctx, "failed to get pd tso group", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
				return false, fmt.Errorf("failed to get pd tso group: %w", err)
			}
			if tsoGroup.GetPhase() != apiv1.Component_AVAILABLE {
				tilog.Info(ctx, "pd tso group is not available yet", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.String("phase", tsoGroup.GetPhase().String()))
				return false, nil
			}

		case apiv1.ComponentType_PD_SCHEDULING:
			schedulingGroup, err := tioperator.GetSchedulingGroup(ctx, remoteCli, sharedPoolId, comp.GetId())
			if err != nil {
				tilog.Error(ctx, "failed to get pd scheduling group", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
				return false, fmt.Errorf("failed to get pd scheduling group: %w", err)
			}
			if schedulingGroup.GetPhase() != apiv1.Component_AVAILABLE {
				tilog.Info(ctx, "pd scheduling group is not available yet", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.String("phase", schedulingGroup.GetPhase().String()))
				return false, nil
			}
		case apiv1.ComponentType_RESOURCE_MANAGER:
			rmGroup, err := tioperator.GetResourceManagerGroup(ctx, remoteCli, sharedPoolId, comp.GetId())
			if err != nil {
				tilog.Error(ctx, "failed to get resource manager group", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
				return false, fmt.Errorf("failed to get resource manager group: %w", err)
			}
			if rmGroup.GetPhase() != apiv1.Component_AVAILABLE {
				tilog.Info(ctx, "resource manager group is not available yet", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.String("phase", rmGroup.GetPhase().String()))
				return false, nil
			}
		}
	}

	return true, nil
}
