package resourcepool

import (
	"context"
	"errors"

	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"

	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"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/openapi/nextgen/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/nextgenglobal"
	"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/grpc/metadata"
)

func (s *serverImpl) CreateResourcePool(
	ctx context.Context,
	req *openapingv1beta2.CreateResourcePoolRequest,
) (result *openapingv1beta2.ResourcePool, err error) {
	ctx, span := tracer.Start(ctx, "CreateResourcePool")
	defer span.End()

	var (
		reqMeta     *metadata.RequestMeta
		createdBy   string
		servicePlan globalcommonv1.ServicePlan
	)
	detailsFunc := func() auditlog.Details {
		pool := result
		if pool == nil {
			pool = req.GetResourcePool()
		}
		details := newResourcePoolAuditDetails("", pool, servicePlan)
		if createdBy != "" {
			details[auditlog.KeyCreatedBy] = createdBy
		}
		if keys := sortedResourcePoolTagKeys(req.GetResourcePool().GetCloudResourceTags()); len(keys) > 0 {
			details[auditlog.KeyChangedTagKeys] = keys
			details[auditlog.KeyNewTagCount] = len(keys)
		}
		return details
	}
	defer func() {
		if !req.GetValidateOnly() {
			s.auditLogSender.SubmitAuditLogForNextgen(
				ctx,
				auditlog.EventCreateResourcePool,
				reqMeta,
				detailsFunc,
				err,
			)
		}
	}()

	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	createdBy = common.BuildCreatedBy(reqMeta.Email, reqMeta.ApiPublicKey)
	globalReq, err := createRequestToGlobal(
		req,
		reqMeta.OrgID,
		createdBy,
	)
	if err != nil {
		return nil, err
	}
	servicePlan = globalReq.GetResourcePool().GetServicePlan()
	response, err := s.nextgenCli.CreateResourcePool(ctx, globalReq)
	if err != nil {
		var cidrStatus *nextgenglobal.CIDRValidationStatusError
		if errors.As(err, &cidrStatus) {
			return nil, publicCIDRValidationStatus(cidrStatus)
		}
		return nil, err
	}
	if err := validateOrganizationPool(response, reqMeta.OrgID); err != nil {
		return nil, err
	}
	servicePlan = response.GetResourcePool().GetServicePlan()
	result, err = profileToOpenAPI(response)
	return result, err
}

func publicCIDRValidationStatus(validationErr *nextgenglobal.CIDRValidationStatusError) error {
	if validationErr == nil || validationErr.Detail() == nil || len(validationErr.Detail().GetBlockingFindings()) == 0 {
		return status.Error(codes.Internal, "resource pool CIDR validation detail is invalid")
	}
	findings, err := cidrValidationFindingsToOpenAPI(validationErr.Detail().GetBlockingFindings())
	if err != nil || len(findings) == 0 {
		return status.Error(codes.Internal, "resource pool CIDR validation detail is invalid")
	}
	withDetails, err := status.New(codes.InvalidArgument, "resource pool VPC CIDR is invalid").WithDetails(
		&errdetails.ErrorInfo{
			Reason: findings[0].GetCode().String(),
			Domain: "tidbcloud.com",
		},
		&errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{
			Field:       "resourcePool.vpcCidr",
			Description: "The resource pool VPC CIDR is invalid.",
		}}},
		&openapingv1beta2.ResourcePoolCidrValidationError{BlockingFindings: findings},
	)
	if err != nil {
		return status.Error(codes.Internal, "failed to encode resource pool CIDR validation detail")
	}
	return withDetails.Err()
}

func (s *serverImpl) ListResourcePools(
	ctx context.Context,
	req *openapingv1beta2.ListResourcePoolsRequest,
) (*openapingv1beta2.ListResourcePoolsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListResourcePools")
	defer span.End()

	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	globalReq, err := listRequestToGlobal(req, reqMeta.OrgID)
	if err != nil {
		return nil, err
	}
	response, err := s.nextgenCli.ListResourcePools(ctx, globalReq)
	if err != nil {
		return nil, err
	}
	resourcePools := make([]*openapingv1beta2.ResourcePoolSummary, 0, len(response.GetResourcePools()))
	for _, profile := range response.GetResourcePools() {
		if err := validateOrganizationPool(profile, reqMeta.OrgID); err != nil {
			return nil, err
		}
		summary, err := profileToSummary(profile)
		if err != nil {
			return nil, err
		}
		resourcePools = append(resourcePools, summary)
	}
	return &openapingv1beta2.ListResourcePoolsResponse{
		ResourcePools: resourcePools,
		TotalSize:     response.GetTotalSize(),
		NextPageToken: response.GetNextPageToken(),
	}, nil
}

