package service

import (
	"crypto/sha256"
	"encoding/hex"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

const (
	gkeComputeClassGroup   = "cloud.google.com"
	gkeComputeClassVersion = "v1"
	gkeComputeClassKind    = "ComputeClass"
	gkeComputeClassLabel   = "compute class"

	gatekeeperMutationGroup   = "mutations.gatekeeper.sh"
	gatekeeperMutationVersion = "v1"
	gatekeeperAssignKind      = "Assign"
	gatekeeperAssignLabel     = "gatekeeper assign"

	gkeHeterogeneousComponentLabelKey          = "tidbcloud.com/component"
	gkeHeterogeneousDeployLabelKey             = "tidbcloud.com/heterogeneous-deploy"
	gkeHeterogeneousComputeClassLabelKey       = "tidbcloud.com/gke-compute-class"
	gkeHeterogeneousGatekeeperAssignPurposeKey = "tidbcloud.com/gke-heterogeneous-gatekeeper-assign-purpose"
	gkeHeterogeneousGatekeeperExemptLabelKey   = "tidbcloud.com/gke-heterogeneous-gatekeeper-exempt"
	gkeHeterogeneousInstanceTypesAnnotationKey = "tidbcloud.com/gke-compute-class-instance-types"
	gkeHeterogeneousInstanceTypesHashLabelKey  = "tidbcloud.com/gke-compute-class-instance-types-hash"
	gkeHeterogeneousBindingsAnnotationKey      = "tidbcloud.com/gke-compute-class-bindings"
	gkeHeterogeneousBindingsHashLabelKey       = "tidbcloud.com/gke-compute-class-bindings-hash"
	gkeHeterogeneousComputeClassScopeLabelKey  = "tidbcloud.com/gke-compute-class-scope"
	gkeHeterogeneousBaseInstanceTypeLabelKey   = "tidbcloud.com/gke-compute-class-base-instance-type"
	gkeHeterogeneousExtraInstanceTypeLabelKey  = "tidbcloud.com/gke-compute-class-heterogeneous-instance-type"
	gkeHeterogeneousBaseNodePoolLabelKey       = "tidbcloud.com/gke-compute-class-base-nodepool"
	gkeHeterogeneousExtraNodePoolLabelKey      = "tidbcloud.com/gke-compute-class-heterogeneous-nodepool"
	gkeComputeClassDeployLabelValue            = "gke-compute-class"
	gkeComputeClassScopeBindingsLabelValue     = "bindings"
	gatekeeperAssignDeployLabelValue           = "gatekeeper-assign"
	gatekeeperAssignPurposeNodeSelector        = "compute-class-node-selector"
	gatekeeperAssignPurposeTopologySpread      = "topology-spread-node-affinity-policy"
	gkeHeterogeneousManagedByClusterAgent      = "cluster-agent"

	gatekeeperTopologySpreadConstraintLocation   = "spec.topologySpreadConstraints[topologyKey:'topology.kubernetes.io/zone']"
	gatekeeperTopologySpreadNodeAffinityLocation = gatekeeperTopologySpreadConstraintLocation + ".nodeAffinityPolicy"
	gkeWhenUnsatisfiableDoNotScaleUp             = "DoNotScaleUp"
)

var (
	gkeComputeClassGVK      = schema.GroupVersionKind{Group: gkeComputeClassGroup, Version: gkeComputeClassVersion, Kind: gkeComputeClassKind}
	gkeComputeClassListGVK  = schema.GroupVersionKind{Group: gkeComputeClassGroup, Version: gkeComputeClassVersion, Kind: gkeComputeClassKind + "List"}
	gatekeeperAssignGVK     = schema.GroupVersionKind{Group: gatekeeperMutationGroup, Version: gatekeeperMutationVersion, Kind: gatekeeperAssignKind}
	gatekeeperAssignListGVK = schema.GroupVersionKind{Group: gatekeeperMutationGroup, Version: gatekeeperMutationVersion, Kind: gatekeeperAssignKind + "List"}
)

type gkeHeterogeneousDataplaneComponent struct {
	componentType cloudv1.ComponentType
	tidbGroupID   string
	tidbGroupKind apiv1.TiDBGroupKind
}

func (c *gkeHeterogeneousDataplaneComponent) name() string {
	if c == nil {
		return ""
	}
	return string(c.componentType)
}

func (c *gkeHeterogeneousDataplaneComponent) isTiDB() bool {
	return c != nil && c.componentType == cloudv1.ComponentTiDB
}

type gkeHeterogeneousAssignSettings struct {
	namespace    string
	nodeSelector map[string]string
	matchLabels  map[string]string
	pods         []corev1.Pod
}

func (s *AgentService) validateGKEClusterAgentRequest(clusterID, apiName string) (string, error) {
	clusterID = strings.TrimSpace(clusterID)
	if clusterID == "" {
		return "", status.Error(codes.InvalidArgument, "cluster_id is required")
	}
	// Regional-server routes these APIs through a cluster-specific gateway. The
	// namespace check prevents a forwarded request from mutating another cluster.
	clusterNamespace := strings.TrimSpace(s.clusterNamespace)
	if clusterNamespace == "" {
		return "", status.Error(codes.FailedPrecondition, "cluster-agent namespace is required")
	}
	if namespace := buildNamespace(clusterID); namespace != clusterNamespace {
		return "", status.Errorf(codes.PermissionDenied, "cluster_id %s does not match this cluster-agent namespace %s", clusterID, clusterNamespace)
	}
	provider := strings.TrimSpace(s.provider)
	if provider != consts.ProviderTypeGKE && provider != consts.ProviderTypeGCP {
		return "", status.Errorf(codes.FailedPrecondition, "%s require gke/gcp cluster-agent provider, got %q", apiName, provider)
	}
	return clusterID, nil
}

func resolveGKEHeterogeneousComponent(componentType apicommon.ComponentType, groupID string) (*gkeHeterogeneousDataplaneComponent, error) {
	groupID = strings.TrimSpace(groupID)
	switch componentType {
	case apicommon.ComponentType_TIKV:
		// tidb_group_id only selects TiDB groups. TiKV has one heterogeneous
		// dataplane assignment per cluster/component, so request values are ignored.
		return &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, nil
	case apicommon.ComponentType_TIDB:
		if groupID == "" {
			return nil, status.Error(codes.InvalidArgument, "tidb_group_id is required for TiDB heterogeneous deploy")
		}
		return &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB, tidbGroupID: groupID}, nil
	default:
		return nil, status.Errorf(codes.InvalidArgument, "unsupported heterogeneous component type %q", componentType)
	}
}

