package infraapi

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/pingcap/errors"
	entity "github.com/tidbcloud/core-svc/model"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/internal-portal/core-portal/central/config"
	runtimeutil "github.com/tidbcloud/internal-portal/core-portal/central/runtime/util"
	"github.com/tidbcloud/pkgv2/tracing"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/manager"
)

// TagPrefix is the prefix of label that  will be propagated to the underlay resource.
// etc: tags.tidbcloud.com/tenant then the tc created by infra-api will have the "tenant" label.
const TagPrefix = "tags.tidbcloud.com/"

var Tracer = otel.Tracer("central/infra-api")

const (
	tenantLabel  = TagPrefix + "tenant"
	projectLabel = TagPrefix + "project"
	clusterLabel = TagPrefix + "cluster"
)

const (
	projectNameLabel = "projectName"
)

// types for provider type
const (
	ProviderTypeAWS        string = "aws"
	ProviderTypeEKS        string = "eks"
	ProviderTypeGCP        string = "gcp"
	ProviderTypeAWSDevTier string = "aws-free-tier"
)

const (
	infraAPIClientDefaultTimeout = 30 * time.Second
)

// API is the wrapper level to the infra API.
type API struct {
	projectName    string
	mgr            manager.Manager
	client         client.Client // we can mock this client for unit tests.
	InfraAPIConfig config.InfraAPIConfig
}

var (
	api  *API
	once sync.Once
)

type InfraFacade interface {
	GetCluster(ctx context.Context, projectID uint64, clusterID uint64) (c *cloudv1.Cluster, err error)
	UpdateCluster(ctx context.Context, c *cloudv1.Cluster) error
}

const (
	defaultQPS     = 500
	defaultBurst   = 1000
	defaultTimeout = 5 * time.Second
)

func mustGetBaseKubeCli() *kubernetes.Clientset {
	// gardener cluster k8s client
	var cfg *rest.Config
	var err error
	kubeConfig := config.Global().GardenerConfig.KubeConfigPath
	if kubeConfig == "" {
		cfg, err = rest.InClusterConfig()
	} else {
		cfg, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
	}
	if err != nil {
		panic(fmt.Sprintf("initialize the kubernetes config failed: %s", err))
	}

	// Overwrite some configurations.
	cfg.QPS = defaultQPS
	cfg.Burst = defaultBurst
	cfg.Timeout = defaultTimeout

	// Initialize the k8s/gardener clients.
	baseKubeCli, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		panic(fmt.Sprintf("initialize the base kube cli: %s", err))
	}
	return baseKubeCli
}

func InitInfraAPI() *API {
	gardenerConfig := config.Global().GardenerConfig

	once.Do(func() {
		var err error
		// using the gardenerProjectName as the identifier of different central deployment
		api, err = NewAPI(gardenerConfig.GardenerProjectName, config.Global().InfraAPIConfig)
		if err != nil {
			panic("initialize the infra API failed")
		}
	})
	return api
}

func GetInfraAPI() *API {
	if api == nil {
		panic("infraAPI should be initialized first")
	}
	return api
}

// NewAPI returns a new API instance.
func NewAPI(projectName string, cfg config.InfraAPIConfig) (*API, error) {
	scheme := runtime.NewScheme()
	err := cloudv1.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Annotate(err, "register infra API cloudv1")
	}

	err = corev1.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Annotate(err, "register infra API corev1")
	}

	var restCfg *rest.Config
	if cfg.KubeConfigPath != "" {
		restCfg, err = clientcmd.BuildConfigFromFlags("", cfg.KubeConfigPath)
	} else {
		restCfg, err = runtimeutil.GetInfraAPIKubeConfigFromBase(mustGetBaseKubeCli())
	}
	if err != nil {
		return nil, errors.Annotate(err, "retrieve infra API server kubeconfig failed")
	}

	// set timeout for infra api client, ref to https://github.com/kubernetes/client-go/blob/master/rest/config.go
	restCfg.Timeout = infraAPIClientDefaultTimeout

	mgr, err := ctrl.NewManager(restCfg, ctrl.Options{
		Scheme:             scheme,
		MetricsBindAddress: "0", // disable
		LeaderElection:     false,
	})
	if err != nil {
		return nil, errors.Annotate(err, "create controller-runtime manager failed")
	}

	// we need to start it to use the client.
	// we never stop it now.
	go func() {
		err := mgr.Start(context.TODO())
		if err != nil {
			panic("failed to start mgr: " + err.Error())
		}
	}()

	return &API{
		projectName:    projectName,
		mgr:            mgr,
		client:         mgr.GetClient(),
		InfraAPIConfig: cfg,
	}, nil
}

