package shadowpool

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/samber/lo"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s *serverImpl) createShadowPool(
	ctx context.Context,
	req *openapiserverlessv1beta1.CreateShadowPoolRequest,
) (*openapiserverlessv1beta1.ShadowPool, error) {
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	pool, err := buildEntityFromCreateRequest(req.GetShadowPool(), reqMeta)
	if err != nil {
		return nil, err
	}

	if req.GetValidateOnly() {
		return toOpenAPIShadowPool(pool, 0), nil
	}

	pool.ID = entity.GenClusterID()
	if err := s.poolRepo.Create(ctx, pool); err != nil {
		return nil, err
	}

	created, err := s.poolRepo.Get(ctx, pool.ID)
	if err != nil {
		return nil, err
	}

	return toOpenAPIShadowPool(created, 0), nil
}

func (s *serverImpl) getShadowPool(
	ctx context.Context,
	req *openapiserverlessv1beta1.GetShadowPoolRequest,
) (*openapiserverlessv1beta1.ShadowPool, error) {
	pool, err := s.getOwnedShadowPool(ctx, req.GetShadowPoolId())
	if err != nil {
		return nil, err
	}

	availablePreparedCount, err := s.clusterRepo.CountClusterByFilter(ctx, &repository.ClusterFilter{
		ShadowPoolIds: []uint64{pool.ID},
		ShadowStates:  []entity.ShadowState{entity.ShadowStatePrepared},
	})
	if err != nil {
		return nil, err
	}

	return toOpenAPIShadowPool(pool, uint32(availablePreparedCount)), nil
}

func (s *serverImpl) listShadowPools(
	ctx context.Context,
	req *openapiserverlessv1beta1.ListShadowPoolsRequest,
) (*openapiserverlessv1beta1.ListShadowPoolsResponse, error) {
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	filter := &repository.ShadowPoolFilter{OrgIDs: []uint64{reqMeta.OrgID}}
	if len(reqMeta.ValidProjectList) > 0 {
		filter.ProjectIDs = lo.Uniq(reqMeta.ValidProjectList)
	}
	pools, err := s.poolRepo.List(ctx, filter)
	if err != nil {
		return nil, err
	}

	offset, err := parsePageToken(req.GetPageToken())
	if err != nil {
		return nil, err
	}
	pageSize := normalizePageSize(req.GetPageSize())
	totalSize := len(pools)
	if offset >= totalSize {
		return &openapiserverlessv1beta1.ListShadowPoolsResponse{
			ShadowPools: []*openapiserverlessv1beta1.ShadowPool{},
			TotalSize:   uint32(totalSize),
		}, nil
	}

	end := min(offset+pageSize, totalSize)
	result := make([]*openapiserverlessv1beta1.ShadowPool, 0, end-offset)
	for _, pool := range pools[offset:end] {
		result = append(result, toOpenAPIShadowPool(pool, 0))
	}

	nextPageToken := ""
	if end < totalSize {
		nextPageToken = strconv.Itoa(end)
	}

	return &openapiserverlessv1beta1.ListShadowPoolsResponse{
		ShadowPools:   result,
		NextPageToken: nextPageToken,
		TotalSize:     uint32(totalSize),
	}, nil
}

func (s *serverImpl) deleteShadowPool(
	ctx context.Context,
	req *openapiserverlessv1beta1.DeleteShadowPoolRequest,
) (*openapiserverlessv1beta1.DeleteShadowPoolResponse, error) {
	pool, err := s.getOwnedShadowPool(ctx, req.GetShadowPoolId())
	if err != nil {
		return nil, err
	}

	if err := s.poolRepo.Delete(ctx, pool.ID); err != nil {
		return nil, err
	}

	return &openapiserverlessv1beta1.DeleteShadowPoolResponse{}, nil
}

