package v1beta

import (
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/samber/lo"
	openapilegacy "github.com/tidbcloud/idl/pbgen/proto/openapi/cm"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/types"
	apiutils "github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/utils"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/auth"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc"
	globaldedicatedv1grpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/dedicated"
	mgmtgrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/mgmt"
	openapigrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/openapi"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func CreateCluster(ctx *gin.Context) {
	req := &openapilegacy.CreateClusterReq{}
	err := types.ShouldBindJSON(ctx, req)
	if err != nil {
		types.OpenAPIInvalidArgumentResp(ctx, fmt.Errorf("bind request body failed: %w", err))
		return
	}

	orgId, err := auth.GetOrgIDByHeader(ctx)
	if err != nil {
		types.OpenAPIInvalidArgumentResp(ctx, err)
		return
	}

	projectId, err := strconv.ParseUint(ctx.Param("project_id"), 10, 64)
	if err != nil {
		types.OpenAPIInvalidArgumentResp(ctx, fmt.Errorf("invalid project id: %w", err))
		return
	}

	req.ProjectId = projectId

	switch req.ClusterType {
	case openapilegacy.ClusterType_DEVELOPER:
		allow, err := apiutils.IsServerlessAllow(ctx, orgId)
		if err != nil {
			types.OpenAPIInternalErrorResp(ctx, err)
			return
		}
		if allow {
			// tidb management service
			c, err := mgmtgrpc.CreateServerlessCluster(
				grpc.NewContext(ctx),
				&openapiserverlessv1beta1.CreateClusterRequest{
					Cluster: convertCreateClusterReqToServerlessCluster(req),
				},
			)
			if err != nil {
				types.OpenAPIErrorResp(ctx, err)
				return
			}
			clusterID, err := strconv.ParseUint(c.ClusterId, 10, 64)
			if err != nil {
				types.OpenAPIInternalErrorResp(ctx, fmt.Errorf("parse cluster id failed: %w", err))
				return
			}

			types.SuccessResp(ctx, &openapilegacy.CreateClusterResp{Id: clusterID})
			return
		}
	case openapilegacy.ClusterType_DEDICATED:
		// new dedicated cluster
		allow, err := apiutils.IsDedicatedAllow(ctx, orgId, projectId)
		if err != nil {
			types.OpenAPIInternalErrorResp(ctx, err)
			return
		}
		if allow {
			// check req.Config
			if req.Config == nil || req.Config.Components == nil {
				types.OpenAPIInvalidArgumentResp(ctx, fmt.Errorf("invalid request body: components is required"))
				return
			}

			// check cidr
			for _, ip := range req.GetConfig().GetIpAccessList() {
				_, _, err := net.ParseCIDR(ip.Cidr)
				if err != nil {
					types.OpenAPIInvalidArgumentResp(ctx, fmt.Errorf("invalid cidr: %s", ip.Cidr))
					return
				}
			}

			createReq, berr := convertCreateClusterReqToDedicatedCluster(req)
			if berr != nil {
				types.OpenAPIWithBizErrorResp(ctx, berr)
				return
			}
			c, err := mgmtgrpc.CreateDedicatedCluster(
				grpc.NewContext(ctx),
				&openapidedicatedv1beta1.CreateClusterRequest{
					Cluster: createReq,
				},
			)
			if err != nil {
				types.OpenAPIErrorResp(ctx, err)
				return
			}
			clusterID, err := strconv.ParseUint(c.ClusterId, 10, 64)
			if err != nil {
				types.OpenAPIInternalErrorResp(ctx, fmt.Errorf("parse cluster id failed: %w", err))
				return
			}

			// check ip access list
			if len(req.GetConfig().GetIpAccessList()) > 0 {
				// wait for cluster region
				var cluster *openapidedicatedv1beta1.Cluster
				for {
					time.Sleep(1 * time.Second)
					cluster, err = mgmtgrpc.GetDedicatedCluster(
						grpc.NewContext(ctx),
						&openapidedicatedv1beta1.GetClusterRequest{
							ClusterId: c.ClusterId,
						},
					)
					if err != nil {
						tilog.Warn(ctx, "get cluster failed", zap.Error(err))
						continue
					}
					// if cluster region is not equal to region id, means cluster is creating in global
					// not creating in global, region display name -> us-west-1
					// creating in global, region display name -> XXXX (us-west-1)
					// region id -> aws-us-west-1
					if strings.ToLower(
						fmt.Sprintf("%s-%s", cluster.GetCloudProvider(), cluster.GetRegionDisplayName()),
					) != cluster.GetRegionId() &&
						len(cluster.GetTidbNodeSetting().GetTidbNodeGroups()) > 0 {
						break
					}
				}

				// update ip access lis
				tidbGroupId, err := strconv.ParseUint(
					cluster.GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTidbNodeGroupId(),
					10,
					64,
				)
				if err != nil {
					types.OpenAPIInternalErrorResp(ctx, fmt.Errorf("parse tidb node group id failed: %w", err))
					return
				}
				_, err = globaldedicatedv1grpc.UpdateTidbNodeGroup(
					grpc.NewContext(ctx),
					&globaldedicatedv1.UpdateTidbNodeGroupRequest{
						TidbNodeGroup: &globaldedicatedv1.TidbNodeGroup{
							Id:        tidbGroupId,
							ClusterId: clusterID,
							Endpoint: &globaldedicatedv1.TidbNodeGroup_Endpoint{
								PublicEndpoint: &globaldedicatedv1.TidbNodeGroup_Endpoint_PublicEndpoint{
									EnablePublicLb: lo.ToPtr(true),
									IpAllowList: lo.Map(
										req.GetConfig().GetIpAccessList(),
										func(item *openapilegacy.IpAccessListItem, _ int) *globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress {
											return &globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress{
												CidrBlock:   item.Cidr,
												Description: item.Description,
											}
										},
									),
								},
							},
						},
						ValidateOnly: false,
					},
				)
				if err != nil {
					types.OpenAPIErrorResp(ctx, err)
					return
				}
			}
			types.SuccessResp(ctx, &openapilegacy.CreateClusterResp{Id: clusterID})
			return
		}
	case openapilegacy.ClusterType_CLUSTER_TYPE_UNSPECIFIED:
		types.OpenAPIInvalidArgumentResp(ctx, fmt.Errorf("invalid cluster type: %v", req.ClusterType))
		return
	}

	// old openAPI
	resp, err := openapigrpc.CreateCluster(grpc.NewContext(ctx), req)
	if err != nil {
		types.OpenAPIErrorResp(ctx, err)
		return
	}

	types.SuccessResp(ctx, resp)
}

