package region

import (
	"context"
	"math"
	"strconv"
	"strings"

	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (t serverImpl) GetByocSharedComponentScaleConstraints(
	ctx context.Context,
	req *openapingv1beta2.GetByocSharedComponentScaleConstraintsRequest,
) (*openapingv1beta2.ByocSharedComponentScaleConstraints, error) {
	ctx, span := tracer.Start(ctx, "GetByocSharedComponentScaleConstraints")
	defer span.End()

	reqMeta, resourcePoolID, err := t.loadOwnedResourcePool(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}

	resp, err := t.nextgenCli.GetByocSharedComponentScaleConstraints(ctx, &globalngv1.GetByocSharedComponentScaleConstraintsRequest{
		ResourcePoolId: resourcePoolID,
		OrganizationId: reqMeta.OrgID,
	})
	if err != nil {
		return nil, err
	}

	return convertByocSharedComponentScaleConstraints(resp), nil
}

func (t serverImpl) ScaleByocSharedComponents(
	ctx context.Context,
	req *openapingv1beta2.ScaleByocSharedComponentsRequest,
) (*openapingv1beta2.ScaleByocSharedComponentsResponse, error) {
	ctx, span := tracer.Start(ctx, "ScaleByocSharedComponents")
	defer span.End()

	reqMeta, resourcePoolID, err := t.loadOwnedResourcePool(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	if err := validateScaleByocSharedComponentsRequest(req); err != nil {
		return nil, err
	}

	var tikv *globalngv1.ByocSharedComponentScaleTarget
	if req.GetTikv() != nil {
		tikv, err = convertScaleTargetToGlobal(req.GetTikv())
		if err != nil {
			return nil, err
		}
	}
	var tiflashWrite *globalngv1.ByocSharedComponentScaleTarget
	if req.GetTiflashWrite() != nil {
		tiflashWrite, err = convertScaleTargetToGlobal(req.GetTiflashWrite())
		if err != nil {
			return nil, err
		}
	}
	var tiflashCompute *globalngv1.ByocSharedComponentScaleTarget
	if req.GetTiflashCompute() != nil {
		tiflashCompute, err = convertScaleTargetToGlobal(req.GetTiflashCompute())
		if err != nil {
			return nil, err
		}
	}

	resp, err := t.nextgenCli.ScaleByocSharedComponents(ctx, &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: resourcePoolID,
		OrganizationId: reqMeta.OrgID,
		Tikv:           tikv,
		TiflashWrite:   tiflashWrite,
		TiflashCompute: tiflashCompute,
		ValidateOnly:   req.GetValidateOnly(),
	})
	if err != nil {
		return nil, err
	}

	return convertByocSharedComponentsScaleResponse(resp), nil
}

func (t serverImpl) loadOwnedResourcePool(
	ctx context.Context,
	resourcePoolIDStr string,
) (*metadata.RequestMeta, uint64, error) {
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	resourcePoolID, err := strconv.ParseUint(resourcePoolIDStr, 10, 64)
	if err != nil {
		return nil, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid resource_pool_id")
	}

	poolProfile, err := t.nextgenCli.GetResourcePool(ctx, &globalngv1.GetResourcePoolRequest{
		ResourcePoolId: resourcePoolID,
	})
	if err != nil {
		return nil, 0, err
	}
	if poolProfile.GetResourcePool().GetOwnerType() != globalngv1.ResourcePool_ORGANIZATION ||
		poolProfile.GetResourcePool().GetOwnerId() != reqMeta.OrgID {
		return nil, 0, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	if poolProfile.GetResourcePool().GetServicePlan() != globalcommonv1.ServicePlan_BYOC {
		return nil, 0, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}

	return reqMeta, resourcePoolID, nil
}

func convertByocSharedComponentScaleConstraints(
	in *globalngv1.ByocSharedComponentScaleConstraints,
) *openapingv1beta2.ByocSharedComponentScaleConstraints {
	if in == nil {
		return nil
	}

	out := &openapingv1beta2.ByocSharedComponentScaleConstraints{
		ResourcePoolId: strconv.FormatUint(in.GetResourcePoolId(), 10),
		VcpusLimit:     int64(in.GetVcpusLimit()),
		Preview:        convertScalePreviewToOpenAPI(in.GetPreview()),
	}
	for _, component := range in.GetComponents() {
		out.Components = append(out.Components, &openapingv1beta2.ByocSharedComponentScaleConstraint{
			ComponentType:        convertComponentTypeToOpenAPI(component.GetComponentType()),
			ComponentId:          component.GetComponentId(),
			Exists:               component.GetExists(),
			Phase:                component.GetPhase(),
			Current:              convertScaleTargetToOpenAPI(component.GetCurrent()),
			BlockingReasons:      convertConstraintReasonsToOpenAPI(component.GetBlockingReasons()),
			NodeCountConstraints: convertNodeCountConstraintsToOpenAPI(component.GetNodeCountConstraints()),
			NodeSpecConstraints:  convertNodeSpecConstraintsToOpenAPI(component.GetNodeSpecConstraints()),
		})
	}

	return out
}

func convertByocSharedComponentsScaleResponse(
	in *globalngv1.ScaleByocSharedComponentsResponse,
) *openapingv1beta2.ScaleByocSharedComponentsResponse {
	if in == nil {
		return nil
	}

	out := &openapingv1beta2.ScaleByocSharedComponentsResponse{
		Preview:     convertScalePreviewToOpenAPI(in.GetPreview()),
		OperationId: in.GetOperationId(),
	}
	for _, component := range in.GetComponents() {
		out.Components = append(out.Components, &openapingv1beta2.ByocSharedComponentScalePlanComponent{
			ComponentType:   convertComponentTypeToOpenAPI(component.GetComponentType()),
			ComponentId:     component.GetComponentId(),
			Current:         convertScaleTargetToOpenAPI(component.GetCurrent()),
			Target:          convertScaleTargetToOpenAPI(component.GetTarget()),
			Allowed:         component.GetAllowed(),
			BlockingReasons: component.GetBlockingReasons(),
		})
	}

	return out
}

func convertScalePreviewToOpenAPI(
	in *globalngv1.ByocSharedComponentScalePreviewSummary,
) *openapingv1beta2.ByocSharedComponentScalePreviewSummary {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.ByocSharedComponentScalePreviewSummary{
		CurrentVcpusUsed: int64(in.GetCurrentVcpusUsed()),
		ScalingVcpusUsed: int64(in.GetScalingVcpusUsed()),
		MaxVcpusLimit:    int64(in.GetMaxVcpusLimit()),
		Allowed:          in.GetAllowed(),
		BlockingReasons:  in.GetBlockingReasons(),
	}
}

func convertScaleTargetToOpenAPI(
	in *globalngv1.ByocSharedComponentScaleTarget,
) *openapingv1beta2.ByocSharedComponentScaleTarget {
	if in == nil {
		return nil
	}

	out := &openapingv1beta2.ByocSharedComponentScaleTarget{
		Storages: make([]*openapingv1beta2.ByocSharedComponentStorage, 0, len(in.GetStorages())),
	}
	if in.NodeCount != nil {
		out.NodeCount = proto.Int64(int64(in.GetNodeCount()))
	}
	if in.Cpu != nil {
		out.Cpu = proto.Int64(int64(in.GetCpu()))
	}
	if in.MemoryGi != nil {
		out.MemoryGi = proto.Int64(int64(in.GetMemoryGi()))
	}
	for _, storage := range in.GetStorages() {
		item := &openapingv1beta2.ByocSharedComponentStorage{
			StorageName: storage.GetStorageName(),
		}
		if storage.SizeGi != nil {
			item.SizeGi = proto.Int64(int64(storage.GetSizeGi()))
		}
		out.Storages = append(out.Storages, item)
	}
	return out
}

func convertScaleTargetToGlobal(
	in *openapingv1beta2.ByocSharedComponentScaleTarget,
) (*globalngv1.ByocSharedComponentScaleTarget, error) {
	out := &globalngv1.ByocSharedComponentScaleTarget{
		Storages: make([]*globalngv1.ByocSharedComponentStorage, 0, len(in.GetStorages())),
	}
	seenStorageNames := make(map[string]struct{}, len(in.GetStorages()))
	if in.NodeCount != nil {
		value, err := toUint32Field("node_count", in.GetNodeCount())
		if err != nil {
			return nil, err
		}
		out.NodeCount = proto.Uint32(value)
	}
	if in.Cpu != nil {
		value, err := toUint32Field("cpu", in.GetCpu())
		if err != nil {
			return nil, err
		}
		out.Cpu = proto.Uint32(value)
	}
	if in.MemoryGi != nil {
		value, err := toUint32Field("memory_gi", in.GetMemoryGi())
		if err != nil {
			return nil, err
		}
		out.MemoryGi = proto.Uint32(value)
	}
	for _, storage := range in.GetStorages() {
		name := strings.TrimSpace(storage.GetStorageName())
		if name == "" {
			return nil, bizerr.New(bizerr.ErrCodeParams, "storage_name is required")
		}
		key := strings.ToUpper(name)
		if _, ok := seenStorageNames[key]; ok {
			return nil, bizerr.New(bizerr.ErrCodeParams, "duplicate storage_name %s", name)
		}
		seenStorageNames[key] = struct{}{}
		item := &globalngv1.ByocSharedComponentStorage{
			StorageName: name,
		}
		if storage.SizeGi != nil {
			value, err := toUint32Field("size_gi", storage.GetSizeGi())
			if err != nil {
				return nil, err
			}
			item.SizeGi = proto.Uint32(value)
		}
		out.Storages = append(out.Storages, item)
	}
	return out, nil
}

func validateScaleByocSharedComponentsRequest(req *openapingv1beta2.ScaleByocSharedComponentsRequest) error {
	requestedTargets := 0
	for _, component := range []struct {
		name   string
		target *openapingv1beta2.ByocSharedComponentScaleTarget
	}{
		{name: "tikv", target: req.GetTikv()},
		{name: "tiflash_write", target: req.GetTiflashWrite()},
		{name: "tiflash_compute", target: req.GetTiflashCompute()},
	} {
		target := component.target
		if target == nil {
			continue
		}
		requestedTargets++
		if !hasAnyScaleTargetField(target) {
			return bizerr.New(bizerr.ErrCodeParams, "%s target must specify at least one field", component.name)
		}
	}
	if requestedTargets == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "at least one shared component target is required")
	}
	return nil
}

