package env

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"k8s.io/utils/pointer"

	"github.com/tidbcloud/eks-provider/tests/e2e/dag"
	"github.com/tidbcloud/eks-provider/tests/e2e/data"
	"github.com/tidbcloud/eks-provider/tests/e2e/label"
	"github.com/tidbcloud/eks-provider/tests/e2e/suite"
	"github.com/tidbcloud/eks-provider/tests/e2e/utils/helper"
	"github.com/tidbcloud/eks-provider/tests/e2e/utils/wait"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

func init() {
	suite.AddEnv(
		label.New(
			NewClusterEnv(CloudAWS, InfraProviderVersionOld),
			"setup TiDB cluster with old infra provider",
		),
		label.New(
			NewClusterEnv(CloudAWS, InfraProviderVersionNew),
			"setup TiDB cluster with new infra provider",
		),
		label.New(
			NewAWSClusterWithEndUserEnv(InfraProviderVersionOld),
			"aws end user for TiDB cluster created by old infra provider",
		),
		label.New(
			NewAWSClusterWithEndUserEnv(InfraProviderVersionNew),
			"aws end user for TiDB cluster created by new infra provider",
		),
		label.New(
			NewUpdateClusterEnv(CloudAWS, InfraProviderVersionNew, data.ClusterWithExtraGroup()),
			"update cluster with extra group",
		),
	)
}

type ClusterEnv struct {
	Config  *Config
	Network *cloudv1.Network
	Cluster *cloudv1.Cluster

	Reconciler string

	cloud string
	v     InfraProviderVersion
}

func NewClusterEnv(cloud string, v InfraProviderVersion) dag.Env {
	return &ClusterEnv{
		cloud: cloud,
		v:     v,
	}
}

func (e *ClusterEnv) Name() string {
	return WithCloudAndVersion("Cluster", e.cloud, e.v)
}

func (e *ClusterEnv) Setup(ctx context.Context) error {
	cluster := data.NewDefaultCluster(e.Network)
	if err := helper.Create(ctx, e.Config.InfraAPIClient, cluster, data.WithReconciler(e.Reconciler)); err != nil {
		return err
	}

	if err := wait.WaitForClusterPhase(
		ctx,
		e.Config.InfraAPIClient,
		cluster,
		cloudv1.ClusterPhaseRunning,
		wait.LongTaskTimeout,
	); err != nil {
		return err
	}

	e.Cluster = cluster

	return nil
}

func (e *ClusterEnv) Envs() []string {
	switch e.cloud {
	case CloudAWS:
		return []string{
			WithCloudAndVersion("Network", e.cloud, e.v),
		}
	}

	panic("unsupported cloud " + e.cloud)
}

func (e *ClusterEnv) Cleanup() error {
	fmt.Println("try to clean up cluster env")
	return nil
}

// AWSCluster env for testing connections from end user
type AWSClusterWithEndUserEnv struct {
	Config  *Config
	Network *cloudv1.Network
	Cluster *cloudv1.Cluster

	EndUserVPC         *ec2v1beta1.VPC
	AWSEndUserLambdaFn string

	v InfraProviderVersion
}

func NewAWSClusterWithEndUserEnv(v InfraProviderVersion) dag.Env {
	return &AWSClusterWithEndUserEnv{v: v}
}

func (e *AWSClusterWithEndUserEnv) Name() string {
	return WithCloudAndVersion("ClusterWithEndUser", CloudAWS, e.v)
}

