package network

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"

	"cloud.google.com/go/container/apiv1/containerpb"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"

	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionalapiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/clustercli"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/gcpcontainer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/message"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/approute"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/internet"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
)

type NetworkService struct {
	kubecli    client.Client
	vpcrw      vpc.VPCReaderWriter
	appboxrw   appbox.APPBoxReaderWriter
	approuter  approute.APPRouteReader
	internetrw internet.InternetReaderWriter
	config     rsconfig.RootConfig
	ccli       *clustercli.ClusterCli
	gkecli     gcpcontainer.ClusterGetter
}

func NewNetworkService(
	cli client.Client,
	config rsconfig.RootConfig,
) *NetworkService {
	return &NetworkService{
		kubecli:    cli,
		vpcrw:      vpc.NewVPCReaderWriter(cli),
		appboxrw:   appbox.NewAPPBoxReaderWriter(cli),
		approuter:  approute.NewAPPRouteReader(cli),
		internetrw: internet.NewInternetReaderWriter(cli),
		config:     config,
		ccli:       clustercli.NewClusterCli(config.ClusterControlServiceEndpoint),
	}
}

func (s *NetworkService) WithGKEClient(gkecli gcpcontainer.ClusterGetter) {
	s.gkecli = gkecli
}

func (s *NetworkService) GetNetwork(ctx context.Context, req *regionalapiv1.GetCompatNetworkRequest) (*regionalapiv1.CompatNetwork, error) {
	vpcList := &v1alpha1.VPCList{}
	err := s.vpcrw.List(ctx, vpcList, client.MatchingLabels{
		v1alpha1.VPCIDKey: req.VpcId,
	})
	if err != nil {
		log.Error("failed to list vpcs", zap.Error(err))
		return nil, bizerr.New(bizerr.K8SClientError, "failed to list vpcs")
	}
	if len(vpcList.Items) == 0 {
		return nil, bizerr.New(bizerr.ResourceNotFound, "vpc not found")
	}
	if len(vpcList.Items) != 1 {
		return nil, bizerr.New(bizerr.K8SClientError, "vpc record is not unique")
	}
	targetVPC := vpcList.Items[0]

	internetList := &v1alpha1.InternetList{}
	if err = s.internetrw.List(ctx, internetList, client.MatchingLabels{
		v1alpha1.VPCNameKey: targetVPC.Name,
	}); err != nil {
		log.Error("failed to list internets", zap.Error(err))
		return nil, bizerr.New(bizerr.K8SClientError, "failed to list internets")
	}
	if len(internetList.Items) == 0 {
		return nil, bizerr.New(bizerr.ResourceNotFound, "internet not found")
	}
	var targetInternet v1alpha1.Internet
	for _, internet := range internetList.Items {
		// only consider active, ready for delete internet and deleting internet
		if internet.Status.Phase == v1alpha1.InternetPhaseActive ||
			internet.Status.Phase == v1alpha1.InternetPhaseReadyForDelete {
			targetInternet = internet
			break
		}
	}

	if targetInternet.Name == "" {
		return nil, bizerr.New(bizerr.ResourceNotFound, "internet not found")
	}

	appboxList := &v1alpha1.APPBoxList{}
	if err := s.appboxrw.List(ctx, appboxList, client.MatchingLabels{
		v1alpha1.VPCNameKey: targetVPC.Name,
	}); err != nil {
		log.Error("failed to list appboxes", zap.Error(err))
		return nil, bizerr.New(bizerr.K8SClientError, "failed to list appboxes")
	}
	if len(appboxList.Items) == 0 {
		return nil, bizerr.New(bizerr.ResourceNotFound, "appbox not found")
	}
	var targetAPPBox v1alpha1.APPBox
	for _, appbox := range appboxList.Items {
		// only consider active, ready for delete appbox
		if appbox.Status.Phase == v1alpha1.APPBoxPhaseActive ||
			appbox.Status.Phase == v1alpha1.APPBoxPhaseReadyForDelete {
			targetAPPBox = appbox
			break
		}
	}

	if targetAPPBox.Name == "" {
		return nil, bizerr.New(bizerr.ResourceNotFound, "appbox not found")
	}

	switch targetVPC.Spec.Provider {
	case consts.ProviderTypeEKS:
		cluster := &eksv1beta1.Cluster{}
		if err := s.kubecli.Get(ctx, client.ObjectKey{Name: targetAPPBox.Status.KubernetesClusterName}, cluster); err != nil {
			log.Error("failed to get eks cluster", zap.Error(err))
			return nil, bizerr.New(bizerr.K8SClientError, "failed to get eks cluster")
		}
		network, err := genEKSCompatNetwork(&targetVPC, &targetAPPBox, cluster)
		if err != nil {
			log.Error("failed to generate eks compat network", zap.Error(err))
			return nil, bizerr.New(bizerr.K8SClientError, "failed to generate eks compat network")
		}
		network.ExternalIps = targetInternet.Status.ExternalIPs
		// we need to overwrite the extarnal id for migrated appbox
		if targetAPPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
			externalID, err := s.getExternalID(ctx, &targetAPPBox)
			// if error occuer, do not fail the api, just log the error, otherwise overwrite the external id.
			if err != nil {
				log.Error("failed to get external id", zap.Error(err))
			} else {
				// this is a defense logic
				if network.Iam == nil {
					network.Iam = new(regionalapiv1.CompatNetwork_IAM)
					network.Iam.AccountId = targetVPC.Spec.AccountRef.AccountID
				}
				network.Iam.ExternalId = externalID
			}
		}
		return network, nil
	case consts.ProviderTypeGKE:
		cluster := &gkev1beta2.Cluster{}
		if err := s.kubecli.Get(ctx, client.ObjectKey{Name: targetAPPBox.Status.KubernetesClusterName}, cluster); err != nil {
			log.Error("failed to get gke cluster", zap.Error(err))
			return nil, bizerr.New(bizerr.K8SClientError, "failed to get gke cluster")
		}
		network, err := genGKECompatNetwork(s.gkecli, &targetVPC, &targetAPPBox, cluster)
		if err != nil {
			log.Error("failed to generate gke compat network", zap.Error(err))
			return nil, bizerr.New(bizerr.K8SClientError, "failed to generate gke compat network")
		}
		network.ExternalIps = targetInternet.Status.ExternalIPs
		return network, nil
	default:
		return nil, bizerr.New(bizerr.ParameterInvalid, "provider not supported")
	}
}

