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/provider-aws/apis/lambda/v1alpha1"
	"github.com/tidbcloud/eks-provider/pkg/k8s"
	"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, plMngr privateLinkMngr) *invokeLambda {
	return &invokeLambda{
		tokMngr: tokMngr,
		aws:     aws,
		plMngr:  plMngr,
	}
}

// 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 := getReader(baseCli), getReader(infraCli)
	frpcImageName, err := GetConfigWithKey(ctx, baseReader, info, frpcImageKey)
	if err != nil {
		return err
	}
	epKey := ResourceKey(info, frpsEpName)
	epSvcDNS, err := i.plMngr.getEndpointDNS(ctx, infraReader, epKey.Name)
	if err != nil {
		return err
	}
	lambdaFunc := &lambdav1alpha1.Function{}
	err = infraReader.Get(ctx, ResourceKey(info, lambdaFuncName), lambdaFunc)
	if err != nil {
		return err
	}
	if lambdaFunc.Annotations == nil || lambdaFunc.Annotations[crossplaneExternalName] == "" {
		return fmt.Errorf("lambda external name is empty")
	}
	lambdaFuncName := lambdaFunc.Annotations[crossplaneExternalName]

	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.eksConnection.Spec.AssumeRole,
			ExternalID: info.eksConnection.Spec.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 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, infraCli client.Reader, info ProcedureInfo) (bool, error) {
	return true, nil
}

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

var _ Procedure = &invokeLambda{}
