package infraapi

import (
	"context"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/pingcap/errors"
	"github.com/samber/lo"
	"github.com/tidbcloud/core-portal/internal/central/config"
	entity "github.com/tidbcloud/core-svc/model"
	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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/clientcmd"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

// 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")

// types for provider type
const (
	LabelInfraProviderType        = "cloud.pingcap.com/provider-type"
	ProviderTypeAWS        string = "aws"
	ProviderTypeEKS        string = "eks"
	ProviderTypeGCP        string = "gcp"
	ProviderTypeGKE        string = "gke"
	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

	// ErrInfraAPIDecommissioned is returned by all API methods because the
	// infra API server has been permanently decommissioned.
	ErrInfraAPIDecommissioned = errors.New("infra API server is decommissioned")
)

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

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

	once.Do(func() {
		// The infra API server has been decommissioned. Return an API instance
		// with no underlying client; all methods return ErrInfraAPIDecommissioned.
		api = &API{
			projectName:    gardenerConfig.GardenerProjectName,
			InfraAPIConfig: config.Global().InfraAPIConfig,
		}
	})
	return api
}

func buildCacheConfig(selector labels.Selector) map[client.Object]cache.ByObject {
	return map[client.Object]cache.ByObject{
		&cloudv1.Cluster{}: {
			Label: selector,
		},
		&cloudv1.Network{}: {
			Label: selector,
		},
	}
}

// NewAPI returns a new API instance.
// nolint: unused
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")
	}

	if cfg.KubeConfigPath == "" {
		return nil, errors.New("kubeConfigPath is not set")
	}
	restCfg, err := clientcmd.BuildConfigFromFlags("", cfg.KubeConfigPath)
	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

	selector := lo.Must1(metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
		MatchExpressions: []metav1.LabelSelectorRequirement{
			{
				Key:      LabelInfraProviderType,
				Operator: metav1.LabelSelectorOpIn,
				Values: []string{
					ProviderTypeAWS,
					ProviderTypeGCP,
					ProviderTypeEKS,
					ProviderTypeGKE,
				},
			},
		},
	}))

	mgr, err := ctrl.NewManager(restCfg, ctrl.Options{
		Scheme: scheme,
		Metrics: metricsserver.Options{
			BindAddress: "0", // disable
		},
		// Only load objects to local cache if it matches the label selector.
		Cache: cache.Options{
			ByObject: buildCacheConfig(selector),
		},
		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, onNewArch bool, xaccount bool) (string, error) {
	var providerType string
	switch cloudProvider {
	case entity.Aws:
		if free {
			providerType = ProviderTypeAWSDevTier
		} else {
			if xaccount || onNewArch {
				providerType = ProviderTypeEKS
			} else {
				providerType = ProviderTypeAWS
			}
		}
	case entity.Gcp:
		if free {
			return "", errors.New("currently do not support gcp dev tier provider")
		}
		if onNewArch {
			providerType = ProviderTypeGKE
		} else {
			providerType = ProviderTypeGCP
		}
	}
	return providerType, nil
}

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

// checkClient returns ErrInfraAPIDecommissioned when the underlying client is
// nil (i.e. the infra API server has been decommissioned).
func (a *API) checkClient() error {
	if a.client == nil {
		return ErrInfraAPIDecommissioned
	}
	return nil
}

// GetNetworkOfCluster get the network of specified cluster
func (a *API) GetNetworkOfCluster(ctx context.Context, projectID uint64, clusterID uint64) (network *cloudv1.Network, err error) {
	if err := a.checkClient(); err != nil {
		return nil, err
	}
	c, err := a.GetCluster(ctx, projectID, clusterID)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	network = new(cloudv1.Network)
	err = a.client.Get(ctx, c.GetNetworkRef(), network)
	if err != nil {
		return nil, errors.AddStack(err)
	}
	return
}

// GetNetworkInProject get the network to be used.
// NOTE: The network project is not always same as cluster project
func (a *API) GetNetworkInProject(ctx context.Context, providerType string, projectID uint64, region string) (network *cloudv1.Network, err error) {
	if err := a.checkClient(); err != nil {
		return nil, err
	}
	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
}

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

func (a *API) GetCluster(ctx context.Context, projectID uint64, clusterID uint64) (c *cloudv1.Cluster, err error) {
	if err := a.checkClient(); err != nil {
		return nil, err
	}
	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
}

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)
}