func resolveGKEHeterogeneousComputeClassComponent(componentType apicommon.ComponentType) (*gkeHeterogeneousDataplaneComponent, error) {
	switch componentType {
	case apicommon.ComponentType_TIKV:
		return &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, nil
	case apicommon.ComponentType_TIDB:
		return &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB}, nil
	default:
		return nil, status.Errorf(codes.InvalidArgument, "unsupported heterogeneous component type %q", componentType)
	}
}

func resolveGKEHeterogeneousGatekeeperComponent(componentType apicommon.ComponentType, groupID string, groupKind apiv1.TiDBGroupKind) (*gkeHeterogeneousDataplaneComponent, error) {
	component, err := resolveGKEHeterogeneousComponent(componentType, groupID)
	if err != nil {
		return nil, err
	}
	if !component.isTiDB() {
		if groupKind != apiv1.TiDBGroupKind_TIDB_GROUP_KIND_UNSPECIFIED {
			return nil, status.Error(codes.InvalidArgument, "tidb_group_kind is only supported for TiDB heterogeneous deploy")
		}
		return component, nil
	}
	// The agent does not read Cluster CR group metadata, so TiDB requests must
	// carry the group kind used to build the pod selector.
	switch groupKind {
	case apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT, apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA:
		component.tidbGroupKind = groupKind
		return component, nil
	default:
		return nil, status.Error(codes.InvalidArgument, "tidb_group_kind is required for TiDB heterogeneous deploy")
	}
}

func gkeHeterogeneousPodMatchLabels(component *gkeHeterogeneousDataplaneComponent) map[string]string {
	matchLabels := map[string]string{consts.ComponentLabelKey: component.name()}
	if !component.isTiDB() {
		matchLabels[consts.InstanceLabelKey] = consts.ClusterPrefix
	}
	return matchLabels
}

func gkeHeterogeneousTiDBGroupInstanceLabel(groupID string) string {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" {
		return consts.ClusterPrefix
	}
	return namer.NewName(groupID, consts.ClusterPrefix, "", -1)
}

func defaultGKEHeterogeneousResourceName(clusterID string, component *gkeHeterogeneousDataplaneComponent, suffix string) string {
	return gkeheterogeneous.ResourceName(clusterID, component.componentType, component.tidbGroupID, suffix)
}

func gatekeeperAssignExemptToken(name string) string {
	sum := sha256.Sum256([]byte(name))
	return "hga-" + hex.EncodeToString(sum[:])[:16]
}

func gkeHeterogeneousLabels(clusterID, deployLabelValue string, component *gkeHeterogeneousDataplaneComponent) map[string]interface{} {
	labels := map[string]interface{}{
		consts.ManagedByLabelKey:          gkeHeterogeneousManagedByClusterAgent,
		consts.ClusterLabelKey:            clusterID,
		gkeHeterogeneousDeployLabelKey:    deployLabelValue,
		gkeHeterogeneousComponentLabelKey: component.name(),
	}
	if component.isTiDB() && component.tidbGroupID != "" {
		labels[consts.LabelKeyTiDBGroupId] = component.tidbGroupID
	}
	return labels
}

func gkeHeterogeneousComponentType(component string) apicommon.ComponentType {
	switch cloudv1.ComponentType(strings.TrimSpace(component)) {
	case cloudv1.ComponentTiDB:
		return apicommon.ComponentType_TIDB
	case cloudv1.ComponentTiKV:
		return apicommon.ComponentType_TIKV
	default:
		return apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	}
}
