package service

import (
	"context"
	"fmt"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	k8smeta "k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

type managedUnstructuredValidateFunc func(current, desired *unstructured.Unstructured) error

func createManagedUnstructured(ctx context.Context, cli client.Client, desired *unstructured.Unstructured, validate managedUnstructuredValidateFunc) (*unstructured.Unstructured, error) {
	current, _, err := createManagedUnstructuredWithCreated(ctx, cli, desired, validate)
	return current, err
}

func preflightManagedUnstructured(ctx context.Context, cli client.Client, desired *unstructured.Unstructured, validate managedUnstructuredValidateFunc) error {
	key := client.ObjectKeyFromObject(desired)
	current := newManagedUnstructuredCurrent(desired)
	err := cli.Get(ctx, key, current)
	if apierrors.IsNotFound(err) {
		return nil
	}
	if err != nil {
		return gkeHeterogeneousResourceClientError(desired.GetKind(), err, "get current %s %s", desired.GetKind(), desired.GetName())
	}
	return validateManagedUnstructuredCurrent(current, desired, validate, codes.FailedPrecondition)
}

func createManagedUnstructuredWithCreated(ctx context.Context, cli client.Client, desired *unstructured.Unstructured, validate managedUnstructuredValidateFunc) (*unstructured.Unstructured, bool, error) {
	key := client.ObjectKeyFromObject(desired)
	current := newManagedUnstructuredCurrent(desired)
	err := cli.Get(ctx, key, current)
	if err == nil {
		if err := validateManagedUnstructuredCurrent(current, desired, validate, codes.AlreadyExists); err != nil {
			return nil, false, err
		}
		return current, false, nil
	}
	if !apierrors.IsNotFound(err) {
		return nil, false, gkeHeterogeneousResourceClientError(desired.GetKind(), err, "get current %s %s", desired.GetKind(), desired.GetName())
	}

	createErr := cli.Create(ctx, desired)
	created := createErr == nil
	if createErr != nil && !apierrors.IsAlreadyExists(createErr) {
		return nil, false, gkeHeterogeneousResourceClientError(desired.GetKind(), createErr, "create %s %s", desired.GetKind(), desired.GetName())
	}

	current = newManagedUnstructuredCurrent(desired)
	if getErr := cli.Get(ctx, key, current); getErr != nil {
		return nil, created, gkeHeterogeneousResourceClientError(desired.GetKind(), getErr, "get current %s %s after create", desired.GetKind(), desired.GetName())
	}
	if err := validateManagedUnstructuredCurrent(current, desired, validate, codes.AlreadyExists); err != nil {
		return nil, created, err
	}
	return current, created, nil
}

func newManagedUnstructuredCurrent(desired *unstructured.Unstructured) *unstructured.Unstructured {
	current := &unstructured.Unstructured{}
	current.SetGroupVersionKind(desired.GroupVersionKind())
	return current
}

func validateManagedUnstructuredCurrent(current, desired *unstructured.Unstructured, validate managedUnstructuredValidateFunc, incompatibleCode codes.Code) error {
	if err := validate(current, desired); err != nil {
		if status.Code(err) == codes.AlreadyExists && incompatibleCode != codes.AlreadyExists {
			return status.Error(incompatibleCode, status.Convert(err).Message())
		}
		return err
	}
	if equality.Semantic.DeepEqual(current.Object["spec"], desired.Object["spec"]) {
		return nil
	}
	return status.Errorf(incompatibleCode, "%s %s already exists with different spec", desired.GetKind(), desired.GetName())
}

func getGKEHeterogeneousResource(ctx context.Context, cli client.Client, gvk schema.GroupVersionKind, name, resourceLabel string) (*unstructured.Unstructured, error) {
	obj := &unstructured.Unstructured{}
	obj.SetGroupVersionKind(gvk)
	if err := cli.Get(ctx, client.ObjectKey{Name: name}, obj); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "%s %s not found", resourceLabel, name)
		}
		return nil, gkeHeterogeneousResourceClientError(resourceLabel, err, "get %s", resourceLabel)
	}
	return obj, nil
}

func gkeHeterogeneousResourceClientError(resourceLabel string, err error, format string, args ...interface{}) error {
	action := fmt.Sprintf(format, args...)
	if k8smeta.IsNoMatchError(err) {
		return status.Errorf(codes.FailedPrecondition, "%s: %s API is not ready; ensure the CRD is installed/enabled: %s", action, resourceLabel, err)
	}
	return status.Errorf(codes.Internal, "%s: %s", action, err)
}