func (s *serverImpl) GetResourcePool(
	ctx context.Context,
	req *openapingv1beta2.GetResourcePoolRequest,
) (*openapingv1beta2.ResourcePool, error) {
	ctx, span := tracer.Start(ctx, "GetResourcePool")
	defer span.End()

	reqMeta, resourcePoolID, err := requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	return s.getResourcePool(ctx, reqMeta.OrgID, resourcePoolID)
}

func (s *serverImpl) UpdateResourcePool(
	ctx context.Context,
	req *openapingv1beta2.UpdateResourcePoolRequest,
) (result *openapingv1beta2.ResourcePool, err error) {
	ctx, span := tracer.Start(ctx, "UpdateResourcePool")
	defer span.End()

	if req == nil || req.GetResourcePool() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource_pool is required")
	}
	var (
		reqMeta        *metadata.RequestMeta
		resourcePoolID uint64
		previous       *openapingv1beta2.ResourcePool
		servicePlan    globalcommonv1.ServicePlan
		updateName     bool
		updateVcpu     bool
	)
	detailsFunc := func() auditlog.Details {
		pool := result
		if pool == nil {
			pool = req.GetResourcePool()
		}
		details := newResourcePoolAuditDetails(req.GetResourcePool().GetResourcePoolId(), pool, servicePlan)
		switch {
		case updateName:
			details[auditlog.KeyAction] = resourcePoolUpdateDisplayNameAction
			details[auditlog.KeyUpdateMask] = "display_name"
			details[auditlog.KeyOldValue] = previous.GetDisplayName()
			details[auditlog.KeyNewValue] = req.GetResourcePool().GetDisplayName()
		case updateVcpu:
			details[auditlog.KeyAction] = resourcePoolUpdateVcpuLimitAction
			details[auditlog.KeyUpdateMask] = "vcpu_limit"
			details[auditlog.KeyOldValue] = resourcePoolVcpuLimitAuditValue(previous.GetVcpuLimit())
			details[auditlog.KeyNewValue] = resourcePoolVcpuLimitAuditValue(req.GetResourcePool().GetVcpuLimit())
		}
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLogForNextgen(
			ctx,
			auditlog.EventUpdateResourcePool,
			reqMeta,
			detailsFunc,
			err,
		)
	}()

	reqMeta, resourcePoolID, err = requestMetaAndResourcePoolID(ctx, req.GetResourcePool().GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	updateName, updateVcpu, err = resolveUpdatePaths(req)
	if err != nil {
		return nil, err
	}
	previous, servicePlan, _ = s.getResourcePoolWithServicePlan(ctx, reqMeta.OrgID, resourcePoolID)
	if updateName {
		_, err = s.nextgenCli.UpdateResourcePoolName(ctx, &globalngv1.UpdateResourcePoolNameRequest{
			ResourcePoolId: resourcePoolID,
			Name:           req.GetResourcePool().GetDisplayName(),
		})
		if err != nil {
			return nil, err
		}
	}
	if updateVcpu {
		limit, mode, err := vcpuLimitToGlobal(req.GetResourcePool().GetVcpuLimit())
		if err != nil {
			return nil, err
		}
		globalReq := &globalngv1.UpdateResourcePoolInstanceLimitRequest{
			ResourcePoolId: resourcePoolID,
			VcpuLimitMode:  mode,
		}
		if mode == globalngv1.ResourcePool_LIMITED {
			globalReq.VcpusLimit = &limit
		}
		_, err = s.nextgenCli.UpdateResourcePoolInstanceLimit(ctx, globalReq)
		if err != nil {
			return nil, err
		}
	}
	result, servicePlan, err = s.getResourcePoolWithServicePlan(ctx, reqMeta.OrgID, resourcePoolID)
	return result, err
}

