package cluster

import (
	"fmt"
	"sort"
	"strings"

	ccdnsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/dns/v1beta1"
	cck8sv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1"
	elbapi "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	awsdnsv1alpha1 "github.com/crossplane/provider-aws/apis/route53/v1alpha1"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	corev1 "k8s.io/api/core/v1"
	netv1 "k8s.io/api/networking/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/pingcap/label"
)

const (
	defaultTiDBPort = 4000

	ClusterNameLabelKey      = "cluster.cloud.pingcap.com/name"
	ClusterNamespaceLabelKey = "cluster.cloud.pingcap.com/namespace"
)

type dnsAlias struct {
	// name is a readable name of the record
	name string
	// alias is the alias address
	alias string
	// address is the source address
	address string
	// hostedZoneId is the dns hosted zone id of the source address
	hostedZoneId string
}

func getPublicServiceName(clusterName string) string {
	return fmt.Sprintf("%s-tidb-public", clusterName)
}

func getPrivateServiceName(clusterName string) string {
	return fmt.Sprintf("%s-tidb-private", clusterName)
}

func getTiDBWhiteListPolicyName(clusterName string) string {
	return fmt.Sprintf("%s-tidb-whitelist", clusterName)
}

// nolint: gomnd,lll // it's default
func getAwsElbName(elbDNSName string) (string, error) {
	// the format of elbDNSName is like this: k8s-sharedtier-db1tidbp-e9a2b945e3-5238eea775943ac8.elb.us-west-2.amazonaws.com
	elbDNSNameSlice := strings.SplitN(elbDNSName, "-", 5)
	if len(elbDNSNameSlice) < 5 {
		return "", fmt.Errorf("tidb ELB DNS name format error")
	}
	elbDNSNameSlice = elbDNSNameSlice[:len(elbDNSNameSlice)-1]

	return strings.Join(elbDNSNameSlice, "-"), nil
}

func (r *reconcileContext) publicDnsRecordName() string {
	return fmt.Sprintf("%s-public", r.cluster.ClusterScopeName())
}

func (r *reconcileContext) reconcileTidbAccess() error {
	var sourceRangesSorted []string
	sourceRangesSorted = append(sourceRangesSorted, r.cluster.Spec.SqlEndpoints.Public.LoadBalancerSourceRanges...)
	sort.Strings(sourceRangesSorted)

	peers := []netv1.NetworkPolicyPeer{}
	for _, source := range sourceRangesSorted {
		peers = append(peers, netv1.NetworkPolicyPeer{IPBlock: &netv1.IPBlock{CIDR: source}})
	}

	// nolint: gomnd // it's default
	tidbPort := intstr.FromInt(4000)
	pTCP := corev1.ProtocolTCP
	wantedTidbAccessPolicy := &netv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      getTiDBWhiteListPolicyName(r.tcKey.Name),
			Namespace: r.tcKey.Namespace,
		},
		Spec: netv1.NetworkPolicySpec{
			PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
			PodSelector: metav1.LabelSelector{
				MatchLabels: map[string]string{
					types.LabelKeyComponent: types.LabelValComponentTiDB,
					types.LabelKeyInstance:  r.tcKey.Name,
				},
			},
		},
	}

	if len(peers) != 0 {
		wantedTidbAccessPolicy.Spec.Ingress = append(wantedTidbAccessPolicy.Spec.Ingress, netv1.NetworkPolicyIngressRule{
			From: peers,
			Ports: []netv1.NetworkPolicyPort{
				{
					Port:     &tidbPort,
					Protocol: &pTCP,
				},
			},
		})
	}

	nowTidbAccessPolicy := &netv1.NetworkPolicy{}
	key := client.ObjectKey{Name: getTiDBWhiteListPolicyName(r.tcKey.Name), Namespace: r.tcKey.Namespace}
	if err := r.Cache.Get(r.ctx, key, nowTidbAccessPolicy); err != nil {
		if !apierrors.IsNotFound(err) {
			return err
		} else {
			return r.createOrGetWithOwnerLabel(wantedTidbAccessPolicy)
		}
	}

	if !equality.Semantic.DeepEqual(&wantedTidbAccessPolicy.Spec, &nowTidbAccessPolicy.Spec) {
		return r.Client.Update(r.ctx, wantedTidbAccessPolicy)
	}

	return nil
}

func (r *reconcileContext) ensureDNSRecord(alias dnsAlias) error {
	switch r.network.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		return r.ensureDNSRecordInAWS(alias)
	case types.ProviderTypeGCPSharedTier:
		return r.ensureDNSRecordInGCP(alias)
	}

	return r.ensureDNSRecordInAWS(alias)
}