func (s *serverImpl) updateShadowPool(
	ctx context.Context,
	req *openapiserverlessv1beta1.UpdateShadowPoolRequest,
) (*openapiserverlessv1beta1.ShadowPool, error) {
	update := req.GetShadowPool()
	if update == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool is required")
	}

	pool, err := s.getOwnedShadowPool(ctx, update.GetShadowPoolId())
	if err != nil {
		return nil, err
	}

	if req.GetUpdateMask() == nil || len(req.GetUpdateMask().GetPaths()) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "update_mask is required")
	}

	for _, rawPath := range req.GetUpdateMask().GetPaths() {
		path := strings.TrimPrefix(rawPath, "shadow_pool.")
		switch path {
		case "name":
			if update.GetName() == "" {
				return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.name is required")
			}
			if !common.CheckDisplayNameFmt(update.GetName()) {
				return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.name not allowed: %s", update.GetName())
			}
			pool.Name = update.GetName()
		case "min_capacity":
			if update.GetMinCapacity() < 0 {
				return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.min_capacity must be greater than or equal to 0")
			}
			pool.MinCapacity = int(update.GetMinCapacity())
		case "enabled":
			pool.Enabled = update.GetEnabled()
		default:
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"unsupported update path %q, only name,min_capacity,enabled are allowed",
				rawPath,
			)
		}
	}

	if err := s.poolRepo.Update(ctx, pool); err != nil {
		return nil, err
	}

	latest, err := s.poolRepo.Get(ctx, pool.ID)
	if err != nil {
		return nil, err
	}

	return toOpenAPIShadowPool(latest, 0), nil
}

func (s *serverImpl) replaceShadowPoolInitSQLs(
	ctx context.Context,
	req *openapiserverlessv1beta1.ReplaceShadowPoolInitSqlsRequest,
) (*openapiserverlessv1beta1.ShadowPool, error) {
	pool, err := s.getOwnedShadowPool(ctx, req.GetShadowPoolId())
	if err != nil {
		return nil, err
	}

	pool.InitSQLs = req.GetInitSqls()
	if req.GetValidateOnly() {
		return toOpenAPIShadowPool(pool, 0), nil
	}

	if err := s.poolRepo.Update(ctx, pool); err != nil {
		return nil, err
	}

	latest, err := s.poolRepo.Get(ctx, pool.ID)
	if err != nil {
		return nil, err
	}

	return toOpenAPIShadowPool(latest, 0), nil
}

func (s *serverImpl) replaceShadowPoolInstanceDemand(
	ctx context.Context,
	req *openapiserverlessv1beta1.ReplaceShadowPoolInstanceDemandRequest,
) (*openapiserverlessv1beta1.ShadowPool, error) {
	if req.GetInstanceDemand() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "instance_demand is required")
	}

	pool, err := s.getOwnedShadowPool(ctx, req.GetShadowPoolId())
	if err != nil {
		return nil, err
	}

	pool.InstanceDemand.MonthlySpendingLimit = req.GetInstanceDemand().GetMonthlySpendingLimit()

	if err := s.poolRepo.Update(ctx, pool); err != nil {
		return nil, err
	}

	latest, err := s.poolRepo.Get(ctx, pool.ID)
	if err != nil {
		return nil, err
	}

	return toOpenAPIShadowPool(latest, 0), nil
}

func (s *serverImpl) getOwnedShadowPool(ctx context.Context, shadowPoolID string) (*entity.ShadowPool, error) {
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	id, err := parseShadowPoolID(shadowPoolID)
	if err != nil {
		return nil, err
	}

	pool, err := s.poolRepo.Get(ctx, id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "shadow pool not found")
		}
		return nil, err
	}

	if pool.OrganizationID != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
	}
	if err := reqMeta.CheckProjectIDsPermission(pool.ProjectID); err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
	}

	return pool, nil
}