func (s *serverImpl) DeleteResourcePool(
	ctx context.Context,
	req *openapingv1beta2.DeleteResourcePoolRequest,
) (result *emptypb.Empty, err error) {
	ctx, span := tracer.Start(ctx, "DeleteResourcePool")
	defer span.End()

	var (
		reqMeta        *metadata.RequestMeta
		resourcePoolID uint64
		previous       *openapingv1beta2.ResourcePool
		servicePlan    globalcommonv1.ServicePlan
	)
	detailsFunc := func() auditlog.Details {
		return newResourcePoolAuditDetails(req.GetResourcePoolId(), previous, servicePlan)
	}
	defer func() {
		s.auditLogSender.SubmitAuditLogForNextgen(
			ctx,
			auditlog.EventDeleteResourcePool,
			reqMeta,
			detailsFunc,
			err,
		)
	}()

	reqMeta, resourcePoolID, err = requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	previous, servicePlan, _ = s.getResourcePoolWithServicePlan(ctx, reqMeta.OrgID, resourcePoolID)
	if _, err = s.nextgenCli.DeleteResourcePool(ctx, &globalngv1.DeleteResourcePoolRequest{
		ResourcePoolId: resourcePoolID,
	}); err != nil {
		return nil, err
	}
	result = &emptypb.Empty{}
	return result, nil
}

func (s *serverImpl) ResumeResourcePool(
	ctx context.Context,
	req *openapingv1beta2.ResumeResourcePoolRequest,
) (result *openapingv1beta2.ResumeResourcePoolResponse, err error) {
	ctx, span := tracer.Start(ctx, "ResumeResourcePool")
	defer span.End()

	var reqMeta *metadata.RequestMeta
	detailsFunc := func() auditlog.Details {
		details := newResourcePoolAuditDetails(
			req.GetResourcePoolId(),
			nil,
			globalcommonv1.ServicePlan_BYOC,
		)
		details[auditlog.KeyAction] = resourcePoolResumeAction
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLogForNextgen(
			ctx,
			auditlog.EventUpdateResourcePool,
			reqMeta,
			detailsFunc,
			err,
		)
	}()

	var resourcePoolID uint64
	reqMeta, resourcePoolID, err = requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	err = appcommon.ResumeResourcePoolIdempotently(ctx, s.nextgenCli, resourcePoolID)
	if err != nil {
		return nil, err
	}
	result = &openapingv1beta2.ResumeResourcePoolResponse{}
	return result, nil
}

func (s *serverImpl) GetResourcePoolCapacityConstraints(
	ctx context.Context,
	req *openapingv1beta2.GetResourcePoolCapacityConstraintsRequest,
) (*openapingv1beta2.ResourcePoolCapacityConstraints, error) {
	ctx, span := tracer.Start(ctx, "GetResourcePoolCapacityConstraints")
	defer span.End()

	reqMeta, resourcePoolID, err := requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	response, err := s.nextgenCli.GetResourcePoolCapacityConstraints(
		ctx,
		&globalngv1.GetResourcePoolCapacityConstraintsRequest{
			ResourcePoolId: resourcePoolID,
			OrganizationId: reqMeta.OrgID,
		},
	)
	if err != nil {
		return nil, err
	}
	return constraintsToOpenAPI(response), nil
}

func (s *serverImpl) ResizeResourcePoolCapacity(
	ctx context.Context,
	req *openapingv1beta2.ResizeResourcePoolCapacityRequest,
) (result *openapingv1beta2.ResizeResourcePoolCapacityResponse, err error) {
	ctx, span := tracer.Start(ctx, "ResizeResourcePoolCapacity")
	defer span.End()

	var (
		reqMeta        *metadata.RequestMeta
		resourcePoolID uint64
		previous       *openapingv1beta2.ResourcePool
		servicePlan    globalcommonv1.ServicePlan
	)
	detailsFunc := func() auditlog.Details {
		details := newResourcePoolAuditDetails(req.GetResourcePoolId(), previous, servicePlan)
		details[auditlog.KeyOldValue] = resourcePoolCapacityAuditValue(previous.GetCapacity())
		details[auditlog.KeyNewValue] = resourcePoolCapacityAuditValue(req.GetTargetCapacity())
		if result != nil {
			details[auditlog.KeyOldValue] = resourcePoolCapacityAuditValue(result.GetCurrentCapacity())
			details[auditlog.KeyNewValue] = resourcePoolCapacityAuditValue(result.GetTargetCapacity())
		}
		return details
	}
	defer func() {
		if !req.GetValidateOnly() {
			s.auditLogSender.SubmitAuditLogForNextgen(
				ctx,
				auditlog.EventResizeResourcePoolCapacity,
				reqMeta,
				detailsFunc,
				err,
			)
		}
	}()

	reqMeta, resourcePoolID, err = requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	previous, servicePlan, _ = s.getResourcePoolWithServicePlan(ctx, reqMeta.OrgID, resourcePoolID)
	targetCapacity, err := targetCapacityToGlobal(req.GetTargetCapacity())
	if err != nil {
		return nil, err
	}
	response, err := s.nextgenCli.ResizeResourcePoolCapacity(
		ctx,
		&globalngv1.ResizeResourcePoolCapacityRequest{
			ResourcePoolId: resourcePoolID,
			OrganizationId: reqMeta.OrgID,
			TargetCapacity: targetCapacity,
			ValidateOnly:   req.GetValidateOnly(),
		},
	)
	if err != nil {
		return nil, err
	}
	result = &openapingv1beta2.ResizeResourcePoolCapacityResponse{
		CurrentCapacity: capacityToOpenAPI(response.GetCurrentCapacity()),
		TargetCapacity:  capacityToOpenAPI(response.GetTargetCapacity()),
	}
	return result, nil
}

