package openapiv1beta

import (
	"encoding/json"
	"net/http"
	"time"

	httptransport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	"github.com/icholy/digest"
	"github.com/samber/lo"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/openapiv1beta/client"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/openapiv1beta/client/cluster"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/openapiv1beta/client/project"
)

type Cli struct {
	*client.TiDBCloudAPI

	su *suite.Suite
}

func NewClient(su *suite.Suite, host, userName, pwd string) (*Cli, error) {
	ts := &digest.Transport{
		Username: userName,
		Password: pwd,
	}

	trans := httptransport.NewWithClient(host, "", []string{"https"}, &http.Client{Transport: ts})

	return &Cli{
		su:           su,
		TiDBCloudAPI: client.New(trans, strfmt.Default),
	}, nil
}

func (cli *Cli) RespJson(resp any) string {
	b, err := json.Marshal(resp)
	require.NoErrorf(cli.su.T(), err, "marshal error: %s", err)
	return string(b)
}

func (cli *Cli) CreateAccessPointAllowErr(prjID, clusterID string) (*cluster.CreateAccesspointOKBody, error) {
	params := cluster.NewCreateAccesspointParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	params.SetBody(cluster.CreateAccesspointBody{
		DisplayName: lo.ToPtr("e2e-test"),
		PublicEndpoint: &cluster.CreateAccesspointParamsBodyPublicEndpoint{
			IPAllowlist: []*cluster.CreateAccesspointParamsBodyPublicEndpointIPAllowlistItems0{
				{
					Cidr: "0.0.0.0/0",
				},
			},
		},
	})

	resp, err := cli.Cluster.CreateAccesspoint(params)
	if err != nil {
		return nil, err
	}

	require.NoError(cli.su.T(), err)
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) CreateAccessPoint(prjID, clusterID string) (*cluster.CreateAccesspointOKBody, error) {
	resp, err := cli.CreateAccessPointAllowErr(prjID, clusterID)
	return resp, err
}

func (cli *Cli) GetAccessPointAllowErr(prjID, clusterID, accessPointID string) (*cluster.GetAccesspointOKBody, error) {
	params := cluster.NewGetAccesspointParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	if accessPointID != "" {
		params.SetAccesspointID(accessPointID)
	}

	resp, err := cli.Cluster.GetAccesspoint(params)
	if err != nil {
		return nil, err
	}

	require.NoError(cli.su.T(), err)
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) GetAccessPoint(prjID, clusterID, accessPointID string) *cluster.GetAccesspointOKBody {
	resp, err := cli.GetAccessPointAllowErr(prjID, clusterID, accessPointID)
	require.NoError(cli.su.T(), err)
	return resp
}

func (cli *Cli) ListAccessPointAllowErr(prjID, clusterID string) (*cluster.ListAccesspointsOKBody, error) {
	params := cluster.NewListAccesspointsParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	resp, err := cli.Cluster.ListAccesspoints(params)
	if err != nil {
		return nil, err
	}

	require.NoError(cli.su.T(), err)
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) ListAccessPoint(prjID, clusterID string) *cluster.ListAccesspointsOKBody {
	resp, err := cli.ListAccessPointAllowErr(prjID, clusterID)
	require.NoError(cli.su.T(), err)
	return resp
}

func (cli *Cli) DeleteAccessPoint(prjID, clusterID, accessPointID string) {
	params := cluster.NewDeleteAccesspointParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	params.SetAccesspointID(accessPointID)

	_, err := cli.Cluster.DeleteAccesspoint(params)
	require.NoError(cli.su.T(), err)
}

type ScaleTiDBConfig struct {
	NodeQuantity int32
	NodeSize     string
}

func (cli *Cli) ScaleClusterByAccessPoint(
	prjID, clusterID, accessPointID string,
	cfg *ScaleTiDBConfig,
) (*cluster.UpdateClusterOK, error) {
	params := cluster.NewUpdateClusterParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	params.SetBody(cluster.UpdateClusterBody{
		Config: &cluster.UpdateClusterParamsBodyConfig{
			Components: &cluster.UpdateClusterParamsBodyConfigComponents{
				Tidb: &cluster.UpdateClusterParamsBodyConfigComponentsTidb{
					AccesspointID: accessPointID,
					NodeQuantity:  cfg.NodeQuantity,
					NodeSize:      cfg.NodeSize,
				},
			},
		},
	})

	resp, err := cli.Cluster.UpdateCluster(params)
	return resp, err
}

func (cli *Cli) ScaleCluster(
	prjID, clusterID string,
	config *cluster.UpdateClusterParamsBodyConfigComponents,
) (*cluster.UpdateClusterOK, error) {
	params := cluster.NewUpdateClusterParams()
	if prjID != "" {
		params.SetProjectID(prjID)
	}

	if clusterID != "" {
		params.SetClusterID(clusterID)
	}

	params.SetBody(cluster.UpdateClusterBody{
		Config: &cluster.UpdateClusterParamsBodyConfig{
			Components: config,
		},
	})

	resp, err := cli.Cluster.UpdateCluster(params)
	return resp, err
}

func (cli *Cli) DeleteClusterAllowErr(clusterID, prjID string) (any, error) {
	param := cluster.NewDeleteClusterParams()
	param.SetClusterID(clusterID)
	param.SetProjectID(prjID)
	resp, err := cli.Cluster.DeleteCluster(param)
	if err != nil {
		return nil, err
	}

	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))
	return resp.Payload, nil
}