func genEKSCompatNetwork(vpc *v1alpha1.VPC, appbox *v1alpha1.APPBox, cluster *eksv1beta1.Cluster) (*regionalapiv1.CompatNetwork, error) {
	network := &regionalapiv1.CompatNetwork{
		K8SName:          appbox.Status.KubernetesClusterName,
		K8SVersion:       appbox.Spec.KubernetesConfig.Version,
		K8SEndpoint:      cluster.Status.AtProvider.Endpoint,
		VpcId:            vpc.GetLabels()[v1alpha1.VPCIDKey],
		ProxyBaseUrl:     appbox.Status.ProxyBaseUrl,
		NodeRole:         appbox.Status.IAM.NodeRoleARN,
		DataflowAgentUrl: appbox.Status.DataflowAgentUrl,
		Info: &regionalapiv1.CompatNetwork_Aws_{
			Aws: &regionalapiv1.CompatNetwork_Aws{
				NodeSubnets:    make([]*regionalapiv1.CompatNetwork_Aws_NodeSubnet, 0),
				PrivateSubnets: make([]*regionalapiv1.CompatNetwork_Aws_NodeSubnet, 0),
			},
		},
		Iam: &regionalapiv1.CompatNetwork_IAM{
			AccountId: vpc.Spec.AccountRef.AccountID,
			// use project id as external id
			ExternalId: appbox.GetLabels()[v1alpha1.TagProjectKey],
		},
		TenantId: vpc.GetLabels()[v1alpha1.TagTenantKey],
	}
	for _, subnet := range vpc.Status.NodeSubnets {
		network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.NodeSubnets = append(network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.NodeSubnets, &regionalapiv1.CompatNetwork_Aws_NodeSubnet{
			SubnetId:           subnet.SubnetID,
			AvailabilityZone:   subnet.AvailabilityZone,
			AvailabilityZoneId: subnet.AvailabilityZoneID,
		})
	}
	for _, subnet := range vpc.Status.PrivateSubnets {
		network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.PrivateSubnets = append(network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.PrivateSubnets, &regionalapiv1.CompatNetwork_Aws_NodeSubnet{
			SubnetId:         subnet.SubnetID,
			AvailabilityZone: subnet.AvailabilityZone,
		})
	}
	for _, subnet := range vpc.Status.SecondaryNodeSubnets {
		network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.SecondaryNodeSubnets = append(network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.SecondaryNodeSubnets, &regionalapiv1.CompatNetwork_Aws_NodeSubnet{
			SubnetId:           subnet.SubnetID,
			AvailabilityZone:   subnet.AvailabilityZone,
			AvailabilityZoneId: subnet.AvailabilityZoneID,
		})
	}
	network.Info.(*regionalapiv1.CompatNetwork_Aws_).Aws.SecurityGroupId = vpc.Status.SecurityGroup.SecurityGroupID
	if appbox.Status.Phase == v1alpha1.APPBoxPhaseActive {
		network.K8SReady = true
	}
	if !network.K8SReady {
		return network, nil
	}
	ca, err := base64.StdEncoding.DecodeString(cluster.Status.AtProvider.CertificateAuthorityData)
	if err != nil {
		return nil, err
	}
	network.K8SCa = string(ca)

	return network, nil
}

