package controllers

import (
	"context"
	"fmt"

	corev1 "k8s.io/api/core/v1"
	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	pkgconfig "github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	eksconsts "github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/utils"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/cidr"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/types"
)

const (
	VPCNameField           = ".spec.vpcRef.name"
	APPBoxNameField        = ".spec.appBoxRef.name"
	KubernetesVersionField = ".spec.kubernetesConfig.version"

	// VNetNameLimit is the limit of network name.
	// At present, subnet name is `{vnetName}-node`, so we need to limit the length of vnet name.
	VNetNameLimit = 57
)

func GetNetworkSpec(ctx context.Context, cli client.Client, cluster *cloudv1.Cluster) (*cloudv1.Network, error) {
	network := new(cloudv1.Network)
	networkKey := cluster.GetNetworkRef()
	if err := cli.Get(ctx, networkKey, network); err != nil {
		return nil, err
	}
	return network, nil
}

func GetMaxConcurrentReconciles(cfg config.RootConfig, name string) int {
	maxConcurrentReconciles := eksconsts.DefaultMaxConcurrentReconciles
	if v, ok := cfg.MaxConcurrentReconciles[name]; ok {
		maxConcurrentReconciles = v
	}
	return maxConcurrentReconciles
}

var (
	bindingMode   = storagev1.VolumeBindingWaitForFirstConsumer
	reclaimPolicy = corev1.PersistentVolumeReclaimDelete
)

// ConvertAWSStorageClass converts the AWS cloudv1.StorageClass to storagev1.StorageClass
func ConvertAWSStorageClass(sc cloudv1.StorageClass) *storagev1.StorageClass {
	res := &storagev1.StorageClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: sc.Name,
		},
		VolumeBindingMode:    &bindingMode,
		AllowVolumeExpansion: pointer.Bool(true),
		ReclaimPolicy:        &reclaimPolicy,
		Provisioner:          "ebs.csi.aws.com",
		Parameters: map[string]string{
			"csi.storage.k8s.io/fstype": "ext4",
			"type":                      sc.Type,
			"iops":                      fmt.Sprint(sc.IOPS),
			"throughput":                fmt.Sprint(sc.Throughput),
			"encrypted":                 "true",
			// Pass the tags to the persistent volumes
			// See https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/TAGGING.md for more information
			"tagSpecification": fmt.Sprintf("%s=%s", consts.TagKeyCrossplaneVendor, consts.TagValCrossplaneVendor),
		},
		MountOptions: []string{"nodelalloc,noatime"},
	}
	if sc.Encryption != nil && sc.Encryption.AwsKMSARN != nil {
		res.Parameters["kmsKeyId"] = *sc.Encryption.AwsKMSARN
	}
	return res
}

func EnsureChartDeploymentReady(ctx context.Context, getter k8s.ClientGetter, key types.NamespacedName) error {
	cli, err := getter.GetClient(ctx)
	if err != nil {
		return err
	}

	app := new(appsv1.Deployment)
	if err := cli.Get(ctx, key, app); err != nil {
		return err
	}

	if utils.IsStatusConditionTrue(app.Status.Conditions, appsv1.DeploymentAvailable) {
		return nil
	}
	return fmt.Errorf("chart deployment %s not ready", key.String())
}

var _ helm.Applier = &MockHelmApplier{}

type MockHelmApplier struct {
	err   error
	chart helm.Package
}

func (m *MockHelmApplier) ExpectedApplyErr(err error) *MockHelmApplier {
	m.err = err
	return m
}

func (m *MockHelmApplier) Apply(ctx context.Context, cli k8s.ClientGetter, chart helm.Package, _ ...helm.ApplyOption) error {
	m.chart = chart
	return m.err
}

func (m *MockHelmApplier) GetAppliedChart() helm.Package {
	return m.chart
}

// GetResourceGroupName returns the resource group name for the VPC.
// The resource group name is the unique identifier of the VPC.
// The resource group name is used to manage the resource group in Azure.
func GetResourceGroupName(vpc *v1alpha1.VPC) string {
	return namer.UniqueIdentifier(vpc.ObjectMeta, namer.WithLimit(VNetNameLimit))
}

func GetUnacceptedVPCEndpoints(connsStatus []v1alpha1.ConnectionStatus) []string {
	eps := []string{}
	for _, conn := range connsStatus {
		if conn.Phase == v1alpha1.ConnectionPhasePending {
			eps = append(eps, conn.Endpoint)
		}
	}

	return eps
}

func GetGKECIDRDispatcher(vpc *v1alpha1.VPC, config rsconfig.RootConfig) (*cidr.GKECIDRDispatcher, error) {
	tenantID := vpc.GetLabels()[v1alpha1.TagTenantKey]
	projectID := vpc.GetLabels()[v1alpha1.TagProjectKey]

	var opts []cidr.Option
	if config.FeatureGates != nil &&
		pkgconfig.FeatureEnabled(config.FeatureGates, pkgconfig.AllocateMoreNATs, tenantID, projectID) {
		opts = append(opts, cidr.AllocateMoreNATs())
	}

	cidrDispatcher, err := cidr.NewGKECIDRDispatcher(vpc.Spec.CIDR, opts...)
	if err != nil {
		return nil, err
	}
	if err = cidrDispatcher.Allocate(); err != nil {
		return nil, err
	}
	if v1alpha1.IsOriginatedFromGardener(vpc) &&
		config.FeatureGates != nil &&
		pkgconfig.FeatureEnabled(config.FeatureGates, pkgconfig.SetGKECIDRPlaceholder, tenantID, projectID) {
		if err := cidrDispatcher.SetPrefixPlaceholder(); err != nil {
			return nil, err
		}
	}

	return cidrDispatcher, nil
}
