package server

import (
	"context"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/clustergateway"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (s *serverImpl) ListPlacementPolicies(ctx context.Context, sharedPoolId, tikvGroupId string) ([]*entity.PlacementPolicy, error) {
	sharedPool, err := s.loadSharedPoolForPlacementPolicy(ctx, sharedPoolId)
	if err != nil {
		return nil, err
	}

	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get remote client for shared pool %s: %s", sharedPoolId, err)
	}
	return tioperator.ListPlacementPolicies(ctx, remoteCli, sharedPoolId, tikvGroupId)
}

func (s *serverImpl) GetPlacementPolicy(ctx context.Context, sharedPoolId, placementPolicyId string) (*entity.PlacementPolicy, error) {
	sharedPool, err := s.loadSharedPoolForPlacementPolicy(ctx, sharedPoolId)
	if err != nil {
		return nil, err
	}

	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get remote client for shared pool %s: %s", sharedPoolId, err)
	}
	return tioperator.GetPlacementPolicy(ctx, remoteCli, sharedPoolId, placementPolicyId)
}

func (s *serverImpl) CreatePlacementPolicy(ctx context.Context, sharedPoolId, clusterId, tikvGroupId string) (*entity.PlacementPolicy, error) {
	return s.writePlacementPolicy(ctx, sharedPoolId, clusterId, clusterId, tikvGroupId, false)
}

func (s *serverImpl) UpdatePlacementPolicy(ctx context.Context, sharedPoolId, placementPolicyId, tikvGroupId string) (*entity.PlacementPolicy, error) {
	return s.writePlacementPolicy(ctx, sharedPoolId, placementPolicyId, placementPolicyId, tikvGroupId, true)
}

func (s *serverImpl) writePlacementPolicy(ctx context.Context, sharedPoolId, placementPolicyId, clusterId, tikvGroupId string, allowUpdate bool) (*entity.PlacementPolicy, error) {
	unlockFunc, err := s.lock(ctx, sharedPoolId)
	if err != nil {
		tilog.Error(ctx, "failed to lock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		return nil, err
	}
	defer func() {
		if err := unlockFunc(); err != nil {
			tilog.Error(ctx, "failed to unlock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		}
	}()

	sharedPool, err := s.loadSharedPoolForPlacementPolicy(ctx, sharedPoolId)
	if err != nil {
		return nil, err
	}
	if err := guardSharedPoolMutation(sharedPool, sharedPoolMutationBlockedWhilePaused); err != nil {
		return nil, err
	}
	if err := s.mergeSharedPoolStatusFromDagTask(ctx, sharedPool); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to merge sharedPool status from dag task: %s", err)
	}
	if err := validatePlacementPolicyTiKVGroupWritable(sharedPool, tikvGroupId); err != nil {
		return nil, err
	}

	keyspace, err := s.clustergatewayCli.GetKeySpace(ctx, sharedPool, clusterId)
	if err != nil {
		if clustergateway.IsNotFoundError(err) {
			return nil, status.Errorf(codes.FailedPrecondition, "cluster %s not found: %s", clusterId, err)
		}
		return nil, status.Errorf(codes.Internal, "failed to get keyspace %s for shared pool %s: %s", clusterId, sharedPoolId, err)
	}

	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get remote client for shared pool %s: %s", sharedPoolId, err)
	}

	policy := &entity.PlacementPolicy{
		SharedPoolId:      sharedPoolId,
		PlacementPolicyId: placementPolicyId,
		ClusterId:         clusterId,
		TikvGroupId:       tikvGroupId,
	}
	if !allowUpdate {
		return tioperator.CreatePlacementPolicy(ctx, remoteCli, sharedPoolId, policy, keyspace.GetId())
	}
	return tioperator.UpdatePlacementPolicy(ctx, remoteCli, sharedPoolId, policy, keyspace.GetId())
}

func (s *serverImpl) DeletePlacementPolicy(ctx context.Context, sharedPoolId, placementPolicyId string) error {
	sharedPool, err := s.loadSharedPoolForPlacementPolicy(ctx, sharedPoolId)
	if err != nil {
		return err
	}
	if err := guardSharedPoolMutation(sharedPool, sharedPoolMutationBlockedWhilePaused); err != nil {
		return err
	}

	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return status.Errorf(codes.Internal, "failed to get remote client for shared pool %s: %s", sharedPoolId, err)
	}
	return tioperator.DeletePlacementPolicy(ctx, remoteCli, sharedPoolId, placementPolicyId)
}

func (s *serverImpl) loadSharedPoolForPlacementPolicy(ctx context.Context, sharedPoolId string) (*entity.SharedPool, error) {
	sharedPool, err := s.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return nil, status.Errorf(codes.NotFound, "sharedPool %s not found", sharedPoolId)
		}
		return nil, status.Errorf(codes.Internal, "failed to load sharedPool %s: %s", sharedPoolId, err)
	}
	return sharedPool, nil
}

func validatePlacementPolicyTiKVGroupWritable(sharedPool *entity.SharedPool, tikvGroupId string) error {
	for _, comp := range sharedPool.GetComponentGroups() {
		if comp.GetComponentType() == apiv1.ComponentType_TIKV && comp.GetId() == tikvGroupId {
			if comp.GetPhase() != apiv1.Component_AVAILABLE {
				return status.Errorf(codes.FailedPrecondition, "tikv group %s is not available, current phase: %s", tikvGroupId, comp.GetPhase().String())
			}
			return nil
		}
	}
	return status.Errorf(codes.FailedPrecondition, "tikv group %s not found in shared pool %s", tikvGroupId, sharedPool.GetId())
}