func genGKECompatNetwork(gkecli gcpcontainer.ClusterGetter, vpc *v1alpha1.VPC, appbox *v1alpha1.APPBox, cluster *gkev1beta2.Cluster) (*regionalapiv1.CompatNetwork, error) {
	network := &regionalapiv1.CompatNetwork{
		K8SName:          appbox.Status.KubernetesClusterName,
		K8SVersion:       appbox.Spec.KubernetesConfig.Version,
		K8SEndpoint:      fmt.Sprintf("https://%s", cluster.Status.AtProvider.Endpoint),
		VpcId:            vpc.GetLabels()[v1alpha1.VPCIDKey],
		ProxyBaseUrl:     appbox.Status.ProxyBaseUrl,
		NodeRole:         appbox.Status.IAM.ServiceAccountEmail,
		DataflowAgentUrl: appbox.Status.DataflowAgentUrl,
		Info: &regionalapiv1.CompatNetwork_Gcp_{
			Gcp: &regionalapiv1.CompatNetwork_Gcp{
				NodeZones:   cluster.Spec.ForProvider.Locations,
				NodeSubnets: getGKENodeSubnets(vpc.Status.NodeSubnets),
			},
		},
		Iam: &regionalapiv1.CompatNetwork_IAM{
			AccountId: vpc.Spec.AccountRef.AccountID,
		},
		TenantId: vpc.GetLabels()[v1alpha1.TagTenantKey],
	}
	if appbox.Status.Phase == v1alpha1.APPBoxPhaseActive {
		network.K8SReady = true
	}

	if !network.K8SReady {
		return network, nil
	}
	name := fmt.Sprintf("projects/%s/locations/%s/clusters/%s",
		vpc.Spec.AccountRef.AccountID,
		cluster.Status.AtProvider.Location,
		xpmeta.GetExternalName(cluster))

	resp, err := gkecli.GetCluster(context.Background(), &containerpb.GetClusterRequest{
		Name: name,
	})
	if err != nil {
		log.Error("failed to get gke cluster", zap.Error(err))
		return nil, err
	}
	if resp.MasterAuth != nil {
		ca, err := base64.StdEncoding.DecodeString(resp.MasterAuth.ClusterCaCertificate)
		if err != nil {
			log.Error("failed to decode client certificate", zap.Error(err))
			return nil, err
		}
		network.K8SCa = string(ca)
	}

	return network, nil
}

func (s *NetworkService) GetRegionConfig(ctx context.Context) *regionalapiv1.RegionConfig {
	return &regionalapiv1.RegionConfig{
		Region: s.config.RegionConfig.Name,
		Zones:  s.config.RegionConfig.Zones,
	}
}
func getGKENodeSubnets(NodeSubnets []v1alpha1.Subnet) []string {
	result := make([]string, 0, len(NodeSubnets))
	for _, subnet := range NodeSubnets {
		result = append(result, subnet.SubnetID)
	}
	return result
}

