package cluster

import (
	"context"
	"fmt"
	"strconv"
	"time"

	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"
	gcpdnsv1alpha1 "github.com/crossplane/provider-gcp/apis/dns/v1alpha1"
	"github.com/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	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"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/common"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	"github.com/tidbcloud/infra-api/pkg/pingcap/label"
	"github.com/tidbcloud/infra-api/pkg/providers/aws"
)

const (
	annoKeyUseLegacyDNS = "infra.tidbcloud.com/use-legacy-dns"
	annoValUseLegacyDNS = "true"

	randomLen = 6
	// 36 = len(0-9) + len(a-z)
	formatBase = 36
)

var (
	timestampStartPoint = time.Date(2022, time.January, 0, 0, 0, 0, 0, time.UTC)
)

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 newDefaultDNSIdentity() string {
	n := time.Now().UTC().Unix() - timestampStartPoint.Unix()
	s := strconv.FormatInt(n, formatBase)

	return cloudv1.RandomString(randomLen) + s
}

func (r *reconcileContext) setDefaultDNSIdentity() error {
	if r.Target.Spec.SqlEndpoints.DNS.Identity != "" || isUseLegacyDNS(r.Target) {
		return nil
	}

	// Legacy DNS has been exported to user
	if r.Target.Status.SqlEndpoints.PublicEndpoint != nil || r.Target.Status.SqlEndpoints.PrivateEndpoint != nil {
		if r.Target.Annotations == nil {
			r.Target.Annotations = map[string]string{}
		}
		r.Target.Annotations[annoKeyUseLegacyDNS] = annoValUseLegacyDNS
	} else {
		r.Target.Spec.SqlEndpoints.DNS.Identity = newDefaultDNSIdentity()
	}

	if err := r.Update(r.ctx, r.Target); err != nil {
		return err
	}

	return nil
}

func isUseLegacyDNS(c *cloudv1.Cluster) bool {
	if c.Annotations == nil {
		return false
	}
	val, ok := c.Annotations[annoKeyUseLegacyDNS]
	if ok && val == annoValUseLegacyDNS {
		return true
	}

	return false
}

func (r *reconcileContext) ensureAwsDnsRecord(alias DnsAlias) error {
	return EnsureAwsDnsRecord(r.ctx, r.Client, r.AWS.Elb(), alias, r.Network.Spec.Region, r.providerGardenerSpec.AWS.CrossplaneProvider, r.SubResourceLabels())
}

func EnsureAwsDnsRecord(
	ctx context.Context,
	cli client.Client,
	elbCli aws.ElbClient,
	alias DnsAlias,
	region string,
	providerConfigName string,
	labels map[string]string,
) error {
	elbName, err := getAwsElbName(alias.Address)
	if err != nil {
		return err
	}

	result, err := elbCli.DescribeLoadBalancers(ctx, &elbapi.DescribeLoadBalancersInput{
		Names: []string{elbName},
	}, aws.WithELBRegion(region))
	if err != nil {
		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,
			Labels: labels,
			Annotations: map[string]string{
				// external name will be used as the A record alias address
				meta.AnnotationKeyExternalName: alias.Alias,
			},
		},
		Spec: awsdnsv1alpha1.ResourceRecordSetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: providerConfigName,
				},
			},
			ForProvider: awsdnsv1alpha1.ResourceRecordSetParameters{
				AliasTarget: &awsdnsv1alpha1.AliasTarget{
					DNSName:              fmt.Sprintf("dualstack.%s", alias.Address),
					EvaluateTargetHealth: false,
					HostedZoneID:         elbHostedZoneId,
				},
				Type:   "A",
				ZoneID: pointer.String(alias.HostedZoneId),
			},
		},
	}
	_, err = k8s.CreateIgnoreExists(ctx, cli, rrs)
	return err
}