func getOwnedGKEHeterogeneousResource(ctx context.Context, cli client.Client, clusterID, name, resourceLabel string, gvk schema.GroupVersionKind, deployLabels ...string) (*unstructured.Unstructured, error) {
	obj, err := getGKEHeterogeneousResource(ctx, cli, gvk, name, resourceLabel)
	if err != nil {
		return nil, err
	}
	if !isOwnedGKEHeterogeneousResource(obj, clusterID, deployLabels...) {
		return nil, status.Errorf(codes.NotFound, "%s %s not found", resourceLabel, name)
	}
	return obj, nil
}

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

func gkeHeterogeneousComponentResourceLabel(component *gkeHeterogeneousDataplaneComponent) string {
	if component.isTiDB() && component.tidbGroupID != "" {
		return "component tidb group " + component.tidbGroupID
	}
	return "component " + component.name()
}

func validateGKEHeterogeneousOwner(current, desired *unstructured.Unstructured) error {
	desiredClusterID := strings.TrimSpace(desired.GetLabels()[consts.ClusterLabelKey])
	currentClusterID := strings.TrimSpace(current.GetLabels()[consts.ClusterLabelKey])
	if currentClusterID != desiredClusterID {
		if currentClusterID == "" {
			return status.Errorf(codes.AlreadyExists, "%s %s already exists and is not managed by cluster %s", desired.GetKind(), desired.GetName(), desiredClusterID)
		}
		return status.Errorf(codes.AlreadyExists, "%s %s already exists and is managed by cluster %s", desired.GetKind(), desired.GetName(), currentClusterID)
	}
	desiredDeployLabel := desired.GetLabels()[gkeHeterogeneousDeployLabelKey]
	if !isOwnedGKEHeterogeneousResource(current, desiredClusterID, desiredDeployLabel) {
		return status.Errorf(codes.AlreadyExists, "%s %s already exists and is not managed by gke heterogeneous deploy", desired.GetKind(), desired.GetName())
	}
	if err := validateGKEHeterogeneousComponentOwner(current, desired); err != nil {
		return err
	}
	desiredComponent := desired.GetLabels()[gkeHeterogeneousComponentLabelKey]
	if cloudv1.ComponentType(desiredComponent) == cloudv1.ComponentTiDB {
		// TiDB resources are owned per group, not just per cluster/component. Reusing a
		// same-name object across groups would silently cross-wire heterogeneous deploys.
		desiredGroupID := strings.TrimSpace(desired.GetLabels()[consts.LabelKeyTiDBGroupId])
		currentGroupID := strings.TrimSpace(current.GetLabels()[consts.LabelKeyTiDBGroupId])
		if desiredGroupID != "" && currentGroupID == "" {
			return status.Errorf(codes.AlreadyExists, "%s %s already exists without tidb group label, cannot use it for tidb group %s", desired.GetKind(), desired.GetName(), desiredGroupID)
		}
		if desiredGroupID != "" && currentGroupID != desiredGroupID {
			return status.Errorf(codes.AlreadyExists, "%s %s already exists and is managed for tidb group %s, cannot use it for tidb group %s", desired.GetKind(), desired.GetName(), currentGroupID, desiredGroupID)
		}
	}
	return nil
}

func validateGKEHeterogeneousComponentOwner(current, desired *unstructured.Unstructured) error {
	currentComponent := current.GetLabels()[gkeHeterogeneousComponentLabelKey]
	desiredComponent := desired.GetLabels()[gkeHeterogeneousComponentLabelKey]
	if currentComponent == "" {
		return status.Errorf(codes.AlreadyExists, "%s %s already exists without component label, cannot use it for component %s", desired.GetKind(), desired.GetName(), desiredComponent)
	}
	if currentComponent != desiredComponent {
		return status.Errorf(codes.AlreadyExists, "%s %s already exists and is managed for component %s, cannot use it for component %s", desired.GetKind(), desired.GetName(), currentComponent, desiredComponent)
	}
	return nil
}

func isOwnedGKEHeterogeneousResource(obj *unstructured.Unstructured, clusterID string, deployLabels ...string) bool {
	labels := obj.GetLabels()
	if labels[consts.ClusterLabelKey] != clusterID || labels[consts.ManagedByLabelKey] != gkeHeterogeneousManagedByClusterAgent {
		return false
	}
	actual := labels[gkeHeterogeneousDeployLabelKey]
	for _, expected := range deployLabels {
		if expected != "" && actual == expected {
			return true
		}
	}
	return false
}
