package eks

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

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	"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/privatelinkservice/task"
)

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

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

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

// It's safe to rerun this Create task for migrated resource.
func (t *Endpoint) Create(ctx context.Context) deploy.Result {
	conns, err := t.getVPCEndpointConns(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	allowed := t.PrivateLinkService.Spec.Connections
	connsStatus, unrejected := 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)
		}
	}

	if len(unrejected) > 0 {
		if err := t.rejectVPCEndpoints(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID, unrejected...); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}

	return deploy.Done()
}

func (t *Endpoint) PostCreate(ctx context.Context) deploy.Result {
	conns, err := t.getVPCEndpointConns(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	allowed := t.PrivateLinkService.Spec.Connections
	connsStatus, unrejected := t.calculateConnsStatus(allowed, conns)
	unaccepted := controllers.GetUnacceptedVPCEndpoints(connsStatus)
	if len(unaccepted)+len(unrejected) > 0 {
		return deploy.Panic(fmt.Errorf("some vpc endpoints are not accepted or rejected, requeue")).WithRequeuAfter(time.Second * 30)
	}

	if err := t.setConntionStatus(ctx, connsStatus); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *Endpoint) setConntionStatus(ctx context.Context, conns []v1alpha1.ConnectionStatus) error {
	t.PrivateLinkService.Status.Connections = conns
	return t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService)
}

func (t *Endpoint) Delete(ctx context.Context) deploy.Result {
	if t.PrivateLinkService.Status.EndpointService.ServiceID != "" {
		if err := t.rejectAllVPCEndpoints(ctx, t.PrivateLinkService.Status.EndpointService.ServiceID); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}

func (t *Endpoint) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *Endpoint) rejectAllVPCEndpoints(ctx context.Context, svcID string) error {
	conns, err := t.getVPCEndpointConns(ctx, svcID)
	if err != nil {
		return err
	}
	rejectList := []string{}
	for _, conn := range conns {
		t.Logger.Info("checking vpc endpoint", "conn", conn)
		if isUnrejected(&conn) {
			rejectList = append(rejectList, *conn.VpcEndpointId)
		}
	}
	t.Logger.Info("rejecting vpc endpoints", "rejectList", rejectList)
	if len(rejectList) == 0 {
		return nil
	}
	if err := t.rejectVPCEndpoints(ctx, svcID, rejectList...); err != nil {
		return err
	}

	return nil
}

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

func (t *Endpoint) rejectVPCEndpoints(ctx context.Context, svcID string, rejectList ...string) error {
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.RejectVpcEndpointConnections(ctx,
		&ec2.RejectVpcEndpointConnectionsInput{
			ServiceId:      &svcID,
			VpcEndpointIds: rejectList,
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})
	if err != nil {
		return err
	}

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

	return nil
}

// getVPCEndpointConns intentionally preserves the original single-request
// reconciliation behavior. The IPv4 downgrade safety check uses the separate
// paginated endpointservice.ListConnections helper.
func (t *Endpoint) getVPCEndpointConns(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:   awssdk.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
}

const (
	epStatePendingAcceptance = "pendingacceptance"
	epStatePending           = "pending"
	epStateAvailable         = "available"
	epStateDeleting          = "deleting"
	epStateDeleted           = "deleted"
	epStateRejected          = "rejected"
	epStateFailed            = "failed"
	epStateExpired           = "expired"
)

// 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 *Endpoint) 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
		}
		status.IPAddressType = endpointIPAddressTypeFromAWS(conn.IpAddressType)

		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
}

func endpointIPAddressTypeFromAWS(addressType ec2types.IpAddressType) v1alpha1.EndpointIPAddressType {
	switch strings.ToLower(string(addressType)) {
	case string(v1alpha1.EndpointIPAddressTypeIPv4):
		return v1alpha1.EndpointIPAddressTypeIPv4
	case string(v1alpha1.EndpointIPAddressTypeIPv6):
		return v1alpha1.EndpointIPAddressTypeIPv6
	case string(v1alpha1.EndpointIPAddressTypeDualstack):
		return v1alpha1.EndpointIPAddressTypeDualstack
	default:
		return ""
	}
}

func isUnrejected(conn *ec2types.VpcEndpointConnection) bool {
	state := strings.ToLower(string(conn.VpcEndpointState))
	switch state {
	case epStateAvailable, epStatePending, epStatePendingAcceptance:
		return true
	case epStateDeleting, epStateDeleted, epStateRejected, epStateFailed, epStateExpired:
	}

	return false
}
