package proxy

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/gin-gonic/gin"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	shared "github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	nodepoolutil "github.com/tidbcloud/infra-provider/pkg/nodepool"
)

const (
	gkeHeterogeneousComputeClassInUseByDeployIntentReason = "COMPUTE_CLASS_IN_USE_BY_HETEROGENEOUS_DEPLOY_INTENT"
	gkeHeterogeneousComputeClassInUseByNodePoolReason     = "COMPUTE_CLASS_IN_USE_BY_HETEROGENEOUS_NODEPOOL"
)

func (p *ClusterAgentProxy) rejectDeleteGKEHeterogeneousComputeClassInUse(c *gin.Context, anchor *cloudv1.Cluster) bool {
	if c.Request.Method != http.MethodDelete || !strings.HasSuffix(c.FullPath(), "/gkeHeterogeneous/computeClasses/:name") {
		return false
	}
	computeClassName := strings.TrimSpace(c.Param("name"))
	peers, err := p.listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(c.Request.Context(), anchor)
	if err != nil {
		st := status.Convert(err)
		abortWithGRPCError(c, st.Code(), st.Message(), "")
		return true
	}
	refs, err := gkeheterogeneous.ComputeClassIntentRefs(peers, computeClassName)
	if err != nil {
		st := status.Convert(err)
		abortWithGRPCError(c, st.Code(), st.Message(), "")
		return true
	}
	if len(refs) == 0 {
		nodePoolRefs, nodePoolErr := p.computeClassNodePoolRefsInSameKubernetesClusterScope(c.Request.Context(), anchor, computeClassName)
		if nodePoolErr != nil {
			st := status.Convert(nodePoolErr)
			abortWithGRPCError(c, st.Code(), st.Message(), "")
			return true
		}
		if len(nodePoolRefs) == 0 {
			return false
		}
		abortWithGRPCError(
			c,
			codes.FailedPrecondition,
			fmt.Sprintf(
				"compute class %s is still referenced by heterogeneous nodepool metadata: %s",
				computeClassName,
				gkeheterogeneous.FormatComputeClassNodePoolRefs(nodePoolRefs),
			),
			gkeHeterogeneousComputeClassInUseByNodePoolReason,
		)
		return true
	}
	abortWithGRPCError(
		c,
		codes.FailedPrecondition,
		fmt.Sprintf(
			"compute class %s is still referenced by heterogeneous deploy intent: %s",
			computeClassName,
			gkeheterogeneous.FormatComputeClassIntentRefs(refs),
		),
		gkeHeterogeneousComputeClassInUseByDeployIntentReason,
	)
	return true
}

func (p *ClusterAgentProxy) computeClassNodePoolRefsInSameKubernetesClusterScope(ctx context.Context, anchor *cloudv1.Cluster, computeClassName string) ([]gkeheterogeneous.ComputeClassNodePoolRef, error) {
	if anchor == nil || anchor.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}
	scopeLabels, missingScopeLabels := nodepoolutil.GKEHeterogeneousNodePoolScopeLabels(anchor)
	if len(missingScopeLabels) > 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s/%s is missing required GKE heterogeneous nodepool scope labels: %s", anchor.Namespace, anchor.Name, strings.Join(missingScopeLabels, ","))
	}
	if p == nil || p.kubeClient == nil {
		return nil, status.Error(codes.Internal, "cluster agent proxy kube client is not configured")
	}

	selector := map[string]string{
		namer.ComponentSelectorKey:                       shared.ComponentShared,
		namer.ClusterSelectorKey:                         shared.ClustersShared,
		consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
	}
	for key, value := range scopeLabels {
		selector[key] = value
	}
	var nodePools gkev1beta1.NodePoolList
	if err := p.kubeClient.List(ctx, &nodePools, client.MatchingLabels(selector)); err != nil {
		return nil, status.Errorf(codes.Internal, "list nodepools in same GKE heterogeneous nodepool scope for gke compute class delete guard: %s", err)
	}
	return gkeheterogeneous.NodePoolComputeClassRefs(nodePools.Items, computeClassName), nil
}

func (p *ClusterAgentProxy) listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(ctx context.Context, target *cloudv1.Cluster) ([]*cloudv1.Cluster, error) {
	if target == nil || target.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}
	if _, missingScopeLabels := nodepoolutil.GKEHeterogeneousNodePoolScopeLabels(target); len(missingScopeLabels) > 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s/%s is missing required GKE heterogeneous nodepool scope labels: %s", target.Namespace, target.Name, strings.Join(missingScopeLabels, ","))
	}
	scopeLabels, missingScopeLabels := nodepoolutil.GKEKubernetesClusterScopeLabels(target)
	if len(missingScopeLabels) > 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s/%s is missing required GKE Kubernetes cluster scope labels: %s", target.Namespace, target.Name, strings.Join(missingScopeLabels, ","))
	}
	if p == nil || p.kubeClient == nil {
		return nil, status.Error(codes.Internal, "cluster agent proxy kube client is not configured")
	}

	var clusters cloudv1.ClusterList
	if err := p.kubeClient.List(ctx, &clusters, client.MatchingLabels(scopeLabels)); err != nil {
		return nil, status.Errorf(codes.Internal, "list clusters in same GKE heterogeneous nodepool scope for gke shared nodepool peers: %s", err)
	}

	out := make([]*cloudv1.Cluster, 0, len(clusters.Items)+1)
	seen := map[string]struct{}{}
	for i := range clusters.Items {
		peer := &clusters.Items[i]
		if !nodepoolutil.GKEClustersShareHeterogeneousNodePoolScope(target, peer) {
			continue
		}
		key := peer.Namespace + "/" + peer.Name
		seen[key] = struct{}{}
		out = append(out, peer)
	}
	targetKey := target.Namespace + "/" + target.Name
	if _, ok := seen[targetKey]; !ok {
		out = append(out, target)
	}
	return out, nil
}

func httpStatusForGRPCCode(code codes.Code) int {
	switch code {
	case codes.InvalidArgument:
		return http.StatusBadRequest
	case codes.NotFound:
		return http.StatusNotFound
	case codes.FailedPrecondition:
		return http.StatusPreconditionFailed
	case codes.PermissionDenied:
		return http.StatusForbidden
	default:
		return http.StatusInternalServerError
	}
}

func abortWithGRPCError(c *gin.Context, code codes.Code, msg, reason string) {
	st := status.New(code, msg)
	if strings.TrimSpace(reason) != "" {
		stWithDetails, err := st.WithDetails(&errdetails.ErrorInfo{
			Reason: strings.TrimSpace(reason),
			Domain: "infra-provider",
		})
		if err == nil {
			st = stWithDetails
		}
	}
	body, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(st.Proto())
	if err != nil {
		c.AbortWithStatusJSON(httpStatusForGRPCCode(code), gin.H{
			"code":    int32(code),
			"message": msg,
		})
		return
	}
	c.Abort()
	c.Data(httpStatusForGRPCCode(code), gin.MIMEJSON, body)
}
