package common

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

type Endpoint struct {
	*task.BaseTask
	endpointProvider task.VpcEndpointProvider
}

func NewEndpoint(c *task.Context, endpointProvider task.VpcEndpointProvider) *Endpoint {
	return &Endpoint{
		BaseTask:         &task.BaseTask{Context: c},
		endpointProvider: endpointProvider,
	}
}

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

func (t *Endpoint) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	conns, err := t.endpointProvider.GetAllVPCEndpointConns(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	allowed := t.PrivateLinkService.Spec.Connections
	_, unaccepted, unrejected := calculateConnsStatus(allowed, conns)
	if len(unaccepted) > 0 {
		if err := t.endpointProvider.AcceptVPCEndpoints(ctx, unaccepted); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}

	if len(unrejected) > 0 {
		if err := t.endpointProvider.RejectVPCEndpoints(ctx, 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.endpointProvider.GetAllVPCEndpointConns(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	allowed := t.PrivateLinkService.Spec.Connections
	connsStatus, unaccepted, unrejected := calculateConnsStatus(allowed, conns)
	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.setConnectionStatus(ctx, connsStatus); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *Endpoint) setConnectionStatus(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 {
	// AKS must delete private endpoint connections from the provider side,
	// because Azure blocks PrivateLinkService deletion while connection objects still exist.
	// AWS and Alicloud keep the historical behavior here: they only reject connections during
	// teardown, and do not force-delete the consumer-side endpoint connection objects.
	if deleter, ok := t.endpointProvider.(task.VpcEndpointForceDeleter); ok && deleter.ShouldForceDeleteVPCEndpoints() {
		if err := t.deleteAllVPCEndpoints(ctx, deleter); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		return deploy.Done()
	}
	if t.PrivateLinkService.Status.EndpointService.ServiceID != "" {
		if err := t.rejectAllVPCEndpoints(ctx); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}

func (t *Endpoint) PostDelete(ctx context.Context) deploy.Result {
	if deleter, ok := t.endpointProvider.(task.VpcEndpointForceDeleter); ok && deleter.ShouldForceDeleteVPCEndpoints() {
		if err := t.waitForDeletedVPCEndpoints(ctx); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}

func (t *Endpoint) deleteAllVPCEndpoints(ctx context.Context, deleter task.VpcEndpointForceDeleter) error {
	conns, err := t.endpointProvider.GetAllVPCEndpointConns(ctx)
	if err != nil {
		return err
	}
	if len(conns) == 0 {
		return nil
	}
	t.Logger.Info("deleting vpc endpoint connections", "deleteList", conns)
	return deleter.DeleteVPCEndpoints(ctx, conns)
}

func (t *Endpoint) waitForDeletedVPCEndpoints(ctx context.Context) error {
	conns, err := t.endpointProvider.GetAllVPCEndpointConns(ctx)
	if err != nil {
		return err
	}
	if len(conns) > 0 {
		return fmt.Errorf("vpc endpoint connections still exist: %d", len(conns))
	}
	return nil
}

func (t *Endpoint) rejectAllVPCEndpoints(ctx context.Context) error {
	conns, err := t.endpointProvider.GetAllVPCEndpointConns(ctx)
	if err != nil {
		return err
	}
	var rejectList []task.VpcEndpointConnection
	for _, conn := range conns {
		t.Logger.Info("checking vpc endpoint", "conn", conn)
		if isNotRejectedConn(&conn) {
			rejectList = append(rejectList, conn)
		}
	}
	t.Logger.Info("rejecting vpc endpoints", "rejectList", rejectList)
	if len(rejectList) == 0 {
		return nil
	}
	if err := t.endpointProvider.RejectVPCEndpoints(ctx, rejectList); err != nil {
		return err
	}

	return nil
}

func calculateConnsStatus(allowed []v1alpha1.Connection, conns []task.VpcEndpointConnection) (connsStatus []v1alpha1.ConnectionStatus, unaccepted, notRejected []task.VpcEndpointConnection) {
	connsMap := make(map[string]*task.VpcEndpointConnection, len(conns))
	allowedMap := make(map[string]struct{}, len(allowed))
	for i := range conns {
		connsMap[conns[i].EndpointID] = &conns[i]
	}

	connsStatus = make([]v1alpha1.ConnectionStatus, 0, len(allowed))

	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.Phase = conn.Phase
		status.Message = conn.Message
		allowedMap[item.Endpoint] = struct{}{}
		connsStatus = append(connsStatus, status)
		if status.Phase == v1alpha1.ConnectionPhasePending {
			unaccepted = append(unaccepted, *conn)
		}
	}

	for _, conn := range conns {
		if _, ok := allowedMap[conn.EndpointID]; !ok {
			// if the conn is not in allowed list and is approved, it should be rejected
			if conn.Phase == v1alpha1.ConnectionPhaseActive {
				notRejected = append(notRejected, conn)
			}
		}
	}

	return connsStatus, unaccepted, notRejected
}

func isNotRejectedConn(conn *task.VpcEndpointConnection) bool {
	switch conn.Phase {
	case v1alpha1.ConnectionPhasePending, v1alpha1.ConnectionPhaseActive:
		return true
	default:
		return false
	}
}