// GetClusterProviderType gets infra ProviderTpe based on cloud provider and whether the cluster is a dev tier cluster
func GetClusterProviderType(cloudProvider entity.Cloud, free, eksEnabled bool, xaccount bool) (string, error) {
	var providerType string
	switch cloudProvider {
	case entity.Aws:
		if free {
			providerType = ProviderTypeAWSDevTier
		} else {
			if xaccount || eksEnabled {
				providerType = ProviderTypeEKS
			} else {
				providerType = ProviderTypeAWS
			}
		}
	case entity.Gcp:
		if free {
			return "", errors.New("currently do not support gcp dev tier provider")
		}
		providerType = ProviderTypeGCP
	}
	return providerType, nil
}

func (a *API) GetK8sCli() client.Client {
	return a.client
}

// DeleteTenantNetworks delete all the networks belong to the specific tenant.
func (a *API) DeleteTenantNetworks(ctx context.Context, tenantID uint64) error {
	networks := &cloudv1.NetworkList{}
	err := a.client.List(
		ctx,
		networks,
		client.MatchingLabels(map[string]string{
			tenantLabel:      strconv.FormatUint(tenantID, 10),
			projectNameLabel: a.projectName,
		}),
	)
	if err != nil {
		return errors.AddStack(err)
	}

	for _, item := range networks.Items {
		err := a.client.Delete(ctx, &item)
		if err != nil && !apierrors.IsNotFound(err) {
			return errors.AddStack(err)
		}
	}

	return nil
}

// DeleteProjectNetworks delete all the networks belong to the specific project.
func (a *API) DeleteProjectNetworks(ctx context.Context, projectID uint64) error {
	networks := &cloudv1.NetworkList{}
	err := a.client.List(
		ctx,
		networks,
		client.MatchingLabels(map[string]string{
			projectLabel:     strconv.FormatUint(projectID, 10),
			projectNameLabel: a.projectName,
		}),
	)
	if err != nil {
		return errors.AddStack(err)
	}

	for _, item := range networks.Items {
		err := a.client.Delete(ctx, &item)
		if err != nil && !apierrors.IsNotFound(err) {
			return errors.AddStack(err)
		}
	}

	return nil
}

// DeleteProjectNetworkPeers delete all the NetworkPeer belong to the specific project.
func (a *API) DeleteProjectNetworkPeers(ctx context.Context, projectID uint64) error {
	networkPeers := &cloudv1.NetworkPeerList{}
	err := a.client.List(
		ctx,
		networkPeers,
		client.MatchingLabels(map[string]string{
			projectLabel:     strconv.FormatUint(projectID, 10),
			projectNameLabel: a.projectName,
		}),
	)
	if err != nil {
		return errors.AddStack(err)
	}

	for _, item := range networkPeers.Items {
		err := a.client.Delete(ctx, &item)
		if err != nil {
			return errors.AddStack(err)
		}
	}

	return nil
}

// GetNetwork get the network to be used.
func (a *API) GetNetwork(ctx context.Context, providerType string, projectID uint64, region string) (network *cloudv1.Network, err error) {
	ns := Namespace(a.projectName, projectID)
	name := NetworkName(providerType, region)

	network = new(cloudv1.Network)
	err = a.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, network)
	if err != nil {
		return nil, errors.AddStack(err)
	}
	return
}

// UpdateNetwork update the network with the return value of GetNetwork.
func (a *API) UpdateNetwork(ctx context.Context, d *cloudv1.Network) error {
	err := a.client.Update(ctx, d)
	if err != nil {
		return errors.AddStack(err)
	}
	return nil
}

// DeleteSecretForProject deletes the secret for a project.
func (a *API) DeleteSecretForProject(ctx context.Context, projectID uint64, objectName string) error {
	ns := Namespace(a.projectName, projectID)
	secret := corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      objectName,
			Namespace: ns,
		}}
	err := a.client.Delete(ctx, &secret)
	if err != nil {
		return errors.AddStack(err)
	}
	return nil
}

