package tidbxinstance

import (
	"context"
	"strconv"
	"strings"
	"time"

	"github.com/samber/lo"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"

	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"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

// serverlessMoveServicePlans are the service plans handled by the serverless cluster service.
var serverlessMoveServicePlans = map[string]bool{
	openapicommonv1beta1.ServicePlan_Starter.String():   true,
	openapicommonv1beta1.ServicePlan_Essential.String(): true,
}

// nextgenMoveServicePlans are the service plans handled by the nextgen cluster service.
var nextgenMoveServicePlans = map[string]bool{
	openapicommonv1beta1.ServicePlan_Premium.String():      true,
	openapicommonv1beta1.ServicePlan_BYOC.String():         true,
	openapicommonv1beta1.ServicePlan_Essential_V2.String(): true,
}

// MoveTiDB moves a single Starter, Essential, Premium, BYOC or Essential_V2 cluster to a different project.
// Routing between the serverless and nextgen service is performed here based on the cluster's
// deploy type; the domain-layer services each handle only their own cluster type.
// Requires Org Owner permission (enforced by the RBAC middleware).
func (s *TiDBServer) MoveTiDB(
	ctx context.Context,
	req *bffv1beta2.MoveTiDBRequest,
) (*bffv1beta2.MoveTiDBResponse, error) {
	var (
		err       error
		reqMeta   *metadata.RequestMeta
		clusterID uint64
		tidb      *bffv1beta2.Tidb
	)
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteAPIMetrics(apiMoveTidb, tenantID, time.Since(startTime).Seconds(), err)
	}()

	detailsFunc := func() auditlog.Details {
		tierType := eventbus.TierTypeServerless
		if tidb != nil {
			tierType = eventbus.TierTypeFromServicePlan(tidb.GetServicePlan())
		}
		details := eventbus.NewAuditLogDetails(tierType)
		if tidb != nil {
			details[auditlog.KeyServicePlan] = tidb.GetServicePlan()
		}
		details[eventbus.KeyTargetProjectID] = req.GetTargetProjectId()
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventMoveCluster, reqMeta, detailsFunc, err)
	}()

	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	if req.GetTidbId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tidb_id is required")
	}
	if req.GetTargetProjectId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target_project_id is required")
	}

	clusterID, err = strconv.ParseUint(req.GetTidbId(), 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid tidb_id")
	}
	reqMeta.ClusterID = clusterID
	targetProjectId, err := strconv.ParseUint(req.GetTargetProjectId(), 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid target_project_id")
	}

	targetProject, err := s.accountCli.GetProject(ctx, targetProjectId)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to get target project")
	}
	if targetProject.TenantId != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target project does not belong to the current organization")
	}

	// Fetch the cluster to determine its deploy type for routing.
	cluster, err := s.clusterRepo.GetCluster(ctx, clusterID, false)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to get cluster")
	}

	// Route to the correct domain service based on the cluster's deploy type.
	switch cluster.DeployType {
	case entity.DeployTypeServerless:
		if targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL &&
			targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project %d is not a valid TiDB X project type", targetProjectId)
		}
		_, err = s.serverlessClusterSvc.MoveCluster(ctx, reqMeta.OrgID, clusterID, targetProjectId, req.GetName())
	case entity.DeployTypeNextGen:
		if targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL &&
			targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project %d is not a valid TiDB X project type", targetProjectId)
		}
		_, err = s.nextgenClusterSvc.MoveCluster(ctx, reqMeta.OrgID, clusterID, targetProjectId, req.GetName())
	default:
		return nil, bizerr.New(bizerr.ErrCodeParams,
			"cluster migration is not supported for deploy type %s; only Serverless and NextGen clusters can be moved",
			cluster.DeployType)
	}
	if err != nil {
		return nil, err
	}

	tidb, err = s.GetTidb(ctx, &bffv1beta2.GetTidbRequest{TidbId: req.GetTidbId()}) // warm up cache for the moved cluster

	if err != nil {
		tilog.Error(ctx, "failed to get tidb after move",
			zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, err
	}
	return &bffv1beta2.MoveTiDBResponse{
		Tidb: tidb,
	}, nil
}

