package helper

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/aws/aws-sdk-go-v2/service/lambda"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	ec2v1beta1 "github.com/crossplane/provider-aws/apis/ec2/v1beta1"
	lambdav1alpha1 "github.com/crossplane/provider-aws/apis/lambda/v1alpha1"
	"github.com/onsi/ginkgo"
	"github.com/onsi/gomega"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
	"github.com/tidbcloud/shared-provider/tests/e2e/common/data"
	"github.com/tidbcloud/shared-provider/tests/framework"
	"github.com/tidbcloud/shared-provider/tests/framework/wait"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type LambdaEvent struct {
	DataSourceName string                `json:"dsn"`
	TLS            *LambdaEventTLSConfig `json:"tls,omitempty"`
}

type LambdaEventTLSConfig struct {
	CA   []byte `json:"ca"`
	Cert []byte `json:"cert"`
	Key  []byte `json:"key"`
}

// TODO(liubo02): combine with struct in pkg/controllers/cluster/private_link.go
type PrivateLinkStatus struct {
	PrivateLinkServiceName string `json:"privateLinkServiceName"`
}

// TODO(liubo02): combine with function in pkg/controllers/cluster/private_link.go
func getPrivateLinkServiceName(cluster *cloudv1.Cluster) (string, error) {
	str, ok := cluster.Annotations["alpha.privatelink.tidbcloud.com/status"]
	if !ok {
		return "", fmt.Errorf("can't get status of private link")
	}

	status := PrivateLinkStatus{}
	if err := json.Unmarshal([]byte(str), &status); err != nil {
		return "", err
	}

	return status.PrivateLinkServiceName, nil
}

type AWSVPCEndpointForTiDB struct {
	VPC           *ec2v1beta1.VPC
	Subnet        *ec2v1beta1.Subnet
	SecurityGroup *ec2v1beta1.SecurityGroup
	VPCEndpoint   *ec2v1alpha1.VPCEndpoint

	TiDBClientSecret client.ObjectKey
}

type LambdaFunction struct {
	Fn     *lambdav1alpha1.Function
	Client *lambda.Client
	Event  *LambdaEvent
}

func CreateAWSVPCEndpointForTiDB(
	ctx context.Context,
	f *framework.Framework,
	net *cloudv1.Network,
	cluster *cloudv1.Cluster,
) *AWSVPCEndpointForTiDB {
	svcName, err := getPrivateLinkServiceName(cluster)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	ginkgo.By(fmt.Sprintf("create vpc endpoint for private link service %v", svcName))

	ns := namespace.GetOwnedNamespaceNameFromNetwork(net)

	vesc := ec2v1alpha1.VPCEndpointServiceConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			// TODO(liubo02): extract the function in controllers/cluster
			Name: ns + ".db" + cluster.Name,
		},
	}
	err = f.Client.Get(ctx, client.ObjectKeyFromObject(&vesc), &vesc)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	vpc := data.NewCustomerVPC()
	err = f.Client.Create(ctx, vpc)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	// NOTE(liubo02): it may crach because too many pointers in vesc status
	subnet := data.NewCustomerSubnet(vpc.Name, *vesc.Status.AtProvider.ServiceConfiguration.AvailabilityZones[0])
	err = f.Client.Create(ctx, subnet)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	sg := data.NewVPCEndpointSecurityGroup(vpc.Name)
	err = f.Client.Create(ctx, sg)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	ep := data.NewVPCEndpoint(vpc.Name, subnet.Name, sg.Name, svcName)
	err = f.Client.Create(ctx, ep)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	// wait for vpc endpoint created
	err = wait.WaitForCondition(ctx, f.Client, ep, func() bool {
		if name := meta.GetExternalName(ep); name != "" {
			return true
		}

		return false
	}, LongTaskTimeout)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	return &AWSVPCEndpointForTiDB{
		VPC:           vpc,
		Subnet:        subnet,
		SecurityGroup: sg,
		VPCEndpoint:   ep,

		TiDBClientSecret: client.ObjectKey{
			Namespace: ns,
			Name:      "db" + cluster.Name + "-tidb-server-secret",
		},
	}
}

func DeleteAWSVPCEndpointForTiDB(
	ctx context.Context,
	f *framework.Framework,
	ep *AWSVPCEndpointForTiDB,
) {
	err := f.Client.Delete(ctx, ep.VPCEndpoint)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	err = f.Client.Delete(ctx, ep.SecurityGroup)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	err = f.Client.Delete(ctx, ep.Subnet)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	err = f.Client.Delete(ctx, ep.VPC)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}

func CreateLambda(
	ctx context.Context,
	f *framework.Framework,
	ep *AWSVPCEndpointForTiDB,
) *LambdaFunction {
	fn := data.NewCustomerLambda(ep.Subnet.Name, ep.SecurityGroup.Name,
		// TODO(liubo02): change to use variable
		"385595570414.dkr.ecr.us-east-1.amazonaws.com/dbaas/shared-provider-aws-lambda-mysql-client:v0.0.1",
		[]string{
			"/aws-lambda-mysql-client",
		},
	)
	err := f.Client.Create(ctx, fn)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	err = wait.WaitForCondition(ctx, f.Client, fn, func() bool {
		const trueCondCount = 2
		count := 0
		for _, c := range fn.Status.Conditions {
			switch c.Type {
			case xpv1.TypeReady:
				if c.Status == corev1.ConditionTrue {
					count++
				}
			case xpv1.TypeSynced:
				if c.Status == corev1.ConditionTrue {
					count++
				}
			default:
			}
		}

		return count == trueCondCount
	}, LongTaskTimeout)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	secret := corev1.Secret{}
	err = f.Client.Get(ctx, ep.TiDBClientSecret, &secret)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	e := LambdaEvent{
		DataSourceName: fmt.Sprintf(
			"root@tcp(%s:4000)/",
			*ep.VPCEndpoint.Status.AtProvider.VPCEndpoint.DNSEntries[2].DNSName,
		),
		TLS: &LambdaEventTLSConfig{
			CA:   secret.Data["ca.crt"],
			Cert: secret.Data["tls.crt"],
			Key:  secret.Data["tls.key"],
		},
	}

	return &LambdaFunction{
		Fn:     fn,
		Client: lambda.NewFromConfig(f.AWSConfig),
		Event:  &e,
	}
}

func DeleteLambda(
	ctx context.Context,
	f *framework.Framework,
	fn *LambdaFunction,
) {
	err := f.Client.Delete(ctx, fn.Fn)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}

func (lf *LambdaFunction) Invoke(ctx context.Context) {
	bs, err := json.Marshal(lf.Event)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	res, err := lf.Client.Invoke(ctx, &lambda.InvokeInput{
		FunctionName: pointer.String(lf.Fn.Name),
		Payload:      bs,
	})
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	gomega.Expect(res.Payload).To(gomega.Equal([]byte(`null`)))
	gomega.Expect(res.FunctionError).To(gomega.BeNil())
	gomega.Expect(res.StatusCode).To(gomega.Equal(int32(http.StatusOK)))
}
