package cluster

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/tiinf/transport/crossregion"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	apperror "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/compat/common"
	models "github.com/tidbcloud/infra-provider/pkg/compat/models"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

const (
	errNoneClusterType     = "none cluster type"
	OverrideRegistryKey    = "eks.infra.pingcap.com/override-cluster-registry"
	OverrideRepoPrefixKey  = "eks.infra.pingcap.com/override-cluster-repoprefix"
	OverrideHelperImageKey = "eks.infra.pingcap.com/override-cluster-helper-image"
	clusterurl             = "/inner/mgmt/compat/v1/clusters/%s?showDeleted=true"
)

type CompatClusterClienter interface {
	GetCluster(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error
}

type CompatClusterClient struct {
	Env crossregion.EnvType
	ops *gitops
	cli *ClusterCli
	// If enableFeatures, we'll try to get enabled features from cluster service directly
	enableFeatures bool
}

func NewCompatClusterClient(env crossregion.EnvType, override config.OverrideConfig, endpoint string) *CompatClusterClient {
	c := &CompatClusterClient{
		Env: env,
		ops: newGitops(override),
	}
	if endpoint != "" {
		c.enableFeatures = true
		c.cli = NewClusterCli(endpoint)
	}

	return c
}

func (ccc *CompatClusterClient) GetCluster(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	logger := log.FromContext(ctx).WithName("CompatClusterClient")
	cluster, ok := obj.(*cloudv1.Cluster)
	if !ok {
		return apperror.NewInternalError(errors.New(errNoneClusterType))
	}
	resp, code, err := common.GetGlobalHTTP[models.V1CompatCluster](ctx, ccc.Env, clusterurl, key.Name, nil)
	if err != nil {
		if code == int(bizerr.ErrCodeRecordNotFound) {
			return apperror.NewNotFound(schema.GroupResource{
				Group:    obj.GetObjectKind().GroupVersionKind().Group,
				Resource: obj.GetObjectKind().GroupVersionKind().Kind,
			}, key.Name)
		}
		return apperror.NewInternalError(err)
	}
	if code != http.StatusOK {
		if code == http.StatusNotFound && resp == nil {
			return apperror.NewNotFound(schema.GroupResource{
				Group:    obj.GetObjectKind().GroupVersionKind().Group,
				Resource: obj.GetObjectKind().GroupVersionKind().Kind,
			}, key.Name)
		}
		logger.Info("call global cluster failed", "code", code)
		return apperror.NewInternalError(fmt.Errorf("call global cluster failed, code=%d", code))
	}

	cluster.Name = resp.Id
	cluster.Namespace = key.Namespace
	provider := globalProvier2CompatProvider(resp.Provider)
	cluster.Labels = map[string]string{
		cloudv1.ProviderTypeLabel: provider,
		cloudv1.TenantTagKey:      resp.OrganizationId,
		cloudv1.ProjectTagKey:     resp.ProjectId,
		cloudv1.ClusterTagKey:     resp.Id,
		cloudv1.RegionLabel:       resp.Region,
	}
	// for git ops annotation
	cluster.Annotations = map[string]string{
		OverrideRegistryKey:   ccc.ops.GetOverrideRegistry(),
		OverrideRepoPrefixKey: ccc.ops.GetOverrideRepoprefix(),
		// this is a workaround to make ticdc controller not to update migrated cluster CR when try to apply config
		cloudv1.AnnoTiCDCIgnoreConfigUpdate: "True",
	}

	if ccc.enableFeatures {
		// GetCluster from cluster service directly.
		// Now it's only for features field.
		c, err := ccc.cli.GetCluster(ctx, &clusterctlpb.GetClusterRequest{
			ClusterId: resp.Id,
		})
		if err == nil {
			logger.Info("enabled features", "features", c.Features)
			for _, f := range c.Features {
				cluster.Annotations[cloudv1.AnnoKeyFeatureNamePrefix+f] = "true"
			}
		} else {
			statusErr, ok := status.FromError(err)
			// Ignore not found because we cannot get clusters in logical regions
			// NOTE: DO NOT enable any features for clusters in logical regions until br support 3-layers
			if !ok || statusErr.Code() != codes.NotFound {
				return apperror.NewInternalError(fmt.Errorf("call cluster service failed: %w", err))
			}
		}

	}

	cluster.Spec = cloudv1.ClusterSpec{
		Cluster:     cloudv1.ClusterTopology{},
		NetworkName: fmt.Sprintf("%s-%s", provider, resp.Region),
	}
	if resp.ClusterTopology != nil {
		cluster.Spec.Cluster.TiDB = cloudv1.TiDB{
			Replicas:     int(resp.ClusterTopology.Tidb.Replicas),
			InstanceType: resp.ClusterTopology.Tidb.InstanceType,
		}
		cluster.Spec.Cluster.TiKV = cloudv1.TiKV{
			Replicas:     int(resp.ClusterTopology.Tikv.Replicas),
			InstanceType: resp.ClusterTopology.Tikv.InstanceType,
		}
		cluster.Spec.Cluster.PD = cloudv1.PD{
			Replicas:     int(resp.ClusterTopology.Pd.Replicas),
			InstanceType: resp.ClusterTopology.Pd.InstanceType,
		}
		cluster.Spec.Cluster.TiFlash = cloudv1.TiFlash{
			Replicas:     int(resp.ClusterTopology.Tiflash.Replicas),
			InstanceType: resp.ClusterTopology.Tiflash.InstanceType,
		}
		cluster.Spec.Cluster.TiProxy = cloudv1.TiProxy{
			Replicas:     int(resp.ClusterTopology.Tiproxy.Replicas),
			InstanceType: resp.ClusterTopology.Tiproxy.InstanceType,
		}
	}
	// set cluster k8s status
	switch provider {
	case consts.ProviderTypeEKS:
		{
			eksStatus := cloudv1.EKSClusterStatus{
				TypeMeta: metav1.TypeMeta{
					Kind:       reflect.TypeOf(cloudv1.EKSClusterStatus{}).Name(),
					APIVersion: cloudv1.V1alpha1GroupVersion.String(),
				},
				CommonK8sClusterStatus: cloudv1.CommonK8sClusterStatus{
					Kubernetes: &cloudv1.KubernetesStatus{
						Namespace: fmt.Sprintf("tidb%s", resp.Id),
					},
				},
			}
			err := setKubeStatusEKS(cluster, &eksStatus)
			if err != nil {
				return apperror.NewInternalError(fmt.Errorf("set k8s status failed, error=%w", err))
			}
		}
	case consts.ProviderTypeGKE:
		{
			gkeStatus := cloudv1.GKEClusterStatus{
				TypeMeta: metav1.TypeMeta{
					Kind:       reflect.TypeOf(cloudv1.GKEClusterStatus{}).Name(),
					APIVersion: cloudv1.V1alpha1GroupVersion.String(),
				},
				CommonK8sClusterStatus: cloudv1.CommonK8sClusterStatus{
					Kubernetes: &cloudv1.KubernetesStatus{
						Namespace: fmt.Sprintf("tidb%s", resp.Id),
					},
				},
			}
			err := setKubeStatusGKE(cluster, &gkeStatus)
			if err != nil {
				return apperror.NewInternalError(fmt.Errorf("set k8s status failed, error=%w", err))
			}
		}
	}
	logger.Info("get cluster", "cluster", cluster)
	return nil
}

type gitops struct {
	Override config.OverrideConfig `json:"override"`
}

func newGitops(override config.OverrideConfig) *gitops {
	return &gitops{
		Override: override,
	}
}

func (g *gitops) GetOverrideRegistry() string {
	return g.Override.OverrideRegistry
}

func (g *gitops) GetOverrideRepoprefix() string {
	return g.Override.OverrideRepoPrefix
}

func globalProvier2CompatProvider(provider string) string {
	switch provider {
	case "aws":
		return "eks"
	case "gcp":
		return "gke"
	default:
		return provider
	}
}

func setKubeStatusEKS(cluster *cloudv1.Cluster, status *cloudv1.EKSClusterStatus) error {
	data, err := json.Marshal(status)
	if err != nil {
		return err
	}
	cluster.Status.FromProvider.Raw = data
	cluster.Status.FromProvider.Object = nil
	return nil
}

func setKubeStatusGKE(cluster *cloudv1.Cluster, status *cloudv1.GKEClusterStatus) error {
	data, err := json.Marshal(status)
	if err != nil {
		return err
	}
	cluster.Status.FromProvider.Raw = data
	cluster.Status.FromProvider.Object = nil
	return nil
}