// BatchMoveTiDBs moves multiple Starter/Essential/Premium/BYOC/Essential_V2 clusters to a target project.
// Service plans are separated by deploy type; each domain service handles only its own cluster type.
// Requires Org Owner permission (enforced by the RBAC middleware).
// Max 1000 clusters per call (applied per service).
func (s *TiDBServer) BatchMoveTiDBs(
	ctx context.Context,
	req *bffv1beta2.BatchMoveTiDBsRequest,
) (*bffv1beta2.BatchMoveTiDBsResponse, error) {
	var (
		err        error
		reqMeta    *metadata.RequestMeta
		total      int32
		successIds []string
		failedIds  []string
	)
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteAPIMetrics(apiBatchMoveTidbs, tenantID, time.Since(startTime).Seconds(), err)
	}()

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		details[eventbus.KeySourceProjectID] = req.GetSourceProjectId()
		details[eventbus.KeyTargetProjectID] = req.GetTargetProjectId()
		servicePlans := lo.Map(req.GetServicePlans(), func(p openapicommonv1beta1.ServicePlan, _ int) string {
			return p.String()
		})
		details[eventbus.KeyServicePlans] = strings.Join(servicePlans, ",")
		details[eventbus.KeyTotal] = total
		details[eventbus.KeySuccess] = int64(len(successIds))
		details[eventbus.KeyFailed] = int64(len(failedIds))
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventBatchMoveCluster, reqMeta, detailsFunc, err)
	}()

	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	if req.GetSourceProjectId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "source_project_id is required")
	}
	if req.GetTargetProjectId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target_project_id is required")
	}

	sourceProjectId, err := strconv.ParseUint(req.GetSourceProjectId(), 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid source_project_id")
	}
	targetProjectId, err := strconv.ParseUint(req.GetTargetProjectId(), 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid target_project_id")
	}

	sourceProject, err := s.accountCli.GetProject(ctx, sourceProjectId)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to get source project")
	}
	if sourceProject.TenantId != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodeParams, "source project does not belong to the current organization")
	}

	targetProject, err := s.accountCli.GetProject(ctx, targetProjectId)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to get target project")
	}
	if targetProject.TenantId != reqMeta.OrgID {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target project does not belong to the current organization")
	}

	// Separate the requested service plans by deploy type.
	// An empty filter means "move all supported plans" for each deploy type.
	var slsPlans, ngPlans []string
	for _, p := range req.GetServicePlans() {
		planStr := p.String()
		switch {
		case serverlessMoveServicePlans[planStr]:
			slsPlans = append(slsPlans, planStr)
		case nextgenMoveServicePlans[planStr]:
			ngPlans = append(ngPlans, planStr)
		}
	}
	callServerless := len(req.GetServicePlans()) == 0 || len(slsPlans) > 0
	callNextgen := len(req.GetServicePlans()) == 0 || len(ngPlans) > 0

	if callServerless {
		if targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL &&
			targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project %d is not a valid TiDB X project type", targetProjectId)
		}

		var slsTotal int32
		var slsSuccess, slsFailed []string
		slsTotal, slsSuccess, slsFailed, err = s.serverlessClusterSvc.BatchMoveClusters(
			ctx, reqMeta.OrgID, sourceProjectId, targetProjectId, slsPlans,
		)
		if err != nil {
			return nil, err
		}
		total += slsTotal
		successIds = append(successIds, slsSuccess...)
		failedIds = append(failedIds, slsFailed...)
	}

	if callNextgen {
		if targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL &&
			targetProject.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project %d is not a valid TiDB X project type", targetProjectId)
		}

		var ngTotal int32
		var ngSuccess, ngFailed []string
		ngTotal, ngSuccess, ngFailed, err = s.nextgenClusterSvc.BatchMoveClusters(
			ctx, reqMeta.OrgID, sourceProjectId, targetProjectId, ngPlans,
		)
		if err != nil {
			return nil, err
		}
		total += ngTotal
		successIds = append(successIds, ngSuccess...)
		failedIds = append(failedIds, ngFailed...)
	}

	return &bffv1beta2.BatchMoveTiDBsResponse{
		Total:          total,
		SuccessTidbIds: successIds,
		FailedTidbIds:  failedIds,
	}, nil
}