func (r *reconcileContext) ensureGcpDnsRecord(alias DnsAlias) error {
	// Use aws route53 to provide dns serivce for GCP to avoid different domain in different cloud
	rrs := &awsdnsv1alpha1.ResourceRecordSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:   alias.Name,
			Labels: r.SubResourceLabels(),
			Annotations: map[string]string{
				// external name will be used as the A record alias address
				meta.AnnotationKeyExternalName: alias.Alias,
			},
		},
		Spec: awsdnsv1alpha1.ResourceRecordSetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: r.providerGardenerSpec.AWS.CrossplaneProvider,
				},
			},
			ForProvider: awsdnsv1alpha1.ResourceRecordSetParameters{
				ResourceRecords: []awsdnsv1alpha1.ResourceRecord{
					{
						Value: alias.Address,
					},
				},
				Type:   "A",
				TTL:    pointer.Int64(60),
				ZoneID: pointer.String(alias.HostedZoneId),
			},
		},
	}

	if _, err := k8s.CreateIgnoreExists(r.ctx, r.Client, rrs); err != nil {
		return err
	}

	return nil
}

func (r *reconcileContext) ensureLegacyGcpDnsRecord(alias DnsAlias) error {
	rrs := &gcpdnsv1alpha1.ResourceRecordSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:   alias.Name,
			Labels: r.SubResourceLabels(),
			Annotations: map[string]string{
				// external name will be used as the A record alias address
				meta.AnnotationKeyExternalName: alias.Alias,
			},
		},
		Spec: gcpdnsv1alpha1.ResourceRecordSetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: r.providerGardenerSpec.GCP.CrossplaneProvider,
				},
			},
			ForProvider: gcpdnsv1alpha1.ResourceRecordSetParameters{
				Rrdatas: []string{alias.Address},
				Type:    pointer.String("A"),
				ZoneID:  pointer.String(alias.HostedZoneId),
			},
		},
	}
	_, err := k8s.CreateIgnoreExists(r.ctx, r.Client, rrs)
	return err
}

func (r *reconcileContext) newPrivateService(tc *v1alpha1.TidbCluster, provider *cloudv1.Provider) (*corev1.Service, error) {
	var annotation map[string]string
	switch provider.Spec.Type {
	case consts.ProviderTypeAWS:
		annotation = map[string]string{
			"service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled": "true",
			"service.beta.kubernetes.io/aws-load-balancer-internal":                          "0.0.0.0/0",
			"service.beta.kubernetes.io/aws-load-balancer-type":                              "nlb",
		}
		// set aws resource tag
		tags := common.ConcatenateTags(cloudv1.TagsFromLabels(r.Target.Labels),
			map[string]string{
				common.Cluster:   r.Target.Name,
				common.Component: common.Loadbalance,
				common.UsedBy:    common.CustomerTiDB,
			})
		serviceTag := cloudv1.TagStringForAWS(tags)

		annotation[K8SLabelKeyForAWSLoadBalanceResourceTag] = serviceTag
	case consts.ProviderTypeGCP:
		annotation = map[string]string{"cloud.google.com/load-balancer-type": "Internal"}
	default:
		return nil, fmt.Errorf("unsupported provider type %s", provider.Spec.Type)
	}

	servicePorts := []corev1.ServicePort{
		{
			Name:       "mysql-client",
			Port:       r.Target.GetPrivateSQLEndpointPort(),
			TargetPort: intstr.FromInt(tidbServiceTargetPort),
			Protocol:   corev1.ProtocolTCP,
		},
	}
	gardenProviderSpec, err := cloudv1.GardenerProviderSpecForProvider(provider)
	if err != nil {
		return nil, err
	}
	if gardenProviderSpec.GetEnableGracefulShutdownWebhook() {
		healthCheckerServicePort := corev1.ServicePort{
			Name:       "health-checker",
			Protocol:   corev1.ProtocolTCP,
			Port:       8080,
			TargetPort: intstr.FromInt(8080),
		}
		servicePorts = append(servicePorts, healthCheckerServicePort)
	}

	return &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:        getPrivateServiceName(tc.Name),
			Namespace:   tc.GetNamespace(),
			Annotations: annotation,
		},
		Spec: corev1.ServiceSpec{
			Type:                  corev1.ServiceTypeLoadBalancer,
			SessionAffinity:       corev1.ServiceAffinityNone,
			ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal,
			Ports:                 servicePorts,
			Selector: map[string]string{
				label.InstanceLabelKey:  tc.Name,
				label.ComponentLabelKey: string(cloudv1.ComponentTiDB),
			},
		},
	}, nil
}
