package mgmt

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/errs"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/object"
	"github.com/tidbcloud/aisupport/services/jsm-syncer/config"
	commonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	defaultClusterPageSize = 50
)

type ListClustersOptions struct {
	PageSize  int
	PageToken string
	ClusterID string
}

type Cluster struct {
	ID             string
	Name           string
	Status         string
	ServicePlan    string
	TiDBCloudOrgID string
}

type ClusterPage struct {
	Clusters      []Cluster
	NextPageToken string
}

type Client interface {
	GetClusterByID(ctx context.Context, clusterID string) (*Cluster, error)
	ListClustersByOrgID(ctx context.Context, orgID string, opts ListClustersOptions) (ClusterPage, error)
}

type grpcClient struct {
	conn    *grpc.ClientConn
	timeout time.Duration
	cluster mgmtv1.ClusterServiceClient
}

func NewClient(conf config.MgmtConfig) (Client, error) {
	if strings.TrimSpace(conf.GRPCAddress) == "" {
		return NewUnimplementedClient(), nil
	}

	conn, err := grpc.NewClient(
		strings.TrimSpace(conf.GRPCAddress),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
	)
	if err != nil {
		return nil, fmt.Errorf("dial mgmt grpc %s: %w", conf.GRPCAddress, err)
	}

	return &grpcClient{
		conn:    conn,
		timeout: conf.Timeout,
		cluster: mgmtv1.NewClusterServiceClient(conn),
	}, nil
}

type UnimplementedClient struct{}

func NewUnimplementedClient() Client {
	return UnimplementedClient{}
}

func (UnimplementedClient) GetClusterByID(ctx context.Context, clusterID string) (*Cluster, error) {
	return nil, errs.APIErrorf(http.StatusNotImplemented, "mgmt cluster client is not implemented yet")
}

func (UnimplementedClient) ListClustersByOrgID(ctx context.Context, orgID string, opts ListClustersOptions) (ClusterPage, error) {
	return ClusterPage{}, errs.APIErrorf(http.StatusNotImplemented, "mgmt cluster client is not implemented yet")
}

func (c *grpcClient) GetClusterByID(ctx context.Context, clusterID string) (*Cluster, error) {
	clusterID = strings.TrimSpace(clusterID)
	if clusterID == "" {
		return nil, errs.APIErrorf(http.StatusBadRequest, "cluster_id is required")
	}

	id, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		return nil, errs.APIErrorNew(http.StatusBadRequest, err)
	}
	if id == 0 {
		return nil, errs.APIErrorf(http.StatusBadRequest, "cluster_id must be positive")
	}
	clusterID = strconv.FormatUint(id, 10)

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	listResp, err := c.cluster.ListClusters(ctx, &mgmtv1.ListClustersRequest{
		Filter:   c.newClusterFilter(0, id, nil),
		PageSize: 1,
		Page:     1,
	})
	if err != nil {
		return nil, fmt.Errorf("mgmt ListClusters by cluster ID: %w", err)
	}

	for _, cluster := range readClusters(listResp.GetClusters()) {
		if cluster.ID == clusterID {
			return &cluster, nil
		}
	}
	return nil, nil
}

func (c *grpcClient) ListClustersByOrgID(ctx context.Context, orgID string, opts ListClustersOptions) (ClusterPage, error) {
	if opts.PageSize == 0 {
		opts.PageSize = defaultClusterPageSize
	}

	pagination, err := ResolveOffsetPagination(orgID, opts)
	if err != nil {
		return ClusterPage{}, err
	}

	tenantID, err := strconv.ParseUint(strings.TrimSpace(orgID), 10, 64)
	if err != nil {
		return ClusterPage{}, errs.APIErrorNew(http.StatusBadRequest, err)
	}
	clusterID, err := parseOptionalUintID(opts.ClusterID)
	if err != nil {
		return ClusterPage{}, errs.APIErrorNew(http.StatusBadRequest, err)
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	filter := c.newClusterFilter(tenantID, clusterID, object.Pointer(false))
	listResp, err := c.cluster.ListClusters(ctx, &mgmtv1.ListClustersRequest{
		Filter:   filter,
		PageSize: uint32(pagination.PageSize),
		Page:     uint32(pagination.Page),
	})
	if err != nil {
		return ClusterPage{}, fmt.Errorf("mgmt ListClusters: %w", err)
	}

	countResp, err := c.cluster.CountClusters(ctx, &mgmtv1.CountClustersRequest{Filter: filter})
	if err != nil {
		return ClusterPage{}, fmt.Errorf("mgmt CountClusters: %w", err)
	}

	hasMore := pagination.Page*pagination.PageSize < int(countResp.GetCount())
	nextToken, err := EncodeNextOffsetPageToken(orgID, opts, pagination.Page, hasMore)
	if err != nil {
		return ClusterPage{}, err
	}

	return ClusterPage{
		Clusters:      readClusters(listResp.GetClusters()),
		NextPageToken: nextToken,
	}, nil
}

func (c *grpcClient) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
	if c.timeout <= 0 {
		return context.WithCancel(ctx)
	}
	return context.WithTimeout(ctx, c.timeout)
}

func (c *grpcClient) newClusterFilter(tenantID uint64, clusterID uint64, deleted *bool) *mgmtv1.ClusterFilter {
	filter := &mgmtv1.ClusterFilter{
		View:    mgmtv1.ClusterFilter_CLUSTER_VIEW_BASIC,
		Deleted: deleted,
	}
	if tenantID > 0 {
		filter.TenantIds = []uint64{tenantID}
	}
	if clusterID > 0 {
		filter.ClusterIds = []uint64{clusterID}
	}
	return filter
}

func parseOptionalUintID(raw string) (uint64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, nil
	}
	return strconv.ParseUint(raw, 10, 64)
}

func readClusters(items []*mgmtv1.Cluster) []Cluster {
	clusters := make([]Cluster, 0, len(items))
	for _, item := range items {
		cluster := Cluster{
			ID:          strconv.FormatUint(item.GetClusterId(), 10),
			Name:        item.GetClusterName(),
			Status:      readStatus(item),
			ServicePlan: servicePlanName(item.GetServicePlan()),
		}
		if orgID := item.GetOrganizationId(); orgID > 0 {
			cluster.TiDBCloudOrgID = strconv.FormatUint(orgID, 10)
		}
		clusters = append(clusters, cluster)
	}
	return clusters
}

func readStatus(cluster *mgmtv1.Cluster) string {
	switch cluster.GetDeployType() {
	case mgmtv1.DeployType_DEPLOY_TYPE_DEDICATED:
		if dedicated := cluster.GetDedicatedCluster(); dedicated != nil {
			return dedicated.GetState().String()
		}
	case mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS:
		if serverless := cluster.GetServerlessCluster(); serverless != nil {
			return strings.TrimPrefix(serverless.GetUserStatus().String(), "USER_CLUSTER_STATUS_")
		}
	case mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN:
		if nextGen := cluster.GetNextGenCluster(); nextGen != nil {
			return nextGen.GetState().String()
		}
	}

	return ""
}

func servicePlanName(plan commonv1beta1.ServicePlan) string {
	if name := commonv1beta1.ServicePlan_name[int32(plan)]; name != "" {
		return name
	}
	return commonv1beta1.ServicePlan_SERVICE_PLAN_UNSPECIFIED.String()
}