func convertCreateClusterReqToServerlessCluster(req *openapilegacy.CreateClusterReq) *openapiserverlessv1beta1.Cluster {
	provider := openapicommonv1beta1.Region_CLOUD_PROVIDER_UNSPECIFIED
	switch req.CloudProvider {
	case openapilegacy.CloudProvider_AWS:
		provider = openapicommonv1beta1.Region_aws
	case openapilegacy.CloudProvider_GCP:
		provider = openapicommonv1beta1.Region_gcp
	}

	return &openapiserverlessv1beta1.Cluster{
		DisplayName: req.Name,
		Region: &openapicommonv1beta1.Region{
			CloudProvider: provider,
			Name:          apiutils.ConvertRegionToMgmt(req.Region, provider.String()),
		},
		Labels: map[string]string{
			apiutils.ProjectIDLabelKey: strconv.FormatUint(req.ProjectId, 10),
		},
		Endpoints: &openapiserverlessv1beta1.Cluster_Endpoints{
			Public: &openapiserverlessv1beta1.Cluster_Endpoints_Public{
				Port: req.Config.Port,
			},
		},

		RootPassword: req.GetConfig().GetRootPassword(),
	}
}

func convertCreateClusterReqToDedicatedCluster(
	req *openapilegacy.CreateClusterReq,
) (*openapidedicatedv1beta1.Cluster, bizerr.BizError) {
	tidbSetting := &openapidedicatedv1beta1.Cluster_TidbNodeSetting{
		TidbNodeGroups: make([]*openapidedicatedv1beta1.TidbNodeGroup, 0),
	}
	tikvSetting := &openapidedicatedv1beta1.Cluster_StorageNodeSetting{}
	var tiflashSetting *openapidedicatedv1beta1.Cluster_StorageNodeSetting

	if req.Config.Components.Tidb != nil {
		if req.Config.Components.Tidb.NodeQuantity <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeInvalidNodeQuantity, "tidb node_quantity is invalid")
		}

		if req.Config.Components.Tidb.NodeSize == "" {
			return nil, bizerr.New(bizerr.ErrCodeInvalidNodeSize, "tidb node_size is required")
		}

		tidbSetting.NodeSpecKey = req.Config.Components.Tidb.NodeSize
		tidbSetting.TidbNodeGroups = append(tidbSetting.TidbNodeGroups, &openapidedicatedv1beta1.TidbNodeGroup{
			NodeCount: req.Config.Components.Tidb.NodeQuantity,
		})
	}

	if req.Config.Components.Tikv != nil {
		if req.Config.Components.Tikv.NodeQuantity <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeInvalidNodeQuantity, "tikv node_quantity is invalid")
		}

		if req.Config.Components.Tikv.NodeSize == "" {
			return nil, bizerr.New(bizerr.ErrCodeInvalidNodeSize, "tikv node_size is required")
		}

		if req.Config.Components.Tikv.StorageSizeGib <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeInvalidStorageSize, "tikv storage_size_gib is invalid")
		}
		tikvSetting.NodeCount = req.Config.Components.Tikv.NodeQuantity
		tikvSetting.NodeSpecKey = req.Config.Components.Tikv.NodeSize
		tikvSetting.StorageSizeGi = req.Config.Components.Tikv.StorageSizeGib
	}

	if req.Config.Components.Tiflash != nil {
		if req.Config.Components.Tiflash.NodeQuantity < 0 {
			return nil, bizerr.New(bizerr.ErrCodeInvalidNodeQuantity, "tiflash node_quantity is invalid")
		}

		if req.Config.Components.Tiflash.StorageSizeGib < 0 {
			return nil, bizerr.New(bizerr.ErrCodeInvalidStorageSize, "tiflash storage_size_gib is invalid")
		}

		tiflashSetting = &openapidedicatedv1beta1.Cluster_StorageNodeSetting{
			NodeCount:     req.Config.Components.Tiflash.NodeQuantity,
			NodeSpecKey:   req.Config.Components.Tiflash.NodeSize,
			StorageSizeGi: req.Config.Components.Tiflash.StorageSizeGib,
		}
	}

	return &openapidedicatedv1beta1.Cluster{
		DisplayName:        req.Name,
		RegionId:           strings.ToLower(fmt.Sprintf("%s-%s", req.CloudProvider, req.Region)),
		RootPassword:       req.Config.RootPassword,
		Port:               req.Config.Port,
		TidbNodeSetting:    tidbSetting,
		TikvNodeSetting:    tikvSetting,
		TiflashNodeSetting: tiflashSetting,
		Labels: map[string]string{
			apiutils.ProjectIDLabelKey: strconv.FormatUint(req.ProjectId, 10),
		},
	}, nil
}
