package clusteraction

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"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"
)

var (
	ActionScaleCluster opent.ActionMetadata = opent.ActionMetadata{
		Name: "ScaleCluster",
		Type: opent.ActionTypeAuto,
	}
)

type ClusterComponent struct {
	ComponentType   string `json:"component_type"`
	NodeProfileName string `json:"node_profile_name"`
	Replicas        int32  `json:"replicas"`
	StorageSizeGi   int64  `json:"storage_size_gi"`
	TiDBGroupID     string `json:"tidb_group_id"`
}

type ScaleClusterParam struct {
	Components []ClusterComponent `json:"components"`
}

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

func NewScaleCluster(comps *actions.Components) actions.AutoAction {
	return &scaleCluster{comps: comps}
}

func (a *scaleCluster) Metadata() opent.ActionMetadata {
	return ActionScaleCluster
}

func (a *scaleCluster) PreCheck(ctx context.Context, instance *opent.Instance) error {
	_, _, err := a.getParam(ctx, instance)
	if err != nil {
		return err
	}

	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return err
	}

	cluster, err := a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if cluster.Status != core.ClusterStatus_CLUSTER_STATUS_NORMAL {
		return fmt.Errorf("cluster %d is not normal", cid)
	}

	return err
}

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

	target := actions.ScaleTarget{ComponentType: "Cluster"}
	scaleOpt, _ := instance.GetParam(opent.ParamClusterComponentsToScale)
	var comps ScaleClusterParam
	if json.Unmarshal([]byte(scaleOpt), &comps) == nil && len(comps.Components) > 0 {
		c := comps.Components[0]
		target.ComponentType = c.ComponentType
		target.TargetNodeCount = uint32(c.Replicas)
	}

	return actions.ClusterScaleAdvisory(ctx, log.FromContextOrDef(ctx, a.comps.Logger), a.comps.ClinicCli, a.comps.GeminiCli, strconv.FormatUint(cid, 10), target)
}

func (a *scaleCluster) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	req, newArch, err := a.getParam(ctx, instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	if newArch {
		_, err = a.comps.CoreSvc.Mgmt.Cluster.ScaleClusterComponents(ctx, req.(*mgmtv1.ScaleClusterComponentsRequest))
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
	} else {
		resp, err := a.comps.CoreSvc.Core.ScaleCluster(ctx, req.(*core.ScaleClusterReq))
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
			return actions.FailedActionResult(err), nil
		}
	}

	return actions.RunningActionResult("sent request to scale this cluster", nil), nil
}

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

	cluster, err := a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}

	if cluster.Status == core.ClusterStatus_CLUSTER_STATUS_NORMAL {
		return actions.NewActionResult(opent.ActionStatusSucceeded, "", nil), nil
	}

	return actions.RunningActionResult(fmt.Sprintf("cluster status is '%s'", cluster.Status), nil), nil
}

func (a *scaleCluster) 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
	}

	scaleOpt, _ := instance.GetParam(opent.ParamClusterComponentsToScale)
	if scaleOpt == "" {
		return nil, false, fmt.Errorf("scale param is required")
	}

	var comps ScaleClusterParam
	err = json.Unmarshal([]byte(scaleOpt), &comps)
	if err != nil {
		return nil, false, err
	}
	if newArch {
		return a.build3LayerArchParam(ctx, cluster, comps)
	} else {
		return a.buildCloudNativeArchParam(ctx, cid, comps)
	}
}

func (a *scaleCluster) build3LayerArchParam(ctx context.Context, cluster *core.Cluster, comps ScaleClusterParam) (*mgmtv1.ScaleClusterComponentsRequest, bool, error) {
	components := make([]*dediv1.Cluster_ClusterComponent, 0, len(comps.Components))

	for _, c := range comps.Components {
		newCom := &dediv1.Cluster_ClusterComponent{
			ComponentType: get3LayerArchClusterComponentType(c.ComponentType),
			SpuName:       c.NodeProfileName,
			Count:         get3LayerArchClusterComponentCount(c.Replicas),
		}
		if newCom.ComponentType == dediv1.ComponentType_TIDB {
			if c.TiDBGroupID != "" {
				tidbGroupID, err := strconv.ParseUint(c.TiDBGroupID, 10, 64)
				if err != nil {
					return nil, false, err
				}
				newCom.TidbGroupId = tidbGroupID
			}
		}
		if c.StorageSizeGi != 0 {
			newCom.Storages = map[string]*dediv1.Cluster_ClusterComponent_Storage{
				dediv1.Sku_DATA.String(): {
					SizeGi: uint32(c.StorageSizeGi),
				},
			}
		}
		components = append(components, newCom)
	}

	return &mgmtv1.ScaleClusterComponentsRequest{
		ClusterId:         cluster.Id,
		ClusterComponents: components,
	}, true, nil
}

func (a *scaleCluster) buildCloudNativeArchParam(ctx context.Context, cid uint64, comps ScaleClusterParam) (*core.ScaleClusterReq, bool, error) {
	components := make([]*core.ClusterComponent, 0, len(comps.Components))
	for _, c := range comps.Components {
		components = append(components, &core.ClusterComponent{
			ComponentType:   core.ComponentType(core.ComponentType_value[c.ComponentType]),
			NodeProfileName: c.NodeProfileName,
			Replicas:        c.Replicas,
			StorageSizeGi:   c.StorageSizeGi,
		})
	}
	return &core.ScaleClusterReq{
		ClusterId:      cid,
		NewComponents:  components,
		AllowScaleIn:   true,
		AllowScaleDown: false,
		OperatorId:     0,
		BaseReq:        nil,
	}, false, nil
}

func get3LayerArchClusterComponentType(componentType string) dediv1.ComponentType {
	switch componentType {
	case "COMPONENT_TYPE_TIDB":
		return dediv1.ComponentType_TIDB
	case "COMPONENT_TYPE_TIKV":
		return dediv1.ComponentType_TIKV
	case "COMPONENT_TYPE_TIFLASH":
		return dediv1.ComponentType_TIFLASH
	case "COMPONENT_TYPE_PD":
		return dediv1.ComponentType_PD
	case "COMPONENT_TYPE_TIPROXY":
		return dediv1.ComponentType_TIPROXY
	default:
		return dediv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	}
}

func get3LayerArchClusterComponentCount(count int32) *uint32 {
	countNew := uint32(count)
	return &countNew
}
