package tidbxinstance

import (
	"context"
	"errors"
	"strconv"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	bffv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta2"
	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"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	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/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s *TiDBServer) GetTidb(
	ctx context.Context,
	request *bffv1beta2.GetTidbRequest,
) (tidb *bffv1beta2.Tidb, err error) {
	startTime := time.Now()
	defer func() {
		recordQueryAPIMetrics(apiGetTidb, time.Since(startTime).Seconds(), err)
	}()

	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	if request.TidbId == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tidb id is required")
	}

	clusterId, err := strconv.ParseUint(request.TidbId, 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid tidb id")
	}

	// get the cluster from repo
	cls, err := s.clusterRepo.GetCluster(ctx, clusterId, false)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "tidb not found")
		}
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to get cluster")
	}

	if cls.OrganizationId != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "tidb not found in current organization")
	}

	// FIXME: use data permission filter instead of api permission for now
	// API permissions can only be used after the account service synchronizes the dedicated cluster data.
	all, projectIds, instanceIds, err := s.accountCli.GetInstancePermissionScopeOfUser(ctx, reqMeta.OrgID, reqMeta.UserID)
	if err != nil {
		tilog.Error(ctx,
			"failed to get instance permission scope of user",
			zap.Error(err),
			zap.String("user id", reqMeta.UserID),
			zap.Uint64("org id", reqMeta.OrgID))
		return nil, bizerr.New(bizerr.GeneralInternalErrorCode, "failed to check permission")
	}

	if !all && !lo.Contains(projectIds, cls.ProjectId) && !lo.Contains(instanceIds, cls.ClusterId) {
		return nil, bizerr.New(bizerr.ErrCodePermission, "no permission to get tidb instance")
	}

	switch cls.DeployType {
	case entity.DeployTypeServerless:
		// first try to get the cluster from serverless
		serverlessCluster, err := s.openAPIServerlessClusterServer.GetCluster(
			ctx,
			&openapiserverlessv1beta1.GetClusterRequest{
				ClusterId: request.GetTidbId(),
			},
		)
		if err != nil {
			return nil, bizerr.WrapGrpcError(err, "failed to get serverless cluster")
		}
		tidb, err = s.cvtTiDB(ctx, cls, serverlessCluster)
		return tidb, err
	case entity.DeployTypeDedicated:
		dedicatedCluster, err := s.openAPIDedicatedClusterServer.GetCluster(
			ctx,
			&openapidedicatedv1beta1.GetClusterRequest{
				ClusterId:      request.GetTidbId(),
				GlobalMetaOnly: request.GlobalMetaOnly,
			},
		)
		if err != nil {
			return nil, bizerr.WrapGrpcError(err, "failed to get dedicated cluster")
		}
		tidb, err = s.cvtTiDB(ctx, cls, dedicatedCluster)
		return tidb, err
	case entity.DeployTypeNextGen:
		nextgenCluster, err := s.openAPINextgenClusterServer.GetTidb(
			ctx,
			&openapingv1beta2.GetTidbRequest{
				TidbId: request.GetTidbId(),
			},
		)
		if err != nil {
			return nil, bizerr.WrapGrpcError(err, "failed to get dedicated cluster")
		}
		tidb, err = s.cvtTiDB(ctx, cls, nextgenCluster)
		return tidb, err
	}

	return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown cluster type")
}