func (e *AWSClusterWithEndUserEnv) Setup(ctx context.Context) error {
	peer := data.NewNetworkPeer(e.Network, e.EndUserVPC)
	if err := e.Config.InfraAPIClient.Create(ctx, peer); err != nil {
		return err
	}

	if err := wait.WaitForCondition(ctx, e.Config.InfraAPIClient, peer, func() error {
		if peer.Status.Phase == "Active" {
			return nil
		}

		return fmt.Errorf("expected phase is Active, current is %s", peer.Status.Phase)
	}, wait.ShortTaskTimeout); err != nil {
		return err
	}

	ep := data.NewVPCEndpoint(e.Config.Region, e.Cluster.Status.SqlEndpoints.PrivateLinkService.AWS.ServiceName)
	if err := e.Config.InfraAPIClient.Create(ctx, ep); err != nil {
		return err
	}

	fmt.Println("wait for vpc endpoint created")

	if err := wait.WaitForCondition(ctx, e.Config.InfraAPIClient, ep, func() error {
		if name := meta.GetExternalName(ep); name == "" {
			return fmt.Errorf("vpc endpoint id is not set")
		}

		return nil
	}, wait.ShortTaskTimeout); err != nil {
		return err
	}

	fmt.Println("update cluster")

	if err := helper.TryToUpdate(ctx,
		e.Config.InfraAPIClient,
		e.Cluster,
		data.ClusterWithAllowedPrivateLinkEndpoints(meta.GetExternalName(ep)),
	); err != nil {
		return err
	}

	if err := wait.WaitForCondition(ctx, e.Config.InfraAPIClient, e.Cluster, func() error {
		if len(e.Cluster.Status.SqlEndpoints.PrivateLinkService.AWS.Connections) != 0 {
			return nil
		}
		return fmt.Errorf("connections of private link are 0")
	}, wait.ShortTaskTimeout); err != nil {
		return err
	}

	c := ec2.NewFromConfig(e.Config.AWSConfig)

	if _, err := c.ModifyVpcEndpoint(ctx, &ec2.ModifyVpcEndpointInput{
		VpcEndpointId:     pointer.String(meta.GetExternalName(ep)),
		PrivateDnsEnabled: pointer.Bool(true),
	}); err != nil {
		return err
	}

	if err := wait.WaitForCondition(ctx, e.Config.InfraAPIClient, ep, func() error {
		if ep.Status.AtProvider.State == nil || *ep.Status.AtProvider.State != "available" {
			return fmt.Errorf("vpc endpoint is not available, current is %v", ep.Status.AtProvider.State)
		}

		// 1 regional dns + 3 zonal dns + 1 private dns
		if len(ep.Status.AtProvider.DNSEntries) != 5 {
			return fmt.Errorf("expected length of dns entries are 5, current is %d", len(ep.Status.AtProvider.DNSEntries))
		}

		return nil
	}, wait.LongTaskTimeout); err != nil {
		return err
	}

	return nil
}

func (e *AWSClusterWithEndUserEnv) Envs() []string {
	return []string{
		WithCloudAndVersion("Cluster", CloudAWS, e.v),
		WithCloud("EndUser", CloudAWS),
	}
}

func (e *AWSClusterWithEndUserEnv) Cleanup() error {
	fmt.Println("try to clean up aws cluster with end user env")
	return nil
}

type UpdateClusterEnv struct {
	Config  *Config
	Network *cloudv1.Network
	Cluster *cloudv1.Cluster

	cloud   string
	v       InfraProviderVersion
	patches []data.Patch
}

func NewUpdateClusterEnv(cloud string, v InfraProviderVersion, patches ...data.Patch) dag.Env {
	return &UpdateClusterEnv{
		cloud:   cloud,
		v:       v,
		patches: patches,
	}
}

func (e *UpdateClusterEnv) Name() string {
	return WithCloudAndVersion("UpdateCluster:"+"ExtraGroup", e.cloud, e.v)
}

func (e *UpdateClusterEnv) Setup(ctx context.Context) error {
	cluster := data.NewDefaultCluster(e.Network)
	if err := helper.TryToUpdate(ctx, e.Config.InfraAPIClient, e.Cluster, e.patches...); err != nil {
		return fmt.Errorf("cannot update reconciler of cluster: %w", err)
	}
	if err := helper.TryToUpdateStatus(ctx, e.Config.InfraAPIClient, e.Cluster, data.ResetClusterStatusPhaseToCreating()); err != nil {
		return fmt.Errorf("cannot reset cluster status: %w", err)
	}

	if err := wait.WaitForClusterPhase(
		ctx,
		e.Config.InfraAPIClient,
		cluster,
		cloudv1.ClusterPhaseRunning,
		wait.LongTaskTimeout,
	); err != nil {
		return err
	}

	e.Cluster = cluster

	return nil
}

func (e *UpdateClusterEnv) Envs() []string {
	switch e.cloud {
	case CloudAWS:
		return []string{
			WithCloudAndVersion("Cluster", e.cloud, e.v),
		}
	}

	panic("unsupported cloud " + e.cloud)
}

func (e *UpdateClusterEnv) OptionalEnvs() []string {
	switch e.cloud {
	case CloudAWS:
		return []string{
			WithCloudAndVersion("ClusterWithEndUser", CloudAWS, e.v),
		}
	}

	panic("unsupported cloud " + e.cloud)
}