func hasAnyScaleTargetField(target *openapingv1beta2.ByocSharedComponentScaleTarget) bool {
	if target == nil {
		return false
	}
	return target.NodeCount != nil ||
		target.Cpu != nil ||
		target.MemoryGi != nil ||
		len(target.GetStorages()) > 0
}

func toUint32Field(name string, value int64) (uint32, error) {
	if value < 0 || value > math.MaxUint32 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "%s out of range", name)
	}
	return uint32(value), nil
}

func convertConstraintReasonsToOpenAPI(
	in []*globalngv1.ByocSharedComponentScaleConstraintReason,
) []*openapingv1beta2.ByocSharedComponentScaleConstraintReason {
	out := make([]*openapingv1beta2.ByocSharedComponentScaleConstraintReason, 0, len(in))
	for _, item := range in {
		out = append(out, &openapingv1beta2.ByocSharedComponentScaleConstraintReason{
			Code:    item.GetCode(),
			Message: item.GetMessage(),
		})
	}
	return out
}

func convertNodeCountConstraintsToOpenAPI(
	in *globalngv1.ByocSharedComponentScaleNodeCountConstraints,
) *openapingv1beta2.ByocSharedComponentScaleNodeCountConstraints {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.ByocSharedComponentScaleNodeCountConstraints{
		Current:        int64(in.GetCurrent()),
		Min:            int64(in.GetMin()),
		Max:            int64(in.GetMax()),
		Rule:           convertNodeCountRuleToOpenAPI(in.GetRule()),
		AllowCrossZero: in.GetAllowCrossZero(),
	}
}