func (s *TiDBServer) cvtTiDB(
	ctx context.Context,
	mgmtCluster *entity.Cluster,
	specifiedTiDB any) (*bffv1beta2.Tidb, error) {
	v1beta2Tidb := &bffv1beta2.Tidb{
		TidbId:            strconv.FormatUint(mgmtCluster.ClusterId, 10),
		ServicePlan:       mgmtCluster.ServicePlan,
		EngineType:        normalizeEngineType(mgmtCluster.EngineType),
		OrganizationId:    mgmtCluster.OrganizationId,
		ProjectId:         mgmtCluster.ProjectId,
		OriginalProjectId: mgmtCluster.OriginalProjectId,
		RuVersion:         mgmtCluster.RuVersion,
	}

	switch mgmtCluster.ServicePlan {
	case openapicommonv1beta1.ServicePlan_Dedicated:
		dedicateCluster, ok := specifiedTiDB.(*openapidedicatedv1beta1.Cluster)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown tidb type")
		}
		v1beta2Tidb.Name = dedicateCluster.Name
		if withTags, ok := any(dedicateCluster).(interface {
			GetTags() []*openapicommonv1beta1.Tag
		}); ok {
			v1beta2Tidb.Tags = withTags.GetTags()
		}
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_DedicatedCluster{
			DedicatedCluster: dedicateCluster,
		}
	case openapicommonv1beta1.ServicePlan_Premium:
		premiumInstance, ok := specifiedTiDB.(*openapingv1beta2.Tidb)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = premiumInstance.Name
		v1beta2Tidb.Tags = common.CvtEntityTagsToOpenAPITags(mustNormalizeTags(premiumInstance.GetTags()))
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_PremiumInstance{
			PremiumInstance: premiumInstance,
		}
	case openapicommonv1beta1.ServicePlan_Premium_Reserved:
		premiumReservedInstance, ok := specifiedTiDB.(*openapingv1beta2.Tidb)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = premiumReservedInstance.Name
		v1beta2Tidb.Tags = common.CvtEntityTagsToOpenAPITags(mustNormalizeTags(premiumReservedInstance.GetTags()))
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_PremiumReservedInstance{
			PremiumReservedInstance: premiumReservedInstance,
		}
	case openapicommonv1beta1.ServicePlan_Essential_V2:
		essentialV2Instance, ok := specifiedTiDB.(*openapingv1beta2.Tidb)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = essentialV2Instance.Name
		v1beta2Tidb.Tags = common.CvtEntityTagsToOpenAPITags(mustNormalizeTags(essentialV2Instance.GetTags()))
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_EssentialV2Instance{
			EssentialV2Instance: essentialV2Instance,
		}
	case openapicommonv1beta1.ServicePlan_BYOC:
		byocInstance, ok := specifiedTiDB.(*openapingv1beta2.Tidb)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = byocInstance.Name
		v1beta2Tidb.Tags = common.CvtEntityTagsToOpenAPITags(mustNormalizeTags(byocInstance.GetTags()))
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_ByocInstance{
			ByocInstance: byocInstance,
		}
	case openapicommonv1beta1.ServicePlan_Starter:
		startInstance, ok := specifiedTiDB.(*openapiserverlessv1beta1.Cluster)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = startInstance.Name
		if withTags, ok := any(startInstance).(interface {
			GetTags() []*openapicommonv1beta1.Tag
		}); ok {
			v1beta2Tidb.Tags = withTags.GetTags()
		}
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_StarterInstance{
			StarterInstance: startInstance,
		}
	case openapicommonv1beta1.ServicePlan_Essential:
		essentialInstance, ok := specifiedTiDB.(*openapiserverlessv1beta1.Cluster)
		if !ok {
			tilog.Error(ctx, "failed to convert tidb type", zap.Uint64("cluster_id", mgmtCluster.ClusterId))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmatched service plan")
		}
		v1beta2Tidb.Name = essentialInstance.Name
		if withTags, ok := any(essentialInstance).(interface {
			GetTags() []*openapicommonv1beta1.Tag
		}); ok {
			v1beta2Tidb.Tags = withTags.GetTags()
		}
		v1beta2Tidb.Tidb = &bffv1beta2.Tidb_EssentialInstance{
			EssentialInstance: essentialInstance,
		}
	default:
		tilog.Error(ctx, "unknown tidb type",
			zap.Uint64("cluster_id", mgmtCluster.ClusterId),
			zap.String("service_plan", mgmtCluster.ServicePlan.String()),
		)
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown service plan")
	}

	return v1beta2Tidb, nil
}

func mustNormalizeTags(canonicalTags []string) []entity.Tag {
	tags, err := entity.NormalizeCanonicalTags(canonicalTags)
	if err != nil {
		return nil
	}
	return tags
}