// getExternalID call cluster-service getClusterRole api to get externalID
func (s *NetworkService) getExternalID(ctx context.Context, appbox *v1alpha1.APPBox) (string, error) {
	approutes := new(v1alpha1.APPRouteList)
	opt := &client.MatchingLabels{
		v1alpha1.APPBoxNameKey: appbox.Name,
	}
	if err := s.approuter.List(ctx, approutes, client.InNamespace(appbox.Namespace), opt); err != nil {
		log.Error("failed to list approute", zap.String("appbox", client.ObjectKeyFromObject(appbox).String()), zap.Error(err))
		return "", err
	}
	if len(approutes.Items) == 0 {
		log.Error("no approute under appbox", zap.String("appbox", client.ObjectKeyFromObject(appbox).String()))
		return "", errors.New("no approute under appbox")
	}
	in := new(clusterctlpb.GetClusterRoleRequest)
	in.ClusterId = approutes.Items[0].Name
	data, err := s.ccli.GetClusterRole(ctx, in)
	if err != nil {
		log.Error("failed to get cluster role", zap.Any("input", in), zap.Error(err))
		return "", errors.New("no approute under appbox")
	}
	return data.ExternalId, nil
}

func (s *NetworkService) DeleteNetwork(ctx context.Context, req *message.DeleteNetworkReq) error {
	vpcList := &v1alpha1.VPCList{}
	opt := client.MatchingLabels{}
	if req.VPCID != "" {
		opt[v1alpha1.VPCIDKey] = req.VPCID
	}
	if req.ProjectID != "" {
		opt[utils.GenProjectLabelKey(req.ProjectID)] = "true"
	}
	if req.TenantID != "" {
		opt[utils.GenTenantLabelKey(req.TenantID)] = "true"
	}
	err := s.vpcrw.List(ctx, vpcList, opt)
	if err != nil {
		log.Error("failed to list vpcs", zap.Error(err))
		return bizerr.New(bizerr.K8SClientError, "failed to list vpcs")
	}
	if len(vpcList.Items) == 0 {
		return bizerr.New(bizerr.ResourceNotFound, "vpc not found")
	}

	for _, vpc := range vpcList.Items {
		approutes := &v1alpha1.APPRouteList{}
		if err := s.approuter.List(ctx, approutes, client.MatchingLabels{
			v1alpha1.VPCNameKey: vpc.Name,
		}); err != nil {
			log.Error("failed to list approutes", zap.Error(err))
			return bizerr.New(bizerr.K8SClientError, "failed to list approutes")
		}
		if len(approutes.Items) > 0 {
			return bizerr.New(bizerr.ResuouceIsUsed, "cluster is still using the network")
		}

		appboxes := &v1alpha1.APPBoxList{}
		if err := s.appboxrw.List(ctx, appboxes, client.MatchingLabels{
			v1alpha1.VPCNameKey: vpc.Name,
		}); err != nil {
			log.Error("failed to list appboxes", zap.Error(err))
			return bizerr.New(bizerr.K8SClientError, "failed to list appboxes")
		}

		for _, appbox := range appboxes.Items {
			if err := s.appboxrw.Delete(ctx, &appbox); err != nil {
				log.Error("failed to delete appbox", zap.String("appbox", client.ObjectKeyFromObject(&appbox).String()), zap.Error(err))
				return bizerr.New(bizerr.K8SClientError, "failed to delete appbox")
			}
		}

		internets := &v1alpha1.InternetList{}
		if err := s.internetrw.List(ctx, internets, client.MatchingLabels{
			v1alpha1.VPCNameKey: vpc.Name,
		}); err != nil {
			log.Error("failed to list internets", zap.Error(err))
			return bizerr.New(bizerr.K8SClientError, "failed to list internets")
		}
		for _, internet := range internets.Items {
			if err := s.internetrw.Delete(ctx, &internet); err != nil {
				log.Error("failed to delete internet", zap.String("internet", client.ObjectKeyFromObject(&internet).String()), zap.Error(err))
				return bizerr.New(bizerr.K8SClientError, "failed to delete internet")
			}
		}

		if !req.KeepVPC {
			if err := s.vpcrw.Delete(ctx, &vpc); err != nil {
				log.Error("failed to delete vpc", zap.String("vpc", client.ObjectKeyFromObject(&vpc).String()), zap.Error(err))
				return bizerr.New(bizerr.K8SClientError, "failed to delete vpc")
			}
		}
	}

	return nil
}
