package common

import (
	"context"
	"encoding/base64"
	"fmt"
	"reflect"
	"strings"

	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

// This task is only used for save necessary info into cluster CR
// for compatibility.
// Only run this task for cluster CR with spec.networkName.
type KubernetesInfoSaver struct {
	*task.BaseTask
}

func NewKubernetesInfoSaver(ctx *task.Context) *KubernetesInfoSaver {
	return &KubernetesInfoSaver{
		BaseTask: &task.BaseTask{
			Context: ctx,
		},
	}
}

func (t *KubernetesInfoSaver) Name() string {
	return "KubernetesInfoSaver"
}

func (t *KubernetesInfoSaver) Create(ctx context.Context) deploy.Result {
	if t.IsLegacy() {
		if t.Cluster.Spec.Kubernetes.Cloud.AWS != nil {
			roleArn := t.GetAWSNodeRoleARN()
			if t.Cluster.Spec.Kubernetes.Cloud.AWS.AdminNodeRole == "" {
				// NOTE: use legacy node role as aws admin node role
				t.Cluster.Spec.Kubernetes.Cloud.AWS.AdminNodeRole = roleArn
			}

			externalID := t.GetAWSAuditLogRoleExternalID()
			t.Cluster.Status.NodeIdentity = &cloudv1.NodeIdentity{
				AWS: &cloudv1.AWSIdentity{
					RoleARN:    roleArn,
					ExternalID: externalID,
				},
			}
		}

		if t.Cluster.Spec.Kubernetes.Cloud.GCP != nil {
			sa := t.GetGCPNodePoolServiceAccountEmail()
			if t.Cluster.Spec.Kubernetes.Cloud.GCP.AdminNodeSa == "" {
				// NOTE: use legacy sa as gcp admin sa
				t.Cluster.Spec.Kubernetes.Cloud.GCP.AdminNodeSa = sa
			}

			t.Cluster.Status.NodeIdentity = &cloudv1.NodeIdentity{
				GCP: &cloudv1.GCPIdentity{
					ServiceAccount: sa,
				},
			}
		}
	}

	if t.IsRegionalManaged() {
		t.EventRecorder.Event(t.Cluster, corev1.EventTypeNormal, "SkipClusterWithoutNetworkName", "no need to save info for cluster without network name")
		return deploy.Done()
	}

	t.Cluster.Spec.Kubernetes.Provider = t.Config.Provider

	switch t.Config.Provider {
	case consts.ProviderTypeEKS:
		if err := t.saveEKSInfo(ctx); err != nil {
			t.handleError(err)
		} else {
			t.EventRecorder.Event(t.Cluster, corev1.EventTypeNormal, "SaveInfoSuccessfully", "successfully save info into cluster in eks")
		}
	case consts.ProviderTypeGKE:
		if err := t.saveGKEInfo(ctx); err != nil {
			t.handleError(err)
		} else {
			t.EventRecorder.Event(t.Cluster, corev1.EventTypeNormal, "SaveInfoSuccessfully", "successfully save info into cluster in gke")
		}
	}

	return deploy.Done()
}

func (t *KubernetesInfoSaver) saveEKSInfo(ctx context.Context) error {
	// set aws account
	t.Cluster.Spec.Kubernetes.Account = t.EnvProvider.GetAccount()

	name := namer.LegacyBuildClusterNameByNetwork(t.LegacyNetwork)
	eks := &eksv1beta1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(eks), eks); err != nil {
		return err
	}

	eksName := meta.GetExternalName(eks)

	if eksName == "" || eksName != name {
		return fmt.Errorf("eks %s is not ready or real eks name is not same as crossplane's name(%s)", eks.Name, name)
	}

	vpcID := eks.Status.AtProvider.ResourcesVpcConfig.VpcID
	if vpcID == "" {
		return fmt.Errorf("eks %s vpc is not ready", eks.Name)
	}
	// add guard to make sure that vpc id in eks status is same as EKSNetworkSpec.VpcId
	if vpcID != t.EnvProvider.GetNetwork() {
		return fmt.Errorf("eks %s vpc is not as expected, current %s, expected %s", eks.Name, vpcID, t.EnvProvider.GetNetwork())
	}

	if eks.Status.AtProvider.ResourcesVpcConfig.ClusterSecurityGroupID == "" {
		return fmt.Errorf("eks %s sg is not found", eks.Name)
	}

	if eks.Status.AtProvider.Endpoint == "" {
		return fmt.Errorf("eks %s is not ready", eks.Name)
	}

	if eks.Status.AtProvider.CertificateAuthorityData == "" {
		return fmt.Errorf("eks %s client ca is empty", eks.Name)
	}

	t.Cluster.Spec.Kubernetes.Network = vpcID
	t.Cluster.Spec.Kubernetes.Name = eksName

	// set eks endpoint and ca
	t.Cluster.Spec.Kubernetes.Endpoint = eks.Status.AtProvider.Endpoint
	ca, err := base64.StdEncoding.DecodeString(eks.Status.AtProvider.CertificateAuthorityData)
	if err != nil {
		return err
	}
	t.Cluster.Spec.Kubernetes.CAData = ca

	subnetGroups := []cloudv1.AWSSubnetGroup{
		{
			Name: cloudv1.AWSSubnetGroupNodes,
		},
		{
			Name: cloudv1.AWSSubnetGroupPublicNLB,
		},
		{
			Name: cloudv1.AWSSubnetGroupPrivateNLB,
		},
	}

	nodeSubnets := []string{}
	for _, zone := range t.Zones() {
		nodesSubnet, err := t.getSubnet(ctx, cloudv1.AWSSubnetGroupNodes, name, zone)
		if err != nil {
			return err
		}
		subnetGroups[0].Subnets = append(subnetGroups[0].Subnets, *nodesSubnet)
		nodeSubnets = append(nodeSubnets, nodesSubnet.SubnetID)

		publicSubnet, err := t.getSubnet(ctx, cloudv1.AWSSubnetGroupPublicNLB, name, zone)
		if err != nil {
			return err
		}
		subnetGroups[1].Subnets = append(subnetGroups[1].Subnets, *publicSubnet)
		privateSubnet, err := t.getSubnet(ctx, cloudv1.AWSSubnetGroupPrivateNLB, name, zone)
		if err != nil {
			return err
		}
		subnetGroups[2].Subnets = append(subnetGroups[2].Subnets, *privateSubnet)
	}

	// add guard to prevent wrong node subnets generation
	if !reflect.DeepEqual(nodeSubnets, t.EnvProvider.AWS().GetNodeSubnets(t.Zones()...)) {
		return fmt.Errorf("node subnets are not as expected, current %s, expected %s", nodeSubnets, t.EnvProvider.AWS().GetNodeSubnets(t.Zones()...))
	}

	if t.Cluster.Spec.Kubernetes.Cloud.AWS == nil {
		t.Cluster.Spec.Kubernetes.Cloud.AWS = &cloudv1.CloudAWS{}
	}
	t.Cluster.Spec.Kubernetes.Cloud.AWS.EKSSecurityGroup = eks.Status.AtProvider.ResourcesVpcConfig.ClusterSecurityGroupID
	t.Cluster.Spec.Kubernetes.Cloud.AWS.SubnetGroups = subnetGroups

	return nil
}

