package infraapi

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

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	runtimeutil "github.com/pingcap/dbaas/central/runtime/util"
	"github.com/pingcap/dbaas/pkg/constants"
	"github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"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 (
	dedicated = "dedicated"
)

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

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
	CreateTicdcIgnoreExist(ctx context.Context, projectID uint64, clusterID uint64, cr *cloudv1.TiCDC) error
	UpdateTicdc(ctx context.Context, d *cloudv1.TiCDC) error
	DeleteTicdcIfExist(ctx context.Context, projectID uint64, clusterID uint64) error
	GetTicdc(ctx context.Context, projectID, clusterID uint64) (*cloudv1.TiCDC, error)
	GetNetwork(ctx context.Context, providerType string, projectID uint64, region string) (network *cloudv1.Network, err error)
	CreateAwsNetworkPeer(ctx context.Context, tenantID uint64, projectID uint64, providerType string, shootRegion string, customRegion string, peerAccountId string, customerVpcId string, customerVpcCidr string, peeringType entity.PeeringType) error
	CreateGcpVpcPeer(ctx context.Context, providerType string, projectID uint64, tenantID uint64, shootRegion string, customerProjectId string, customerVpcNetwork string, customRegion string, peeringType entity.PeeringType) error
}

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

	once.Do(func() {
		if config.Global().InfraAPIConfig.NeedInit() {
			var err error
			// using the gardenerProjectName as the identifier of different central deployment
			api, err = NewAPI(gardenerConfig.GardenerProjectName, baseKubeCli, config.Global().InfraAPIConfig)
			if err != nil {
				panic("initialize the infra API failed")
			}
		} else {
			api = &API{
				InfraAPIConfig: config.Global().InfraAPIConfig,
			}
		}
	})
	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, baseKubeCli *kubernetes.Clientset, 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(baseKubeCli)
	}
	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, xaccount bool) (string, error) {
	var providerType string
	switch cloudProvider {
	case entity.Aws:
		if free {
			providerType = ProviderTypeAWSDevTier
		} else {
			if xaccount {
				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
}

// 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) DeleteClusterIfExist(ctx context.Context, projectID uint64, clusterID uint64) (err error) {
	ctx, span := Tracer.Start(ctx, "API.DeleteClusterIfExist")
	defer func() { tracing.EndSpan(span, err) }()
	ns := Namespace(a.projectName, projectID)
	name := ClusterName(clusterID)
	c := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name},
	}

	err = a.client.Delete(ctx, c)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return errors.AddStack(err)
	}
	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
	*cloudv1.KubernetesStatus
}

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,
			KubernetesStatus: s.Kubernetes,
		}
		return status, nil
	case ProviderTypeGCP:
		s, err := cloudv1.GcpClusterStatusFromCluster(c)
		if err != nil {
			return nil, errors.AddStack(err)
		}

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

		status = &ClusterStatus{
			KubernetesStatus: s.Kubernetes,
		}
		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)
}

// AddBillingLabelWhenClusterNormal add labels need for billing when cluster become normal after we create it.
func AddBillingLabelWhenClusterNormal(cr *cloudv1.Cluster) {
	if cr.Labels == nil {
		cr.Labels = make(map[string]string)
	}

	cr.Labels[TagPrefix+constants.TiDBClusterStatusFromCentral] = string(constants.TidbClusterNormal)
}
