package dataplane

import (
	"context"
	"fmt"
	"os"

	"helm.sh/helm/v3/pkg/action"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/util"

	corev1 "k8s.io/api/core/v1"
)

var _ EnvProvider = &provider{}

type provider struct {
	name string
	k    *cloudv1.Kubernetes
	m    K8sClientManager

	region        string
	awsRoleName   string
	awsExternalID string

	// you can also set kubeconfigData in the .spec.Kubernetes.kubeConfigSecretRef
	kubeconfigData []byte

	dryRun bool
}

// newEnvProvider creates a EnvProvider.
func newEnvProvider(
	m K8sClientManager,
	c *cloudv1.Cluster,
	assumeRoleName, assumeRoleExternalID string,
	kubeconfigData []byte,
	dryRun bool,
) (EnvProvider, error) {
	if err := validateKubernetes(&c.Spec.Kubernetes, kubeconfigData); err != nil {
		return nil, err
	}

	p := &provider{
		m:    m,
		k:    c.Spec.Kubernetes.DeepCopy(),
		name: c.Name,

		region:         c.GetRegion(),
		awsRoleName:    assumeRoleName,
		awsExternalID:  assumeRoleExternalID,
		kubeconfigData: kubeconfigData,

		dryRun: dryRun,
	}

	if m.IsRegistered(c.Name) {
		return p, nil
	}

	// validate cfg
	cfg, err := p.newRESTConfig()
	if err != nil {
		return nil, err
	}

	if err := m.Register(c.Name, cfg); err != nil {
		return nil, err
	}

	return p, nil
}

func NewWithKubenets(
	m K8sClientManager,
	clusterID string,
	k *cloudv1.Kubernetes,
	region string,
	assumeRoleName, assumeRoleExternalID string,
	kubeconfigData []byte,
) (EnvProvider, error) {
	if err := validateKubernetes(k, kubeconfigData); err != nil {
		return nil, err
	}

	p := &provider{
		m:    m,
		k:    k.DeepCopy(),
		name: clusterID,

		region:         region,
		awsRoleName:    assumeRoleName,
		awsExternalID:  assumeRoleExternalID,
		kubeconfigData: kubeconfigData,
	}

	if m.IsRegistered(clusterID) {
		return p, nil
	}

	// validate cfg
	cfg, err := p.newRESTConfig()
	if err != nil {
		return nil, err
	}

	if err := m.Register(clusterID, cfg); err != nil {
		return nil, err
	}

	return p, nil
}

func (p *provider) GetClient() client.Client {
	if p.dryRun {
		return client.NewDryRunClient(p.m.GetClient(p.name))
	}
	return p.m.GetClient(p.name)
}

func (p *provider) GetHelmConfig() *action.Configuration {
	return p.m.GetHelmConfig(p.name)
}

func (p *provider) GetRestConfig() *rest.Config {
	return p.m.GetRestConfig(p.name)
}

func (p *provider) GetRegion() string {
	return p.region
}

func (p *provider) GetClusterName() string {
	return p.k.Name
}

func (p *provider) GetClusterEndpoint() string {
	return p.k.Endpoint
}

func (p *provider) GetClusterCA() []byte {
	return p.k.CAData
}

func (p *provider) GetAccount() string {
	return p.k.Account
}

func (p *provider) GetNetwork() string {
	return p.k.Network
}

func (p *provider) AWS() AWSInterface {
	if p.k.Cloud.AWS != nil {
		return p
	}

	panic("cannot call aws interface for another cloud provider")
}

func (p *provider) GCP() GCPInterface {
	if p.k.Cloud.GCP != nil {
		return p
	}

	panic("cannot call gcp interface for another cloud provider")
}

func (p *provider) GetEKSSecurityGroup() string {
	// p.k.Cloud.AWS has been checked when AWS() is called
	return p.k.Cloud.AWS.EKSSecurityGroup
}

func (p *provider) GetNodeSubnets(zones ...string) []string {
	if p.k.Cloud.AWS == nil {
		return nil
	}

	subnets := []string{}
	if len(zones) <= 0 {
		return subnets
	}

	_, subnets = p.k.Cloud.AWS.SubnetGroups.SubnetsFilterByGroupNameOrZones(
		[]cloudv1.AWSSubnetGroupName{cloudv1.AWSSubnetGroupNodes, cloudv1.AWSSubnetGroupSecondaryCIDR},
		zones)

	return subnets
}