func (t *KubernetesInfoSaver) getSubnet(ctx context.Context, groupName cloudv1.AWSSubnetGroupName, vpcName string, zone string) (*cloudv1.AWSSubnet, error) {
	var subnetName string
	switch groupName {
	case cloudv1.AWSSubnetGroupNodes:
		subnetName = namer.NodesSubnetName(vpcName, zone)
	case cloudv1.AWSSubnetGroupPrivateNLB:
		subnetName = namer.PrivateSubnetName(vpcName, zone)
	case cloudv1.AWSSubnetGroupPublicNLB:
		subnetName = namer.PublicSubnetName(vpcName, zone)
	default:
		return nil, fmt.Errorf("invalid aws subnet group: %s", groupName)
	}
	subnet := &ec2v1beta1.Subnet{
		ObjectMeta: metav1.ObjectMeta{
			Name: subnetName,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(subnet), subnet); err != nil {
		return nil, err
	}

	if subnet.Status.AtProvider.SubnetID == "" {
		return nil, fmt.Errorf("subnet %s is not ready", subnetName)
	}
	if subnet.Spec.ForProvider.AvailabilityZone == nil || *subnet.Spec.ForProvider.AvailabilityZone == "" {
		return nil, fmt.Errorf("az for subnet %s is not found", subnetName)
	}
	return &cloudv1.AWSSubnet{
		SubnetID: subnet.Status.AtProvider.SubnetID,
		Zone:     *subnet.Spec.ForProvider.AvailabilityZone,
	}, nil
}

func (t *KubernetesInfoSaver) saveGKEInfo(ctx context.Context) error {
	// set gcp project
	t.Cluster.Spec.Kubernetes.Account = t.EnvProvider.GetAccount()

	name := namer.LegacyBuildClusterNameByNetwork(t.LegacyNetwork)
	gke := &gkev1beta2.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(gke), gke); err != nil {
		return err
	}

	gkeName := meta.GetExternalName(gke)
	if gkeName == "" || gkeName != name {
		return fmt.Errorf("gke %s is not ready or real gke name is not same as crossplane's name(%s)", gke.Name, name)
	}

	if gke.Status.AtProvider.NetworkConfig.Network == "" {
		return fmt.Errorf("gke %s network is not ready", gke.Name)
	}

	if gke.Status.AtProvider.Endpoint == "" {
		return fmt.Errorf("gke %s endpoint is not ready", gke.Name)
	}

	// add guard to avoid network in gke status is not same as GKENetworkSpec.Network
	networkName := getBaseName(gke.Status.AtProvider.NetworkConfig.Network)
	if networkName != t.EnvProvider.GetNetwork() {
		return fmt.Errorf("gke %s network is not as expected, current %s, expected %s", gkeName, networkName, t.EnvProvider.GetNetwork())
	}

	t.Cluster.Spec.Kubernetes.Network = networkName
	t.Cluster.Spec.Kubernetes.Name = gkeName

	// set eks endpoint and ca
	t.Cluster.Spec.Kubernetes.Endpoint = "https://" + gke.Status.AtProvider.Endpoint
	// TODO(liubo02): fetch ca from gcp api
	// ca, err := base64.StdEncoding.DecodeString(gke.Status.AtProvider.CertificateAuthorityData)
	// if err != nil {
	// 	return err
	// }
	// t.Cluster.Spec.Kubernetes.CAData = ca

	if t.Cluster.Spec.Kubernetes.Cloud.GCP == nil {
		t.Cluster.Spec.Kubernetes.Cloud.GCP = &cloudv1.CloudGCP{}
	}
	t.Cluster.Spec.Kubernetes.Cloud.GCP.NodeZones = t.Zones()

	return nil
}

// input: projects/xxx/global/networks/yyy
// output: yyy
func getBaseName(s string) string {
	ss := strings.Split(s, "/")
	return ss[len(ss)-1]
}

func (t *KubernetesInfoSaver) handleError(err error) {
	t.EventRecorder.Eventf(t.Cluster, corev1.EventTypeWarning, "FailedToSaveInfo", "cannot save kubernetes info into cluster: %v", err)
}