func buildEntityFromCreateRequest(
	pool *openapiserverlessv1beta1.ShadowPool,
	reqMeta *metadata.RequestMeta,
) (*entity.ShadowPool, error) {
	if pool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool is required")
	}

	if pool.GetName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.name is required")
	}
	if !common.CheckDisplayNameFmt(pool.GetName()) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.name not allowed: %s", pool.GetName())
	}

	orgID, err := strconv.ParseUint(pool.GetOrganizationId(), 10, 64)
	if err != nil || orgID == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid shadow_pool.organization_id: %s", pool.GetOrganizationId())
	}
	if orgID != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
	}

	projectID, err := strconv.ParseUint(pool.GetProjectId(), 10, 64)
	if err != nil || projectID == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid shadow_pool.project_id: %s", pool.GetProjectId())
	}
	if err := reqMeta.CheckProjectIDsPermission(projectID); err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
	}

	provider, region, err := common.ParseOpenApiRegionNameOrId(pool.GetRegion())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid shadow_pool.region: %s", pool.GetRegion())
	}

	if pool.GetCloudProvider() == openapicommonv1beta1.Region_CLOUD_PROVIDER_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.cloud_provider is required")
	}
	if !strings.EqualFold(provider, pool.GetCloudProvider().String()) {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"shadow_pool.cloud_provider (%s) does not match shadow_pool.region (%s)",
			pool.GetCloudProvider().String(),
			pool.GetRegion(),
		)
	}

	if pool.GetServicePlan() != openapicommonv1beta1.ServicePlan_Starter {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"shadow pool only supports service_plan Starter, got %s",
			pool.GetServicePlan().String(),
		)
	}

	if pool.GetMinCapacity() < 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.min_capacity must be greater than or equal to 0")
	}

	monthlySpendingLimit := uint64(0)
	if pool.GetInstanceDemand() != nil {
		monthlySpendingLimit = pool.GetInstanceDemand().GetMonthlySpendingLimit()
	}

	return &entity.ShadowPool{
		Name:           pool.GetName(),
		OrganizationID: orgID,
		ProjectID:      projectID,
		Region:         region,
		CloudProvider:  strings.ToLower(provider),
		ServicePlan:    openapicommonv1beta1.ServicePlan_Starter.String(),
		Enabled:        pool.GetEnabled(),
		MinCapacity:    int(pool.GetMinCapacity()),
		AutoScalingConfig: entity.AutoScalingConfig{
			MaxBatchSize:      defaultMaxBatchSize,
			TimeToFreeInHours: defaultTimeToFreeInHours,
		},
		InstanceDemand: entity.InstanceDemand{
			MonthlySpendingLimit: monthlySpendingLimit,
		},
		InitSQLs: pool.GetInitSqls(),
	}, nil
}

func toOpenAPIShadowPool(pool *entity.ShadowPool, availableClustersCount uint32) *openapiserverlessv1beta1.ShadowPool {
	if pool == nil {
		return nil
	}

	openAPIRegion := fmt.Sprintf("regions/%s-%s", strings.ToLower(pool.CloudProvider), strings.ToLower(pool.Region))
	cloudProvider := openapicommonv1beta1.Region_CLOUD_PROVIDER_UNSPECIFIED
	if p, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](pool.CloudProvider); err == nil {
		cloudProvider = openapicommonv1beta1.Region_CloudProvider(p)
	}

	servicePlan := openapicommonv1beta1.ServicePlan_SERVICE_PLAN_UNSPECIFIED
	if p, err := enum.FromString[openapicommonv1beta1.ServicePlan](pool.ServicePlan); err == nil {
		servicePlan = openapicommonv1beta1.ServicePlan(p)
	}

	res := &openapiserverlessv1beta1.ShadowPool{
		Name:                   pool.Name,
		OrganizationId:         strconv.FormatUint(pool.OrganizationID, 10),
		ProjectId:              strconv.FormatUint(pool.ProjectID, 10),
		Region:                 openAPIRegion,
		CloudProvider:          cloudProvider,
		ServicePlan:            servicePlan,
		Enabled:                pool.Enabled,
		MinCapacity:            int32(pool.MinCapacity),
		AvailableClustersCount: availableClustersCount,
		InstanceDemand: &openapiserverlessv1beta1.ShadowPool_InstanceDemand{
			MonthlySpendingLimit: pool.InstanceDemand.MonthlySpendingLimit,
		},
		InitSqls: pool.InitSQLs,
	}
	if pool.ID != 0 {
		res.ShadowPoolId = strconv.FormatUint(pool.ID, 10)
	}
	if !pool.CreatedAt.IsZero() {
		res.CreateTime = timestamppb.New(pool.CreatedAt)
	}
	if !pool.UpdatedAt.IsZero() {
		res.UpdateTime = timestamppb.New(pool.UpdatedAt)
	}

	return res
}

func parseShadowPoolID(shadowPoolID string) (uint64, error) {
	if shadowPoolID == "" {
		return 0, bizerr.New(bizerr.ErrCodeParams, "shadow_pool_id is required")
	}
	id, err := strconv.ParseUint(shadowPoolID, 10, 64)
	if err != nil || id == 0 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid shadow_pool_id: %s", shadowPoolID)
	}
	return id, nil
}

func parsePageToken(token string) (int, error) {
	if token == "" {
		return 0, nil
	}
	offset, err := strconv.Atoi(token)
	if err != nil || offset < 0 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid page_token")
	}
	return offset, nil
}

func normalizePageSize(pageSize int32) int {
	if pageSize <= 0 {
		return defaultListPageSize
	}
	if pageSize > maxListPageSize {
		return maxListPageSize
	}
	return int(pageSize)
}