func (cli *Cli) EnsureDeleteCluster(prjID, clusterName string) {
	param := cluster.NewListClustersOfProjectParams()
	param.SetProjectID(prjID)
	clusters, err := cli.Cluster.ListClustersOfProject(param)
	if err != nil {
		// list clusters failed, nothing to do
		return
	}

	cls, ok := lo.Find(clusters.Payload.Items, func(c *cluster.ListClustersOfProjectOKBodyItemsItems0) bool {
		return c != nil && *c.ID != "" && c.Name == clusterName
	})

	if !ok {
		// cluster not found, nothing to do
		return
	}

	paramDelete := cluster.NewDeleteClusterParams()
	paramDelete.SetClusterID(*cls.ID)
	paramDelete.SetProjectID(prjID)
	_, err = cli.Cluster.DeleteCluster(paramDelete)
	require.NoError(cli.su.T(), err)
}

func (cli *Cli) ListClustersAllowErr(prjID string) (*cluster.ListClustersOfProjectOKBody, error) {
	params := cluster.NewListClustersOfProjectParams()
	params.SetProjectID(prjID)
	resp, err := cli.Cluster.ListClustersOfProject(params)
	if err != nil {
		return nil, err
	}

	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) CreateClusterAllowErr(
	prjID string,
	cls cluster.CreateClusterBody,
) (*cluster.CreateClusterOKBody, error) {
	time.Sleep(1 * time.Second)
	params := cluster.NewCreateClusterParams()
	params.SetBody(cls)
	params.SetProjectID(prjID)

	resp, err := cli.Cluster.CreateCluster(params)
	if err != nil {
		return nil, err
	}
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) CreateCluster(prjID string, cls cluster.CreateClusterBody) string {
	resp, err := cli.CreateClusterAllowErr(prjID, cls)
	require.NoError(cli.su.T(), err)
	require.NotEmptyf(cli.su.T(), &resp.ID, "cluster id not found: %s", cli.RespJson(resp))

	return *resp.ID
}

func (cli *Cli) PollClusterStatus(prjID, clusterID string) *cluster.GetClusterOKBodyStatus {
	cls, err := cli.GetClusterAllowErr(prjID, clusterID)
	require.NoError(cli.su.T(), err)
	require.NotEmptyf(cli.su.T(), *cls.Status, "cluster status not found: %s", clusterID)
	return cls.Status
}

func (cli *Cli) GetClusterAllowErr(prjID, clusterID string) (*cluster.GetClusterOKBody, error) {
	param := cluster.NewGetClusterParams()
	param.SetClusterID(clusterID)
	param.SetProjectID(prjID)
	resp, err := cli.Cluster.GetCluster(param)
	if err != nil {
		return nil, err
	}

	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload, nil
}

func (cli *Cli) GetCluster(prjID, clusterID string) *cluster.GetClusterOKBody {
	resp, err := cli.GetClusterAllowErr(prjID, clusterID)
	require.NoError(cli.su.T(), err)
	return resp
}

func (cli *Cli) CreateProject(name string, awsCmekEnabled bool) string {
	params := project.NewCreateProjectParams()
	params.SetBody(project.CreateProjectBody{
		AwsCmekEnabled: lo.ToPtr(awsCmekEnabled),
		Name:           lo.ToPtr(name),
	})

	resp, err := cli.Project.CreateProject(params)
	require.NoError(cli.su.T(), err)
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))
	require.NotEmptyf(cli.su.T(), resp.Payload, "invalid response: %s", cli.RespJson(resp))
	require.NotEmptyf(cli.su.T(), &resp.Payload.ID, "project id not found: %s", cli.RespJson(resp))

	return *resp.Payload.ID
}

func (cli *Cli) ListClusterPrivateLinkEndpoints(projectID, clusterID string) *cluster.ListPrivateEndpointsOKBody {
	params := cluster.NewListPrivateEndpointsParams()
	params.SetProjectID(projectID)
	params.SetClusterID(clusterID)
	resp, err := cli.Cluster.ListPrivateEndpoints(params)
	require.NoError(cli.su.T(), err)
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload
}

func (cli *Cli) CreatePrivateLinkEndpoint(projectID, clusterID, accesspointID, endpointName string) string {
	params := cluster.NewCreatePrivateLinkEndpointParams()
	params.SetProjectID(projectID)
	params.SetClusterID(clusterID)
	params.SetAccesspointID(accesspointID)
	params.SetBody(cluster.CreatePrivateLinkEndpointBody{
		EndpointName: lo.ToPtr(endpointName),
	})
	resp, err := cli.Cluster.CreatePrivateLinkEndpoint(params)
	require.NoError(cli.su.T(), err)
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))

	return lo.FromPtr(resp.Payload.ID)
}

func (cli *Cli) GetPrivateLinkEndpoint(
	projectID, clusterID, accesspointID, privateLinkEndpointID string,
) *cluster.GetPrivateLinkEndpointOKBody {
	params := cluster.NewGetPrivateLinkEndpointParams()
	params.SetProjectID(projectID)
	params.SetClusterID(clusterID)
	params.SetAccesspointID(accesspointID)
	params.SetPrivateLinkEndpointID(privateLinkEndpointID)
	resp, err := cli.Cluster.GetPrivateLinkEndpoint(params)
	require.NoError(cli.su.T(), err)
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload
}

func (cli *Cli) ListPrivateLinkEndpoints(
	projectID, clusterID, accesspointID string,
) *cluster.ListPrivateLinkEndpointsOKBody {
	params := cluster.NewListPrivateLinkEndpointsParams()
	params.SetProjectID(projectID)
	params.SetClusterID(clusterID)
	params.SetAccesspointID(accesspointID)
	resp, err := cli.Cluster.ListPrivateLinkEndpoints(params)
	require.NoError(cli.su.T(), err)
	require.Falsef(cli.su.T(), resp == nil || resp.Payload == nil, "invalid response: %s", cli.RespJson(resp))

	return resp.Payload
}
