package eks

import (
	"context"
	"fmt"
	"strings"
	"time"

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

type ApproveEndpoints struct {
	*task.BaseTask
	awsCli aws.Client
}

func NewApproveEndpoints(c *task.Context, awsCli aws.Client) *ApproveEndpoints {
	return &ApproveEndpoints{BaseTask: &task.BaseTask{Context: c}, awsCli: awsCli}
}

func (t *ApproveEndpoints) Name() string {
	return "ApproveEndpoints"
}

func (t *ApproveEndpoints) Create(ctx context.Context) deploy.Result {
	conns, err := t.getAllVPCEndpointConns(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	allowed := t.PrivateLinkService.Spec.Connections
	connsStatus, _ := t.calculateConnsStatus(allowed, conns)
	unaccepted := controllers.GetUnacceptedVPCEndpoints(connsStatus)
	if len(unaccepted) > 0 {
		if err := t.acceptVPCEndpoints(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID, unaccepted...); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}

func (t *ApproveEndpoints) getAllVPCEndpointConns(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
}

func (t *ApproveEndpoints) acceptVPCEndpoints(ctx context.Context, svcID string, acceptList ...string) error {
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.AcceptVpcEndpointConnections(ctx,
		&ec2.AcceptVpcEndpointConnectionsInput{
			ServiceId:      &svcID,
			VpcEndpointIds: acceptList,
		}, 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
}

// Only connections in allowed list will display the state in status
// State mapping is shown as below:
// - VPC Endpoint should exist before accepting
//   - "not found"       => Failed
//   - 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 (t *ApproveEndpoints) calculateConnsStatus(allowed []v1alpha1.Connection, conns []ec2types.VpcEndpointConnection) ([]v1alpha1.ConnectionStatus, []string) {
	connsMap := map[string]*ec2types.VpcEndpointConnection{}
	allowedMap := map[string]struct{}{}
	for i := range conns {
		connsMap[*conns[i].VpcEndpointId] = &conns[i]
	}

	connsStatus := []v1alpha1.ConnectionStatus{}
	unrejected := []string{}

	for _, item := range allowed {
		status := v1alpha1.ConnectionStatus{
			Endpoint: item.Endpoint,
		}

		conn, ok := connsMap[item.Endpoint]
		if !ok {
			// all conns that are not in allowed list will be ignored in status
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhaseFailed
			status.Phase = v1alpha1.ConnectionPhaseFailed
			status.Message = fmt.Sprintf("The VPC Endpoint Id '%s' does not exist", item)
			connsStatus = append(connsStatus, status)
			continue
		}

		state := strings.ToLower(string(conn.VpcEndpointState))
		switch state {
		case epStateAvailable:
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhaseActive
			status.Phase = v1alpha1.ConnectionPhaseActive
		case epStatePending, epStatePendingAcceptance, epStateRejected:
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhasePending
			status.Phase = v1alpha1.ConnectionPhasePending
		case epStateDeleting, epStateDeleted:
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhaseFailed
			status.Phase = v1alpha1.ConnectionPhaseFailed
			status.Message = fmt.Sprintf("The VPC Endpoint '%s' is %s", status.Endpoint, state)
		case epStateFailed, epStateExpired:
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhaseFailed
			status.Phase = v1alpha1.ConnectionPhaseFailed
			status.Message = fmt.Sprintf("The VPC Endpoint '%s' is failed to be active", status.Endpoint)
		default:
			// status.Phase = cloudv1.PrivateLinkServiceConnectionPhaseFailed
			status.Phase = v1alpha1.ConnectionPhaseFailed
			status.Message = fmt.Sprintf("The VPC Endpoint '%s' is in unknown state %s", status.Endpoint, state)
		}

		allowedMap[item.Endpoint] = struct{}{}
		connsStatus = append(connsStatus, status)
	}

	for _, conn := range conns {
		if _, ok := allowedMap[*conn.VpcEndpointId]; !ok {
			// if the conn is not in allowed list and is approved, it should be rejected
			if strings.ToLower(string(conn.VpcEndpointState)) == epStateAvailable {
				unrejected = append(unrejected, *conn.VpcEndpointId)
			}
		}
	}

	return connsStatus, unrejected
}
