package server

import (
	"context"
	"fmt"
	"sort"
	"strings"

	clusteragentv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	k8sComponentLabelKey  = "app.kubernetes.io/component"
	k8sInstanceLabelKey   = "app.kubernetes.io/instance"
	pingcapGroupLabelKey  = "pingcap.com/group"
	tiProxyComponentValue = "tiproxy"
)

type tiProxyBackendActionContext struct {
	sharedPool         *entity.SharedPool
	remoteCli          client.Client
	tiproxyComponent   string
	tiproxyGroupName   string
	dataPlaneNamespace string
}

func tiProxyGroupName(tiproxyComponentId string) string {
	if strings.HasPrefix(tiproxyComponentId, "db-") {
		return tiproxyComponentId
	}
	return fmt.Sprintf("db-%s", tiproxyComponentId)
}

func (s *serverImpl) markTiProxyBackendsNoNewConnections(ctx context.Context, sharedPoolId, tiproxyGroupId string, podNames []string) error {
	if sharedPoolId == "" {
		return status.Error(codes.InvalidArgument, "sharedPool id is required")
	}
	if tiproxyGroupId == "" {
		return status.Error(codes.InvalidArgument, "tiproxy group id is required")
	}
	if len(podNames) == 0 {
		return status.Error(codes.InvalidArgument, "pod_names is required")
	}
	actionCtx, err := s.prepareTiProxyBackendActionContext(ctx, sharedPoolId, tiproxyGroupId)
	if err != nil {
		return err
	}
	for _, podName := range podNames {
		if podName == "" {
			return status.Error(codes.InvalidArgument, "pod_names contains empty pod name")
		}
		if _, err := getAndValidateTiProxyPod(ctx, actionCtx.remoteCli, actionCtx.dataPlaneNamespace, actionCtx.tiproxyGroupName, podName); err != nil {
			if status.Code(err) == codes.NotFound {
				tilog.Warn(ctx, "TiProxy pod disappeared before legacy unhealthy validation",
					zap.String("sharedPoolId", actionCtx.sharedPool.GetId()),
					zap.String("podName", podName))
				continue
			}
			return err
		}
		if err := s.clustergatewayCli.MarkTiProxyPodUnhealthyLegacy(ctx, actionCtx.sharedPool, &clusteragentv1.MarkTiProxyPodUnhealthyLegacyReq{
			ClusterId: actionCtx.sharedPool.GetId(),
			PodName:   podName,
		}); err != nil {
			return status.Errorf(codes.Internal, "failed to mark tiproxy pod %s unhealthy: %s", podName, err)
		}
	}
	return nil
}

func (s *serverImpl) prepareTiProxyBackendActionContext(ctx context.Context, sharedPoolId, tiproxyGroupId string) (*tiProxyBackendActionContext, error) {
	sharedPool, err := s.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to load sharedPool %s: %s", sharedPoolId, err)
	}

	tiproxyComponentId := strings.TrimPrefix(tiproxyGroupId, "db-")
	tiproxyComponent := entity.FindTiProxyComponent(sharedPool, tiproxyComponentId)
	if tiproxyComponent == nil {
		return nil, status.Errorf(codes.NotFound, "tiproxy component %s not found in sharedPool %s", tiproxyComponentId, sharedPoolId)
	}
	tiproxyComponentId = tiproxyComponent.GetId()

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

	return &tiProxyBackendActionContext{
		sharedPool:         sharedPool,
		remoteCli:          remoteCli,
		tiproxyComponent:   tiproxyComponentId,
		tiproxyGroupName:   tiProxyGroupName(tiproxyComponentId),
		dataPlaneNamespace: sharedPool.GetDataPlaneNamespace(),
	}, nil
}

func (s *serverImpl) tiProxyLoadBalancerNames(ctx context.Context, sharedPool *entity.SharedPool, tiproxyComponentId string) ([]string, error) {
	tidbGroups, err := s.tiProxyTiDBGroups(ctx, sharedPool, tiproxyComponentId)
	if err != nil {
		return nil, err
	}

	seen := make(map[string]struct{}, len(tidbGroups)*2)
	lbNames := make([]string, 0, len(tidbGroups)*2)
	appendLBName := func(lbName string) {
		if lbName == "" {
			return
		}
		if _, ok := seen[lbName]; ok {
			return
		}
		seen[lbName] = struct{}{}
		lbNames = append(lbNames, lbName)
	}
	for _, tidbGroup := range tidbGroups {
		if tidbGroup.GetSqlEndpoint().GetPrivateLb().GetEnabled() {
			appendLBName(entity.GetPrivateSvcName(tidbGroup.GetId()))
		}
		if tidbGroup.GetSqlEndpoint().GetPublicLb().GetEnabled() {
			appendLBName(entity.GetPublicSvcName(tidbGroup.GetId()))
		}
	}
	sort.Strings(lbNames)
	return lbNames, nil
}

func (s *serverImpl) tiProxyTiDBGroups(ctx context.Context, sharedPool *entity.SharedPool, tiproxyComponentId string) ([]*entity.TiDBGroup, error) {
	if entity.HasSharedTiProxy(sharedPool) && tiproxyComponentId == entity.ResolveTiProxyID(sharedPool, "") {
		tidbGroups, err := s.tidbGroupRepo.List(ctx, []string{sharedPool.GetId()}, nil, nil, nil, false)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "failed to list tidb groups for sharedPool %s: %s", sharedPool.GetId(), err)
		}
		return tidbGroups, nil
	}
	tidbGroups, err := s.tidbGroupRepo.List(ctx, []string{sharedPool.GetId()}, nil, []string{tiproxyComponentId}, nil, false)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list tidb groups for tiproxy component %s: %s", tiproxyComponentId, err)
	}
	return tidbGroups, nil
}

func getAndValidateTiProxyPod(ctx context.Context, remoteCli client.Client, namespace, groupName, podName string) (*corev1.Pod, error) {
	var pod corev1.Pod
	key := client.ObjectKey{Namespace: namespace, Name: podName}
	if err := remoteCli.Get(ctx, key, &pod); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "pod %s/%s not found", namespace, podName)
		}
		return nil, status.Errorf(codes.Internal, "failed to get pod %s/%s: %s", namespace, podName, err)
	}
	if pod.Labels[k8sComponentLabelKey] != tiProxyComponentValue {
		return nil, status.Errorf(codes.InvalidArgument, "pod %s/%s is not a tiproxy pod", namespace, podName)
	}
	if pod.Labels[k8sInstanceLabelKey] != groupName && pod.Labels[pingcapGroupLabelKey] != groupName {
		return nil, status.Errorf(codes.InvalidArgument, "pod %s/%s does not belong to tiproxy group %s", namespace, podName, groupName)
	}
	return &pod, nil
}

func listTiProxyPods(ctx context.Context, remoteCli client.Client, namespace, groupName string) ([]corev1.Pod, error) {
	var pods corev1.PodList
	if err := remoteCli.List(ctx, &pods, client.InNamespace(namespace), client.MatchingLabels{k8sComponentLabelKey: tiProxyComponentValue}); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list tiproxy pods in namespace %s: %s", namespace, err)
	}

	out := make([]corev1.Pod, 0, len(pods.Items))
	for i := range pods.Items {
		pod := &pods.Items[i]
		if pod.Labels[k8sInstanceLabelKey] == groupName || pod.Labels[pingcapGroupLabelKey] == groupName {
			out = append(out, *pod)
		}
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].Name < out[j].Name
	})
	return out, nil
}
