package tcaction

import (
	"context"
	"fmt"
	"strconv"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/utils/grpcutil"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	dediv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"

	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"
	apiCommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
)

var (
	ActionScaleTC opent.ActionMetadata = opent.ActionMetadata{
		Name: "ScaleTC",
		Type: opent.ActionTypeAuto,
	}
)

type scaleTC struct {
	actions.BaseAction
	comps *actions.Components
}

func NewScaleTC(comps *actions.Components) actions.AutoAction {
	return &scaleTC{comps: comps}
}

func (a *scaleTC) Metadata() opent.ActionMetadata {
	return ActionScaleTC
}

func (a *scaleTC) PreCheck(ctx context.Context, instance *opent.Instance) error {
	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return err
	}

	_, err = a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if err != nil {
		return err
	}

	return nil
}

func (a *scaleTC) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	scaleClusterReq, newArch, err := a.getParam(ctx, instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}
	output := make(map[opent.OutputName]string)
	if newArch {
		_, err = a.comps.CoreSvc.Mgmt.Cluster.ScaleClusterComponents(ctx, scaleClusterReq.(*mgmtv1.ScaleClusterComponentsRequest))
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		output[opent.OutputScaleClusterID] = strconv.FormatUint(scaleClusterReq.(*mgmtv1.ScaleClusterComponentsRequest).ClusterId, 10)
	} else {
		resp, err := a.comps.CoreSvc.Core.ScaleCluster(ctx, scaleClusterReq.(*core.ScaleClusterReq))
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
			return actions.FailedActionResult(err), nil
		}
		output[opent.OutputScaleClusterID] = strconv.FormatUint(scaleClusterReq.(*core.ScaleClusterReq).ClusterId, 10)
	}

	return actions.RunningActionResult("sent request to scale TC(s)", output), nil
}

func (a *scaleTC) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return nil, err
	}
	componentType, _ := instance.GetParam(opent.ParamComponentType)
	if componentType == "" {
		return nil, fmt.Errorf("component type is required")
	}

	// check components ready
	cluster, err := a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}
	clusterComponent := getClusterComponent(cluster, getClusterComponentType(componentType))

	tcs, err := a.comps.MgmtHTTP.ClusterAgent(strconv.FormatUint(cid, 10)).ListTCs(ctx)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}
	for _, tc := range tcs {
		tcComponent := getTCComponent(tc, getTCComponentType(componentType))
		if clusterComponent == nil || tcComponent == nil {
			continue
		}
		if clusterComponent.ComputeInstanceType != tcComponent.ProviderInstanceType {
			return actions.RunningActionResult(
				fmt.Sprintf("instance of the component %s is expected to be %s, but it is currently %s",
					tcComponent.ComponentType.String(),
					clusterComponent.ComputeInstanceType,
					tcComponent.ProviderInstanceType),
				nil), nil
		}
	}

	// check nodes ready
	nodeReadyReq, err := a.getNodeReadyReq(ctx, instance)
	if err != nil {
		return nil, err
	}
	resp, err := a.comps.MgmtHTTP.ClusterAgent(nodeReadyReq.ClusterId).IsNodeReadyForScaleUpDown(ctx, nodeReadyReq)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}
	for _, result := range resp.ComponentResults {
		if !result.Ready {
			return actions.RunningActionResult(fmt.Sprintf("node(s) is not ready, reason: %s", result.ReadyReason), nil), nil
		}
	}

	return actions.SucceededActionResult(nil), nil
}

func (a *scaleTC) getParam(ctx context.Context, instance *opent.Instance) (interface{}, bool, error) {
	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return nil, false, err
	}
	cluster, err := a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if err != nil {
		return nil, false, err
	}

	newArch := false
	if cluster.Attributes != nil && cluster.Attributes.UseNewDedicatedArch != nil {
		newArch = *cluster.Attributes.UseNewDedicatedArch
	}
	if newArch {
		return a.build3LayerArchParam(ctx, cluster, instance)
	} else {
		return a.buildCloudNativeArchParam(ctx, cluster, instance)
	}
}