func (r *reconcileContext) ensureDNSRecordInAWS(alias dnsAlias) error {
	var err error
	rrs := &awsdnsv1alpha1.ResourceRecordSet{}
	if err = r.InfraCache.Get(r.ctx, client.ObjectKey{Name: alias.name}, rrs); err != nil && apierrors.IsNotFound(err) {
		elbName, err := getAwsElbName(alias.address)
		if err != nil {
			return err
		}
		result, err := r.AwsClient.Elb().DescribeLoadBalancers(r.ctx, &elbapi.DescribeLoadBalancersInput{
			Names: []string{elbName},
		})
		if err != nil {
			r.InfraRecorder.Eventf(r.cluster, corev1.EventTypeWarning, "DescribeLoadBalancers", "%v", err)
			return errors.Annotate(err, "get hosted zone of AWS ELB")
		}
		if len(result.LoadBalancers) == 0 || result.LoadBalancers[0].CanonicalHostedZoneId == nil {
			return errors.Annotatef(err, "AWS ELB not found for address %s", alias.address)
		}
		elbHostedZoneId := *result.LoadBalancers[0].CanonicalHostedZoneId
		rrs = &awsdnsv1alpha1.ResourceRecordSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: alias.name,
				Annotations: map[string]string{
					// external name will be used as the A record alias address
					meta.AnnotationKeyExternalName: alias.alias,
				},
				OwnerReferences: []metav1.OwnerReference{
					*metav1.NewControllerRef(r.cluster, cloudv1.GroupVersion.WithKind("Cluster")),
				},
			},
			Spec: awsdnsv1alpha1.ResourceRecordSetSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{
						Name: types.CrossplaneProviderConfigReferenceName,
					},
				},
				ForProvider: awsdnsv1alpha1.ResourceRecordSetParameters{
					AliasTarget: &awsdnsv1alpha1.AliasTarget{
						DNSName:              fmt.Sprintf("dualstack.%s", alias.address),
						EvaluateTargetHealth: false,
						HostedZoneID:         elbHostedZoneId,
					},
					Type:   "A",
					ZoneID: pointer.StringPtr(alias.hostedZoneId),
				},
			},
		}

		return r.InfraClient.Create(r.ctx, rrs)
	}

	return err
}

func (r *reconcileContext) ensureDNSRecordInGCP(alias dnsAlias) error {
	cur := &ccdnsv1beta1.DNSRecordSet{
		ObjectMeta: metav1.ObjectMeta{
			Name: alias.name,
		},
	}
	if err := r.Cache.Get(r.ctx, client.ObjectKeyFromObject(cur), cur); err != nil {
		if !apierrors.IsNotFound(err) {
			return err
		}
		expected := &ccdnsv1beta1.DNSRecordSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: alias.name,
			},
			Spec: ccdnsv1beta1.DNSRecordSetSpec{
				ManagedZoneRef: cck8sv1alpha1.ResourceRef{
					External: alias.hostedZoneId,
				},
				Name: alias.alias,
				Type: "A",
				Rrdatas: []string{
					alias.address,
				},
			},
		}
		return r.createOrGetWithOwnerLabel(expected)
	}

	// TODO: compare and update dns record set
	return nil
}

func (r *reconcileContext) newService(isPublic bool) *corev1.Service {
	switch r.network.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		return r.newServiceInAWS(isPublic)
	case types.ProviderTypeGCPSharedTier:
		return r.newServiceInGCP(isPublic)
	}

	return r.newServiceInAWS(isPublic)
}

func (r *reconcileContext) newServiceInGCP(isPublic bool) *corev1.Service {
	svcName := getPrivateServiceName(r.tcKey.Name)
	if isPublic {
		svcName = getPublicServiceName(r.tcKey.Name)
	}
	annotations := map[string]string{}

	return r.newServiceWithAnnotations(svcName, annotations)
}

func (r *reconcileContext) newServiceInAWS(isPublic bool) *corev1.Service {
	scheme := "internal"
	svcName := getPrivateServiceName(r.tcKey.Name)
	subnets := r.LoadBalancerClusterName + "-vpc-private-0"
	if isPublic {
		scheme = "internet-facing"
		svcName = getPublicServiceName(r.tcKey.Name)
		subnets = r.LoadBalancerClusterName + "-vpc-public-0"
	}

	annotations := map[string]string{
		"service.beta.kubernetes.io/aws-load-balancer-type":                              "external",
		"service.beta.kubernetes.io/aws-load-balancer-nlb-target-type":                   "ip",
		"service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled": "true",
		"service.beta.kubernetes.io/aws-load-balancer-subnets":                           subnets,
		"service.beta.kubernetes.io/aws-load-balancer-scheme":                            scheme,
		"service.beta.kubernetes.io/aws-load-balancer-target-group-attributes":           "preserve_client_ip.enabled=true",
		"service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags":          r.getNLBServiceAWSTags(isPublic),
	}

	return r.newServiceWithAnnotations(svcName, annotations)
}

func (r *reconcileContext) newServiceWithAnnotations(name string, annotations map[string]string) *corev1.Service {
	servicePorts := []corev1.ServicePort{
		{
			Name:       "mysql-client",
			Port:       defaultTiDBPort,
			TargetPort: intstr.FromInt(defaultTiDBPort),
			Protocol:   corev1.ProtocolTCP,
		},
	}

	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:        name,
			Namespace:   r.tcKey.Namespace,
			Labels:      labels.NewOwnerLabels(r.cluster, nil),
			Annotations: annotations,
		},
		Spec: corev1.ServiceSpec{
			Type:  corev1.ServiceTypeLoadBalancer,
			Ports: servicePorts,
			Selector: map[string]string{
				label.InstanceLabelKey:  r.tcKey.Name,
				label.ComponentLabelKey: string(cloudv1.ComponentTiDB),
			},
		},
	}

	return svc
}

func (r *reconcileContext) getNLBServiceAWSTags(isPublic bool) string {
	component := types.BillingTagValComponentPrivateNLB
	if isPublic {
		component = types.BillingTagValComponentNLB
	}
	tags := r.getTags(component, types.BillingTagValUsedByCustomerTiDB)

	sb := strings.Builder{}
	for _, item := range tags {
		if sb.Len() > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(item.Key)
		sb.WriteByte('=')
		sb.WriteString(item.Value)
	}
	return sb.String()
}
