package region

import (
	"fmt"
	"strconv"

	"github.com/gin-gonic/gin"
	"google.golang.org/protobuf/encoding/protojson"

	"github.com/tidbcloud/devops/api"
	regionapi "github.com/tidbcloud/devops/api/region"
	opent "github.com/tidbcloud/devops/entity/operation"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	opsvcapi "github.com/tidbcloud/devops/pkg/service/operation"
	opsvcregionaction "github.com/tidbcloud/devops/pkg/service/operation/actions/regionaction"
	opsvcins "github.com/tidbcloud/devops/pkg/service/operation/instance"

	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

// ListRegions
//
//	@Id				RegionControllerListRegions
//	@Description	list regions
//	@Tags			RegionController
//	@Accept			json
//	@Produce		json
//	@Param			req	query		regionapi.ListRegionsRequest	false	"ListRegionsRequest"
//	@Success		200	{object}	regionapi.ListRegionsResponse
//	@Router			/api/v1/devops/region/regions/list [get]
func (c *RegionController) ListRegions(ctx *gin.Context) {
	req := &regionapi.ListRegionsRequest{}
	if err := ctx.ShouldBindQuery(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	reqPB := &mgmtv1.ListRegionsRequest{
		Page:     1,
		PageSize: 200, // enough
	}
	if req.DeployType != nil {
		reqPB.DeployType = mgmtv1.DeployType(mgmtv1.DeployType_value[*req.DeployType])
	}
	if req.ServicePlan != nil {
		reqPB.ServicePlan = commonv1.ServicePlan(commonv1.ServicePlan_value[*req.ServicePlan])
	}
	if req.TenantID != nil {
		tidInt, err := strconv.ParseUint(*req.TenantID, 10, 64)
		if err != nil {
			ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
			return
		}

		reqPB.OrganizationId = tidInt
	}

	respPB, err := c.coresvc.Mgmt.Region.ListRegions(ctx, reqPB)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &regionapi.ListRegionsResponse{
		Regions: container.TransformSlice(respPB.Regions, RegionConverter{}.RegionFromPB),
	}

	ctrl.Success(ctx, resp)
}

// GetRegion
//
//	@Id				RegionControllerGetRegion
//	@Description	get region
//	@Tags			RegionController
//	@Accept			json
//	@Produce		json
//	@Param			req	query		regionapi.GetRegionRequest	true	"GetRegionRequest"
//	@Success		200	{object}	regionapi.GetRegionResponse
//	@Router			/api/v1/devops/region/regions/get [get]
func (c *RegionController) GetRegion(ctx *gin.Context) {
	req := &regionapi.GetRegionRequest{}
	if err := ctx.ShouldBindQuery(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	reqPB := &mgmtv1.GetRegionRequest{
		DeployType:        mgmtv1.DeployType(mgmtv1.DeployType_value[req.DeployType]),
		CloudProviderId:   commonv1.CloudProvider_Name(commonv1.CloudProvider_Name_value[req.CloudProvider]),
		RegionLogicalName: req.LogicalName,
		ServicePlan:       commonv1.ServicePlan(commonv1.ServicePlan_value[req.ServicePlan]),
	}
	respPB, err := c.coresvc.Mgmt.Region.GetRegion(ctx, reqPB)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &regionapi.GetRegionResponse{
		Region: RegionConverter{}.RegionFromPB(respPB),
	}
	ctrl.Success(ctx, resp)
}

// RegisterRegion
//
//	@Id				RegionControllerRegisterRegion
//	@Description	register region
//	@Tags			RegionController
//	@Accept			json
//	@Produce		json
//	@Param			req	body		regionapi.RegisterRegionRequest	true	"RegisterRegionRequest"
//	@Success		200	{object}	regionapi.RegisterRegionResponse
//	@Router			/api/v1/devops/region/regions/register [post]
func (c *RegionController) RegisterRegion(ctx *gin.Context) {
	req := &regionapi.RegisterRegionRequest{}
	if err := ctx.ShouldBindJSON(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	if req.DeployType != mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS.String() {
		ctrl.FailWithCode(ctx, fmt.Errorf("only serverless region can be registered"), api.ErrCodeInvalidArgument)
		return
	}

	userEmail := ctx.GetString(consts.CtxUserEmail)
	if userEmail == "" {
		ctrl.FailWithCode(ctx, fmt.Errorf("account not found in ctx"), api.ErrCodeInvalidArgument)
		return
	}

	region := &mgmtv1.Region{
		Region:     &commonv1.Region{},
		DeployType: mgmtv1.DeployType(mgmtv1.DeployType_value[req.DeployType]),
	}

	region.Region.Id = commonv1.Region_Name(commonv1.Region_Name_value[req.RegionName])
	if region.Region.Id == commonv1.Region_NAME_UNSPECIFIED {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid region name: %s", req.RegionName), api.ErrCodeInvalidArgument)
		return
	}
	region.Region.CloudProviderId = commonv1.CloudProvider_Name(commonv1.CloudProvider_Name_value[req.CloudProvider])
	if region.Region.CloudProviderId == commonv1.CloudProvider_NAME_UNSPECIFIED {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid cloud provider: %s", req.CloudProvider), api.ErrCodeInvalidArgument)
		return
	}
	region.Region.DisplayName = req.DisplayName
	region.Region.State = commonv1.Region_State(commonv1.Region_State_value[req.RegionState])
	if region.Region.State == commonv1.Region_STATE_UNSPECIFIED {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid region state: %s", req.RegionState), api.ErrCodeInvalidArgument)
		return
	}
	region.Region.IsPublic = req.IsPublic
	region.Region.ServiceAddress = req.ServiceAddress
	region.Region.LogicalName = req.LogicalRegionName
	servicePlan := commonv1.ServicePlan(commonv1.ServicePlan_value[req.ServicePlan])
	if servicePlan == commonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid region service plan: %s", req.ServicePlan), api.ErrCodeInvalidArgument)
		return
	}
	region.Region.ServicePlan = servicePlan
	if req.DefaultBackupTime != nil {
		region.Region.DefaultBackupTime = *req.DefaultBackupTime
	}
	if req.OpenedTenantIDs != nil {
		for _, tenantIDStr := range *req.OpenedTenantIDs {
			tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
			if err != nil {
				ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
				return
			}
			region.Region.OpenedTenantIds = append(region.Region.OpenedTenantIds, tenantID)
		}
	}

	reqPB := &mgmtv1.RegisterRegionRequest{
		Region: region,
	}
	opReqData, err := protojson.Marshal(reqPB)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("marshal proto message failed: %s", err), api.ErrCodeInternal)
		return
	}

	createOPParam := opsvcapi.CreateInstanceParam{
		Type:      opent.OperationTargetRegion,
		Operation: "RegisterRegion", // NOTE: MUST match the name in operation definition.
		Reason:    req.Reason,
		Applicant: userEmail,
		Params: map[opent.ParamName]string{
			opent.ParamRegionDeployType:      req.DeployType,
			opent.ParamRegisterRegionRequest: string(opReqData),
		},
		Actions: []opsvcins.CreateInstanceActionParam{
			{
				Name:        opsvcregionaction.ActionRegisterRegion.Name,
				StartPolicy: opent.ActionStartPolicyManual,
			},
		},
	}

	insID, err := c.opSvc.CreateInstance(ctx, createOPParam)
	if err != nil {
		ctrl.Fail(ctx, fmt.Errorf("create operation instance failed: %w", err))
		return
	}

	resp := &regionapi.RegisterRegionResponse{
		InstanceID: *insID,
	}
	ctrl.Success(ctx, resp)
}

// UpdateRegion
//
//	@Id				RegionControllerUpdateRegion
//	@Description	update region
//	@Tags			RegionController
//	@Accept			json
//	@Produce		json
//	@Param			body	body		regionapi.UpdateRegionRequest	true	"UpdateRegionRequest"
//	@Success		200		{object}	regionapi.UpdateRegionResponse
//	@Router			/api/v1/devops/region/regions/update [post]
func (c *RegionController) UpdateRegion(ctx *gin.Context) {
	req := &regionapi.UpdateRegionRequest{}
	if err := ctx.ShouldBindJSON(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	if req.DeployType != mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS.String() && req.DeployType != mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN.String() {
		ctrl.FailWithCode(ctx, fmt.Errorf("only serverless and nextgen region can be updated"), api.ErrCodeInvalidArgument)
		return
	}

	userEmail := ctx.GetString(consts.CtxUserEmail)
	if userEmail == "" {
		ctrl.FailWithCode(ctx, fmt.Errorf("account not found in ctx"), api.ErrCodeInvalidArgument)
		return
	}

	getReqPB := &mgmtv1.GetRegionRequest{
		DeployType:        mgmtv1.DeployType(mgmtv1.DeployType_value[req.DeployType]),
		CloudProviderId:   commonv1.CloudProvider_Name(commonv1.CloudProvider_Name_value[req.CloudProvider]),
		RegionLogicalName: req.LogicalName,
		ServicePlan:       commonv1.ServicePlan(commonv1.ServicePlan_value[req.ServicePlan]),
	}
	curRegion, err := c.coresvc.Mgmt.Region.GetRegion(ctx, getReqPB)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	curRegion.Region.State = commonv1.Region_State(commonv1.Region_State_value[req.RegionState])
	if curRegion.Region.State == commonv1.Region_STATE_UNSPECIFIED {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid region state: %s", req.RegionState), api.ErrCodeInvalidArgument)
		return
	}
	curRegion.Region.IsPublic = req.IsPublic
	curRegion.Region.ServiceAddress = req.ServiceAddress
	openedTenantIDs := make([]uint64, 0, len(req.OpenedTenantIDs))
	for _, tenantIDStr := range req.OpenedTenantIDs {
		tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
		if err != nil {
			ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
			return
		}
		openedTenantIDs = append(openedTenantIDs, tenantID)
	}
	curRegion.Region.OpenedTenantIds = openedTenantIDs

	if len(req.RegionWhiteLists) > 0 {
		whiteLists := make([]*commonv1.RegionWhiteList, 0, len(req.RegionWhiteLists))
		for _, item := range req.RegionWhiteLists {
			if item == nil {
				continue
			}
			servicePlanValue, ok := commonv1.ServicePlan_value[item.ServicePlan]
			if !ok {
				ctrl.FailWithCode(ctx, fmt.Errorf("invalid service plan in region white list: %s", item.ServicePlan), api.ErrCodeInvalidArgument)
				return
			}

			itemOpenedTenantIDs := make([]uint64, 0, len(item.OpenedTenantIds))
			for _, tenantIDStr := range item.OpenedTenantIds {
				tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
				if err != nil {
					ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
					return
				}
				itemOpenedTenantIDs = append(itemOpenedTenantIDs, tenantID)
			}

			whiteLists = append(whiteLists, &commonv1.RegionWhiteList{
				ServicePlan:     commonv1.ServicePlan(servicePlanValue),
				IsPublic:        item.IsPublic,
				OpenedTenantIds: itemOpenedTenantIDs,
			})
		}
		curRegion.Region.RegionWhiteLists = whiteLists
	}

	if req.DefaultProviderIdentities != nil {
		defaultProviderIdentities := make([]*nextgenv1.DefaultRegionProviderIdentity, 0, len(*req.DefaultProviderIdentities))
		for _, item := range *req.DefaultProviderIdentities {
			if item == nil {
				continue
			}
			if item.Identity == "" {
				ctrl.FailWithCode(ctx, fmt.Errorf("default provider identity is required"), api.ErrCodeInvalidArgument)
				return
			}
			servicePlanValue, ok := commonv1.ServicePlan_value[item.ServicePlan]
			if !ok || commonv1.ServicePlan(servicePlanValue) == commonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
				ctrl.FailWithCode(ctx, fmt.Errorf("invalid service plan in default provider identity: %s", item.ServicePlan), api.ErrCodeInvalidArgument)
				return
			}

			tenantID, err := strconv.ParseUint(item.TenantID, 10, 64)
			if err != nil {
				ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
				return
			}

			defaultProviderIdentities = append(defaultProviderIdentities, &nextgenv1.DefaultRegionProviderIdentity{
				TenantId:    tenantID,
				Identity:    item.Identity,
				ServicePlan: commonv1.ServicePlan(servicePlanValue),
			})
		}
		curRegion.DefaultProviderIdentities = defaultProviderIdentities
	}

	updateReqPB := &mgmtv1.UpdateRegionRequest{
		Region: curRegion,
	}
	opReqData, err := protojson.Marshal(updateReqPB)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("marshal proto message failed: %s", err), api.ErrCodeInternal)
		return
	}

	createOPParam := opsvcapi.CreateInstanceParam{
		Type:      opent.OperationTargetRegion,
		Operation: "UpdateRegion", // NOTE: MUST match the name in operation definition.
		Reason:    req.Reason,
		Applicant: userEmail,
		Params: map[opent.ParamName]string{
			opent.ParamRegionDeployType:           req.DeployType,
			opent.ParamUpdateRegionRequest:        string(opReqData),
			opent.ParamUpdateRegionRequestChanged: req.Changed,
		},
		Actions: []opsvcins.CreateInstanceActionParam{
			{
				Name:        opsvcregionaction.ActionUpdateRegion.Name,
				StartPolicy: opent.ActionStartPolicyManual,
			},
		},
	}

	insID, err := c.opSvc.CreateInstance(ctx, createOPParam)
	if err != nil {
		ctrl.Fail(ctx, fmt.Errorf("create operation instance failed: %w", err))
		return
	}

	resp := &regionapi.UpdateRegionResponse{
		InstanceID: *insID,
	}
	ctrl.Success(ctx, resp)
}