func (p *provider) newRESTConfig() (*rest.Config, error) {
	// if kubeconfigData directly set, use it
	if p.kubeconfigData != nil {
		return NewRESTConfigForKubeConfig(p.kubeconfigData)
	}

	switch p.k.Provider {
	case cloudv1.ProviderTypeEKS:
		if p.awsRoleName == "" {
			return nil, fmt.Errorf("awsRoleName is not set")
		}
		roleARN := util.GetRoleARN(p.k.Account, p.awsRoleName)
		return NewRESTConfigForEKS(p.k.Name, p.k.Endpoint, p.region, roleARN, p.awsExternalID, p.k.CAData)
	case cloudv1.ProviderTypeGKE:
		return NewRESTConfigForGKE(p.k.Name, p.k.Endpoint, p.k.CAData)
	case cloudv1.ProviderTypeAKS, consts.ProviderTypeKinD:
		return nil, fmt.Errorf("AKS and KinD should use kubeconfigData to build rest config but it's not set")
	}

	return nil, fmt.Errorf("unknown provider: %s", p.k.Provider)
}

func validateKubernetes(k *cloudv1.Kubernetes, kubeconfigData []byte) error {
	errList := []error{}

	if k.Name == "" {
		errList = append(errList, fmt.Errorf("name is not set"))
	}
	if k.Endpoint == "" {
		errList = append(errList, fmt.Errorf("endpoint is not set"))
	}
	if k.Account == "" {
		errList = append(errList, fmt.Errorf("account is not set"))
	}

	switch k.Provider {
	case cloudv1.ProviderTypeEKS:
		if k.Cloud.AWS == nil {
			errList = append(errList, fmt.Errorf("cloud.aws should be set for provider %s", k.Provider))
		} else {
			groups := []cloudv1.AWSSubnetGroupName{
				cloudv1.AWSSubnetGroupNodes,
				cloudv1.AWSSubnetGroupPublicNLB,
				cloudv1.AWSSubnetGroupPrivateNLB,
			}
			isSet := make([]bool, 3)
			for _, subnetGroup := range k.Cloud.AWS.SubnetGroups {
				for i, g := range groups {
					if subnetGroup.Name != g {
						continue
					}
					if isSet[i] {
						errList = append(errList, fmt.Errorf("aws subnet group %s is set twice", g))
					} else {
						isSet[i] = true
					}
				}
			}
			for i, ok := range isSet {
				if !ok {
					errList = append(errList, fmt.Errorf("aws subnet group %s is not set", groups[i]))
				}
			}
		}
	case cloudv1.ProviderTypeGKE:
		if k.Cloud.GCP == nil {
			errList = append(errList, fmt.Errorf("cloud.gcp should be set for provider %s", k.Provider))
		}
	case cloudv1.ProviderTypeAKS:
		if k.KubeConfigSecretRef == nil && string(kubeconfigData) == "" {
			errList = append(errList, fmt.Errorf("kubeConfigSecretRef and kubeConfigContent is not set, can't build aks dataplane client"))
		}
	case consts.ProviderTypeACK:
		if string(kubeconfigData) == "" {
			errList = append(errList, fmt.Errorf("kubeConfigContent is not set, can't build kind dataplane client"))
		}
	case consts.ProviderTypeKinD:
		if string(kubeconfigData) == "" {
			errList = append(errList, fmt.Errorf("kubeConfigContent is not set, can't build kind dataplane client"))
		}
	default:
		errList = append(errList, fmt.Errorf("unknown provider: %s", k.Provider))
	}

	return errors.NewAggregate(errList)
}

func GetDataplaneKubeConfigFromMetaSecret(ctx context.Context, cli client.Client, kubeConfigSecretRef *corev1.SecretReference) ([]byte, error) {
	secret := &corev1.Secret{}
	if err := cli.Get(ctx, client.ObjectKey{
		Namespace: kubeConfigSecretRef.Namespace,
		Name:      kubeConfigSecretRef.Name,
	}, secret); err != nil {
		return nil, err
	}

	kubeconfigData, ok := secret.Data["kubeconfig"]
	if !ok {
		return nil, fmt.Errorf("kubeconfig not found in secret %s/%s", kubeConfigSecretRef.Namespace, kubeConfigSecretRef.Name)
	}

	return kubeconfigData, nil
}

func GetDataplaneKubeConfigFromFile(ctx context.Context, path string) ([]byte, error) {
	return os.ReadFile(path)
}

var _ EnvProvider = &NopEnvProvider{}

type NopEnvProvider struct{}

func (p *NopEnvProvider) GetClient() client.Client {
	return nil
}
func (p *NopEnvProvider) GetHelmConfig() *action.Configuration {
	return nil
}
func (p *NopEnvProvider) GetRestConfig() *rest.Config {
	return nil
}
func (p *NopEnvProvider) GetRegion() string {
	return ""
}

func (p *NopEnvProvider) GetClusterName() string {
	return ""
}
func (p *NopEnvProvider) GetClusterEndpoint() string {
	return ""
}
func (p *NopEnvProvider) GetClusterCA() []byte {
	return nil
}

func (p *NopEnvProvider) GetAccount() string {
	return ""
}
func (p *NopEnvProvider) GetNetwork() string {
	return ""
}

func (p *NopEnvProvider) AWS() AWSInterface {
	return nil
}
func (p *NopEnvProvider) GCP() GCPInterface {
	return nil
}
