package procedure

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

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go/service/lambda"
	lambdav1alpha1 "github.com/crossplane-contrib/provider-aws/apis/lambda/v1alpha1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	invokeLambdaName = "invoke-lambda"
	frpcImageKey     = "frpcImage"
)

type invokeLambda struct {
	aws     AWSService
	tokMngr k8s.EKSTokenManager
	plMngr  privateLinkMngr
}

type lambdaRequest struct {
	// Token is the bearer token for tenant EKS
	Token string `json:"token"`
	// EksArn is the ARN for the tenant eks cluster
	EksArn string `json:"eksArn"`
	// Endpoint is the domain name for the EKS apiserver
	Endpoint string `json:"endpoint"`
	// ServiceDNS is the overwritten DNS records for public services (e.g. Auth0)
	// The IPs will be resolved to endpoint services hosted by TiDB cloud.
	ServiceDNS map[string]string `json:"serviceDNS"`
	// FrpcImage is the image for frpc.
	FrpcImage string `json:"frpcImage"`
	// FrpsEndpointSvc is the endpoint service for frps.
	FrpsEndpointSvc string `json:"frpsEndpointSvc"`
}

func InvokeLambda(tokMngr k8s.EKSTokenManager, aws AWSService) *invokeLambda {
	return &invokeLambda{
		tokMngr: tokMngr,
		aws:     aws,
	}
}

// Deploy deploys resource for the specified tenant.
// The operation need to be idempotent.
func (i *invokeLambda) Deploy(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	baseReader, infraReader := AsReader(baseCli), AsReader(infraCli)
	frpcImageName, err := GetConfigWithKey(ctx, baseReader, info, frpcImageKey)
	if err != nil {
		return err
	}
	pl := &eksv1alpha1.Privatelink{}
	err = infraCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.eksConnection.ObjectMeta)}, pl)
	if err != nil {
		return err
	}
	if pl.Annotations == nil || pl.Annotations[AnnotationKeyEndpointDNS] == "" {
		return fmt.Errorf("endpoint DNS is empty")
	}
	epSvcDNS := pl.Annotations[AnnotationKeyEndpointDNS]

	lambdaFunc := &lambdav1alpha1.Function{}
	err = infraReader.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.eksConnection.ObjectMeta)}, lambdaFunc)
	if err != nil {
		return err
	}
	if lambdaFunc.Annotations == nil || lambdaFunc.Annotations[meta.AnnotationKeyExternalName] == "" {
		return fmt.Errorf("lambda external name is empty")
	}
	lambdaFuncName := lambdaFunc.Annotations[meta.AnnotationKeyExternalName]

	eks, err := getEKSObject(ctx, infraReader, info.eksConnection)
	if err != nil {
		return err
	}
	token, err := i.tokMngr.RetrieveToken(ctx, k8s.EKSBearerTokenOpt{
		EKSTarget: k8s.EKSTarget{
			Cluster:    info.Cluster,
			Region:     info.Region,
			Namespace:  info.namespace,
			AssumeRole: info.assumeRole,
			ExternalID: info.externalID,
		},
	})
	if err != nil {
		return err
	}
	dns, err := getEpSvcDNS(ctx, baseReader)
	if err != nil {
		return err
	}
	req, err := json.Marshal(&lambdaRequest{
		Token:           token,
		EksArn:          eks.Status.AtProvider.Arn,
		Endpoint:        eks.Status.AtProvider.Endpoint,
		ServiceDNS:      dns,
		FrpcImage:       frpcImageName,
		FrpsEndpointSvc: epSvcDNS,
	})
	if err != nil {
		return err
	}
	_, err = i.aws.LambdaAPI(info.getAssumeRole()).InvokeWithContext(ctx, &lambda.InvokeInput{
		FunctionName: aws.String(lambdaFuncName),
		Payload:      req,
	})
	if err != nil {
		return fmt.Errorf("failed to invoke lambda function: %w", err)
	}
	return nil
}

// getEpSvcDNS retrieves IP address for endpoint services
func getEpSvcDNS(ctx context.Context, cli client.Reader) (map[string]string, error) {
	// TODO: retrieves addresses for the endpoint services
	// the original DNS resolution for the public services (e.g. Auth0) will
	// be resolved to IP address for corresponding endpoint service.
	// In the returned map, key will be the original name for public services,
	// and the value will be the IP for endpoint service.
	return nil, nil
}

// Status retrieves the status for the deploying step.
func (i *invokeLambda) Status(ctx context.Context, baseCli client.Reader, infraCli client.Client, info ProcedureInfo) (bool, error) {
	return true, nil
}

// Name returns the name of the step.
func (i *invokeLambda) Name() string {
	return invokeLambdaName
}

var _ Procedure = &invokeLambda{}
