package eks

import (
	"context"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/elliotchance/pie/v2"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
	"k8s.io/utils/pointer"
)

const (
	vpceStatePendingAcceptance = "pendingacceptance"
	vpceStatePending           = "pending"
	vpceStateAvailable         = "available"
	vpceStateDeleting          = "deleting"
	vpceStateDeleted           = "deleted"
	vpceStateRejected          = "rejected"
	vpceStateFailed            = "failed"
	vpceStateExpired           = "expired"
)

type Endpoint struct {
	*task.Context
	awsCli aws.Client
}

func NewEndpoint(c *task.Context, awsCli aws.Client) task.VpcEndpointProvider {
	return &Endpoint{Context: c, awsCli: awsCli}
}

func (t *Endpoint) AcceptVPCEndpoints(ctx context.Context, acceptList []task.VpcEndpointConnection) error {
	svcID := t.PrivateLinkService.Status.EndpointService.ServiceID
	endpointIDs := pie.Map(acceptList, func(conn task.VpcEndpointConnection) string { return conn.EndpointID })
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.AcceptVpcEndpointConnections(ctx,
		&ec2.AcceptVpcEndpointConnectionsInput{
			ServiceId:      &svcID,
			VpcEndpointIds: endpointIDs,
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})
	if err != nil {
		return err
	}

	if len(res.Unsuccessful) != 0 {
		return fmt.Errorf("accept some vpc endpoints(%v) failed", res.Unsuccessful)
	}

	return nil
}

func (t *Endpoint) RejectVPCEndpoints(ctx context.Context, rejectList []task.VpcEndpointConnection) error {
	svcID := t.PrivateLinkService.Status.EndpointService.ServiceID
	endpointIDs := pie.Map(rejectList, func(conn task.VpcEndpointConnection) string { return conn.EndpointID })
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.RejectVpcEndpointConnections(ctx,
		&ec2.RejectVpcEndpointConnectionsInput{
			ServiceId:      &svcID,
			VpcEndpointIds: endpointIDs,
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})
	if err != nil {
		// This method will be used in both creation and deletion
		// Though in creation this should be unexpected, it never happens
		// In deletion, we should treat the rejection as success
		if strings.Contains(err.Error(), "InvalidVpcEndpointServiceId.NotFound") {
			return nil
		}
		return err
	}

	if len(res.Unsuccessful) != 0 {
		return fmt.Errorf("reject some vpc endpoints(%v) failed", res.Unsuccessful)
	}

	return nil
}

func (t *Endpoint) GetAllVPCEndpointConns(ctx context.Context) ([]task.VpcEndpointConnection, error) {
	svcID := t.PrivateLinkService.Status.EndpointService.ServiceID
	awsVPCEConns, err := t.getAWSAllVPCEndpointConns(ctx, svcID)
	if err != nil {
		return nil, err
	}

	conns := make([]task.VpcEndpointConnection, 0, len(awsVPCEConns))
	for i := range awsVPCEConns {
		conns = append(conns, *convertAWSVpceConnection(&awsVPCEConns[i]))
	}
	return conns, nil
}

func (t *Endpoint) getAWSAllVPCEndpointConns(ctx context.Context, svcID string) ([]ec2types.VpcEndpointConnection, error) {
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.DescribeVpcEndpointConnections(ctx,
		&ec2.DescribeVpcEndpointConnectionsInput{
			Filters: []ec2types.Filter{
				{
					Name: pointer.String("service-id"),
					Values: []string{
						svcID,
					},
				},
			},
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})

	if err != nil {
		return nil, err
	}

	return res.VpcEndpointConnections, nil
}

// State mapping is shown as below:
// - VPC Endpoint should exist before accepting
//   - deleting          => Failed
//   - deleted           => Failed
//
// - VPC Endpoint should be ready for accepting
//   - pendingAcceptance => Pending
//   - pending           => Pending
//
// - VPC Endpoint is available
//   - available         => Active
//
// - Rejected VPC Endpoint can be re-accepted, so just display Pending state
//   - rejected          => Pending
//
// - NOTE: These state are hard to produce and test, so just set them to Failed
//   - failed            => Failed
//   - expired           => Failed
func convertAWSVpceConnection(vpceConn *ec2types.VpcEndpointConnection) *task.VpcEndpointConnection {
	conn := &task.VpcEndpointConnection{
		EndpointID: *vpceConn.VpcEndpointId,
	}
	state := strings.ToLower(string(vpceConn.VpcEndpointState))
	switch state {
	case vpceStateAvailable:
		conn.Phase = v1alpha1.ConnectionPhaseActive
	case vpceStatePending, vpceStatePendingAcceptance, vpceStateRejected:
		conn.Phase = v1alpha1.ConnectionPhasePending
	case vpceStateDeleting, vpceStateDeleted:
		conn.Phase = v1alpha1.ConnectionPhaseFailed
		conn.Message = fmt.Sprintf("The VPC Endpoint '%s' is %s", conn.EndpointID, state)
	case vpceStateFailed, vpceStateExpired:
		conn.Phase = v1alpha1.ConnectionPhaseFailed
		conn.Message = fmt.Sprintf("The VPC Endpoint '%s' is failed to be active", conn.EndpointID)
	default:
		conn.Phase = v1alpha1.ConnectionPhaseFailed
		conn.Message = fmt.Sprintf("The VPC Endpoint '%s' is in unknown state %s", conn.EndpointID, state)
	}
	return conn
}