func (a *scaleTC) getNodeReadyReq(ctx context.Context, instance *opent.Instance) (*apiv1.IsNodeReadyForScaleUpDownReq, error) {
	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return nil, err
	}
	componentType, _ := instance.GetParam(opent.ParamComponentType)

	if componentType == "" {
		return nil, fmt.Errorf("component type is required")
	}
	tcComponentType := getTCComponentType(componentType)
	nodeReadyReq := &apiv1.IsNodeReadyForScaleUpDownReq{
		ClusterId:      strconv.FormatUint(cid, 10),
		ComponentTypes: []apiCommon.ComponentType{tcComponentType},
	}
	return nodeReadyReq, nil
}

func getTCComponent(tc *apiv1.TC, componentType apiCommon.ComponentType) *apiv1.TC_ComponentSpec {
	for _, spec := range tc.ComponentSpecs {
		if spec.ComponentType == componentType {
			return spec
		}
	}
	return nil
}

func getTCComponentType(componentType string) apiCommon.ComponentType {
	switch componentType {
	case "tidb":
		return apiCommon.ComponentType_TIDB
	case "tikv":
		return apiCommon.ComponentType_TIKV
	case "tiflash":
		return apiCommon.ComponentType_TIFLASH
	case "pd":
		return apiCommon.ComponentType_PD
	case "tiproxy":
		return apiCommon.ComponentType_TIPROXY
	default:
		return apiCommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	}
}

func getClusterComponent(cluster *core.Cluster, componentType core.ComponentType) *core.ClusterComponent {
	for _, component := range cluster.Components {
		if component.ComponentType == componentType {
			return component
		}
	}
	return nil
}

func getClusterComponentType(componentType string) core.ComponentType {
	switch componentType {
	case "tidb":
		return core.ComponentType_COMPONENT_TYPE_TIDB
	case "tikv":
		return core.ComponentType_COMPONENT_TYPE_TIKV
	case "tiflash":
		return core.ComponentType_COMPONENT_TYPE_TIFLASH
	case "pd":
		return core.ComponentType_COMPONENT_TYPE_PD
	case "tiproxy":
		return core.ComponentType_COMPONENT_TYPE_TIPROXY
	default:
		return core.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	}
}

func get3LayerArchClusterComponentType(componentType string) dediv1.ComponentType {
	switch componentType {
	case "tidb":
		return dediv1.ComponentType_TIDB
	case "tikv":
		return dediv1.ComponentType_TIKV
	case "tiflash":
		return dediv1.ComponentType_TIFLASH
	case "pd":
		return dediv1.ComponentType_PD
	case "tiproxy":
		return dediv1.ComponentType_TIPROXY
	default:
		return dediv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	}
}

func (a *scaleTC) build3LayerArchParam(ctx context.Context, cluster *core.Cluster, instance *opent.Instance) (*mgmtv1.ScaleClusterComponentsRequest, bool, error) {
	componentType, _ := instance.GetParam(opent.ParamComponentType)
	nodeSize, _ := instance.GetParam(opent.ParamNodeSize)
	if componentType == "" || nodeSize == "" {
		return nil, true, fmt.Errorf("scale param is required, componentType=%s, nodeSize=%s", componentType, nodeSize)
	}

	component := &dediv1.Cluster_ClusterComponent{
		ComponentType: get3LayerArchClusterComponentType(componentType),
		SpuName:       nodeSize,
	}

	return &mgmtv1.ScaleClusterComponentsRequest{
		ClusterId: cluster.Id,
		ClusterComponents: []*dediv1.Cluster_ClusterComponent{
			component,
		},
		ForceScale: instance.Params[opent.ParamForceScale] == "true",
	}, true, nil
}

func (a *scaleTC) buildCloudNativeArchParam(ctx context.Context, cluster *core.Cluster, instance *opent.Instance) (*core.ScaleClusterReq, bool, error) {
	components := cluster.Components
	componentType, _ := instance.GetParam(opent.ParamComponentType)
	nodeSize, _ := instance.GetParam(opent.ParamNodeSize)

	if componentType == "" || nodeSize == "" {
		return nil, false, fmt.Errorf("scale param is required, componentType=%s, nodeSize=%s", componentType, nodeSize)
	}

	for i, c := range components {
		if c.ComponentType == getClusterComponentType(componentType) {
			components[i].NodeProfileName = nodeSize
		}
	}

	return &core.ScaleClusterReq{
		ClusterId:      cluster.Id,
		NewComponents:  components,
		AllowScaleIn:   false,
		AllowScaleDown: true,
		OperatorId:     0,
		BaseReq:        nil,
	}, false, nil
}