func (s *serverImpl) UpsertResourcePoolCloudResourceTags(
	ctx context.Context,
	req *openapingv1beta2.UpsertResourcePoolCloudResourceTagsRequest,
) (result *openapingv1beta2.UpsertResourcePoolCloudResourceTagsResponse, err error) {
	ctx, span := tracer.Start(ctx, "UpsertResourcePoolCloudResourceTags")
	defer span.End()

	var (
		reqMeta        *metadata.RequestMeta
		resourcePoolID uint64
		previous       *openapingv1beta2.ResourcePool
		servicePlan    globalcommonv1.ServicePlan
	)
	detailsFunc := func() auditlog.Details {
		details := newResourcePoolAuditDetails(req.GetResourcePoolId(), previous, servicePlan)
		details[auditlog.KeyChangedTagKeys] = sortedResourcePoolTagKeys(req.GetCloudResourceTags())
		details[auditlog.KeyRemovedTagKeys] = sortedResourcePoolRemovedTagKeys(req.GetRemoveCloudResourceTagKeys())
		if previous != nil {
			details[auditlog.KeyOldTagCount] = len(previous.GetCloudResourceTags())
		}
		if result != nil {
			details[auditlog.KeyNewTagCount] = len(result.GetCloudResourceTags())
		}
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLogForNextgen(
			ctx,
			auditlog.EventUpdateResourcePoolCloudResourceTags,
			reqMeta,
			detailsFunc,
			err,
		)
	}()

	reqMeta, resourcePoolID, err = requestMetaAndResourcePoolID(ctx, req.GetResourcePoolId())
	if err != nil {
		return nil, err
	}
	previous, servicePlan, _ = s.getResourcePoolWithServicePlan(ctx, reqMeta.OrgID, resourcePoolID)
	response, err := s.nextgenCli.UpsertResourcePoolCloudResourceTags(
		ctx,
		&globalngv1.UpsertResourcePoolCloudResourceTagsRequest{
			ResourcePoolId:             resourcePoolID,
			OrganizationId:             reqMeta.OrgID,
			CloudResourceTags:          cloneStringMap(req.GetCloudResourceTags()),
			RemoveCloudResourceTagKeys: cloneStringSlice(req.GetRemoveCloudResourceTagKeys()),
		},
	)
	if err != nil {
		return nil, err
	}
	result = &openapingv1beta2.UpsertResourcePoolCloudResourceTagsResponse{
		CloudResourceTags: cloneStringMap(response.GetCloudResourceTags()),
	}
	return result, nil
}

func (s *serverImpl) getResourcePool(
	ctx context.Context,
	organizationID uint64,
	resourcePoolID uint64,
) (*openapingv1beta2.ResourcePool, error) {
	pool, _, err := s.getResourcePoolWithServicePlan(ctx, organizationID, resourcePoolID)
	return pool, err
}

func (s *serverImpl) getResourcePoolWithServicePlan(
	ctx context.Context,
	organizationID uint64,
	resourcePoolID uint64,
) (*openapingv1beta2.ResourcePool, globalcommonv1.ServicePlan, error) {
	response, err := s.nextgenCli.GetResourcePool(ctx, &globalngv1.GetResourcePoolRequest{
		ResourcePoolId: resourcePoolID,
	})
	if err != nil {
		return nil, globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED, err
	}
	if err := validateOrganizationPool(response, organizationID); err != nil {
		return nil, globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED, err
	}
	pool, err := profileToOpenAPI(response)
	return pool, response.GetResourcePool().GetServicePlan(), err
}

func requestMetaAndResourcePoolID(
	ctx context.Context,
	resourcePoolID 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")
	}
	parsed, err := common.ParseResourcePoolID(resourcePoolID)
	if err != nil {
		return nil, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid resource_pool_id")
	}
	return reqMeta, parsed, nil
}