func convertNodeCountRuleToOpenAPI(
	in *globalngv1.ByocSharedComponentScaleNodeCountRule,
) *openapingv1beta2.ByocSharedComponentScaleNodeCountRule {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.ByocSharedComponentScaleNodeCountRule{
		Type:  in.GetType(),
		Value: int64(in.GetValue()),
	}
}

func convertNodeSpecConstraintsToOpenAPI(
	in *globalngv1.ByocSharedComponentScaleNodeSpecConstraints,
) *openapingv1beta2.ByocSharedComponentScaleNodeSpecConstraints {
	if in == nil {
		return nil
	}
	out := &openapingv1beta2.ByocSharedComponentScaleNodeSpecConstraints{
		Current: in.GetCurrent(),
	}
	for _, candidate := range in.GetCandidates() {
		out.Candidates = append(out.Candidates, &openapingv1beta2.ByocSharedComponentScaleNodeSpecCandidate{
			Name:               candidate.GetName(),
			Cpu:                int64(candidate.GetCpu()),
			MemoryGi:           int64(candidate.GetMemoryGi()),
			Available:          candidate.GetAvailable(),
			IsCurrent:          candidate.GetIsCurrent(),
			Reasons:            convertConstraintReasonsToOpenAPI(candidate.GetReasons()),
			RequiredMinSpec:    convertRequiredMinSpecToOpenAPI(candidate.GetRequiredMinSpec()),
			StorageConstraints: convertStorageConstraintsToOpenAPI(candidate.GetStorageConstraints()),
		})
	}
	return out
}

func convertRequiredMinSpecToOpenAPI(
	in *globalngv1.ByocSharedComponentScaleRequiredMinSpec,
) *openapingv1beta2.ByocSharedComponentScaleRequiredMinSpec {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.ByocSharedComponentScaleRequiredMinSpec{
		Name:     in.GetName(),
		Cpu:      int64(in.GetCpu()),
		MemoryGi: int64(in.GetMemoryGi()),
	}
}

func convertStorageConstraintsToOpenAPI(
	in []*globalngv1.ByocSharedComponentScaleStorageConstraint,
) []*openapingv1beta2.ByocSharedComponentScaleStorageConstraint {
	out := make([]*openapingv1beta2.ByocSharedComponentScaleStorageConstraint, 0, len(in))
	for _, item := range in {
		storage := &openapingv1beta2.ByocSharedComponentScaleStorageConstraint{
			StorageName:   item.GetStorageName(),
			Resizable:     item.GetResizable(),
			CurrentSizeGi: int64(item.GetCurrentSizeGi()),
		}
		if item.MaxSizeGi != nil {
			storage.MaxSizeGi = proto.Int64(int64(item.GetMaxSizeGi()))
		}
		out = append(out, storage)
	}
	return out
}

func convertComponentTypeToOpenAPI(
	in globalngv1.ByocSharedComponentType,
) openapingv1beta2.ByocSharedComponentType {
	switch in {
	case globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV:
		return openapingv1beta2.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV
	case globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE:
		return openapingv1beta2.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE
	case globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE:
		return openapingv1beta2.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE
	default:
		return openapingv1beta2.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_UNSPECIFIED
	}
}