// UpdateCluster update the cluster get by GetCluster().
func (a *API) UpdateCluster(ctx context.Context, c *cloudv1.Cluster) error {
	err := a.client.Update(ctx, c)
	if err != nil {
		return errors.AddStack(err)
	}
	return nil
}

// PatchCluster patch clsuter using MergePatchType
func (a *API) PatchCluster(ctx context.Context, projectID uint64, clusterID uint64, data []byte) error {
	ns := Namespace(a.projectName, projectID)
	name := ClusterName(clusterID)

	cr := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
	}

	err := a.client.Patch(ctx, cr, client.RawPatch(types.MergePatchType, data))

	if err != nil {
		return errors.AddStack(err)
	}

	return nil
}

func (a *API) PatchPublicLoadBalanceSourceRanges(
	ctx context.Context,
	projectID uint64,
	clusterID uint64,
	loadBalancerSourceRanges []string,
) error {
	mp := map[string]interface{}{
		"spec": map[string]interface{}{
			"sqlEndpoints": map[string]interface{}{
				"public": map[string]interface{}{
					"loadBalancerSourceRanges": loadBalancerSourceRanges,
				},
			},
		},
	}

	data, err := json.Marshal(mp)
	if err != nil {
		return errors.AddStack(err)
	}

	err = a.PatchCluster(ctx, projectID, clusterID, data)
	if err != nil {
		return errors.Annotatef(err, "failed to patch with data %s", string(data))
	}

	return nil
}

func (a *API) GetCluster(ctx context.Context, projectID uint64, clusterID uint64) (c *cloudv1.Cluster, err error) {
	ctx, span := Tracer.Start(ctx, "API.GetCluster")
	span.SetAttributes(attribute.String("cluster_id", fmt.Sprintf("%v", clusterID)), attribute.String("project_id", fmt.Sprintf("%v", projectID)))
	defer func() { tracing.EndSpan(span, err) }()
	c = new(cloudv1.Cluster)
	ns := Namespace(a.projectName, projectID)
	name := ClusterName(clusterID)

	err = a.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, c)
	if err != nil {
		return nil, err
	}

	return
}

func GetClusterVersion(cluster *cloudv1.Cluster) (string, error) {
	if cluster.Status.DB == nil || cluster.Status.DB.Version == "" {
		return "", errors.Errorf("Cluster %s does not have a DB Status", cluster.Name)
	}
	return cluster.Status.DB.Version, nil
}

type ClusterStatus struct {
	cloudv1.GardenerStatus
}

type NetworkStatus struct {
	ImportRoleARN       string
	VPCCidrBlock        string
	ServiceAccountEmail string
}

func ConvertNetworkStatus(n *cloudv1.Network) (status *NetworkStatus, err error) {
	gs, err := cloudv1.GardenerNetworkStatusFromNetwork(n)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	status = &NetworkStatus{
		ImportRoleARN:       n.Status.Import.RoleARN,
		VPCCidrBlock:        gs.VPCCidrBlock,
		ServiceAccountEmail: n.Status.IAM.ServiceAccountEmail,
	}

	return
}

func ConvertClusterStatus(providerType string, c *cloudv1.Cluster) (status *ClusterStatus, err error) {
	switch providerType {
	case ProviderTypeAWS:
		s, err := cloudv1.AwsClusterStatusFromCluster(c)
		if err != nil {
			return nil, errors.AddStack(err)
		}

		status = &ClusterStatus{
			GardenerStatus: s.Gardener,
		}
		return status, nil
	case ProviderTypeGCP:
		s, err := cloudv1.GcpClusterStatusFromCluster(c)
		if err != nil {
			return nil, errors.AddStack(err)
		}

		status = &ClusterStatus{
			GardenerStatus: s.Gardener,
		}
		return status, nil
	case ProviderTypeAWSDevTier:
		status = &ClusterStatus{}
		return status, nil
	case ProviderTypeEKS:
		status = &ClusterStatus{}
		return status, nil
	default:
		return nil, errors.Errorf("unknown provider type %q", providerType)
	}
}

func ClusterName(id uint64) string {
	return strconv.FormatUint(id, 10)
}

func NetworkName(providerType string, region string) string {
	return providerType + "-" + region
}

// Namespace return the namespace of infra-api
// projectName is any name of one instance of dbaas deployment can can not be changed.
func Namespace(projectName string, projectID uint64) string {
	return projectName + "-" + strconv.FormatUint(projectID, 10)
}
