package nextgen_client

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

var defaultProtojsonUnmarshalOptions = protojson.UnmarshalOptions{
	// enable this filed to enhance compatibility in where the upstream adds some new fields.
	DiscardUnknown: true,
}

type Component_Storage struct {
	Name          string `json:"name,omitempty"`
	Type          string `json:"type,omitempty"`
	SizeGi        uint32 `json:"sizeGi,omitempty"`
	Iops          uint32 `json:"iops,omitempty"`
	ThroughputMbs uint32 `json:"throughputMbs,omitempty"`
	MountPath     string `json:"mountPath,omitempty"`
}

type NodeSpec struct {
	Cpu      uint32 `json:"cpu,omitempty"`
	MemoryGi uint32 `json:"memoryGi,omitempty"`
}

type NodeQuota struct {
	NodeSpec  *NodeSpec `json:"nodeSpec,omitempty"`
	NodeCount uint32    `json:"nodeCount,omitempty"`
	// Read Only: true
	MaxNodeCount uint32 `json:"maxNodeCount,omitempty"`
	// Read Only: true
	MinNodeCount uint32 `json:"minNodeCount,omitempty"`
}

type Component struct {
	Id            string               `json:"id,omitempty"`
	ComponentType string               `json:"componentType,omitempty"`
	NodeQuota     *NodeQuota           `json:"nodeQuota,omitempty"`
	Storages      []*Component_Storage `json:"storages,omitempty"`
	// the component configuration
	// include the image repo and image tag
	Configuration *Component_Configuration `json:"configuration,omitempty"`
	Version       string                   `json:"version,omitempty"`
	Phase         string                   `json:"phase,omitempty"`
}

type Component_Configuration struct {
	ImageRepo string `json:"imageRepo,omitempty"`
	ImageTag  string `json:"imageTag,omitempty"`
}

type SharedPool struct {
	Id              string       `json:"id,omitempty"`
	ComponentGroups []*Component `json:"componentGroups,omitempty"`
	Version         string       `json:"version,omitempty"`
	// the object storage is used to store the cloud storage engine.
	ObjectStorage *SharedPool_ObjectStorage `json:"objectStorage,omitempty"`
	// the internal object storage is used to tidb global sort and sql audit log.
	InternalStorage *SharedPool_ObjectStorage `json:"internalStorage,omitempty"`
	DataplaneName   string                    `json:"dataplaneName,omitempty"`
	Phase           string                    `json:"phase,omitempty"`
}

type SharedPool_ObjectStorage struct {
	Region string `json:"region,omitempty"`
	Bucket string `json:"bucket,omitempty"`
	Prefix string `json:"prefix,omitempty"`
}

type ListTiDBGroupsResp struct {
	TidbGroups []*TiDBGroup `json:"tidbGroups,omitempty"`
	TotalCount int          `json:"totalCount,omitempty"`
}

type TiDBGroup struct {
	Id           string     `json:"id,omitempty"`
	ClusterID    string     `json:"clusterId,omitempty"`
	TenantID     string     `json:"tenantId,omitempty"`
	SharedPoolID string     `json:"sharedPoolId,omitempty"`
	NodeQuota    *NodeQuota `json:"nodeQuota,omitempty"`
	Phase        string     `json:"phase,omitempty"`
	// ...
}

type PlanReplayerFile struct {
	FileName     string `json:"fileName,omitempty"`
	SizeBytes    string `json:"sizeBytes,omitempty"`
	LastModified string `json:"lastModified,omitempty"`
	StorageURI   string `json:"storageUri,omitempty"`
}

type ListPlanReplayerFilesResponse struct {
	Files         []*PlanReplayerFile `json:"files,omitempty"`
	NextPageToken string              `json:"nextPageToken,omitempty"`
}

type GeneratePlanReplayerFileDownloadURLRequest struct {
	FileName string `json:"fileName"`
}

type GeneratePlanReplayerFileDownloadURLResponse struct {
	DownloadURL string `json:"downloadUrl,omitempty"`
	ExpireTime  string `json:"expireTime,omitempty"`
}

type Client struct {
	client *resty.Client
}

func NewClient(addr, token string) *Client {
	client := resty.New().
		SetBaseURL(addr).
		SetAuthToken(token).
		SetTimeout(time.Second * 100).
		SetJSONMarshaler(func(obj any) ([]byte, error) {
			if objP, ok := obj.(proto.Message); ok {
				return protojson.Marshal(objP)
			}

			return json.Marshal(obj)
		}).
		SetJSONUnmarshaler(func(data []byte, obj any) error {
			if objP, ok := obj.(proto.Message); ok {
				return defaultProtojsonUnmarshalOptions.Unmarshal(data, objP)
			}

			return json.Unmarshal(data, obj)
		}).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			fields := []zap.Field{
				zap.String("method", r.Request.Method),
				zap.String("url", r.Request.URL),
				zap.String("status", r.Status()),
			}
			// A successful GeneratePlanReplayerFileDownloadURL response contains a
			// pre-signed URL and its temporary credentials. Keep the request
			// observable without allowing that URL into Clinic logs.
			if !strings.Contains(r.Request.URL, ":generatePlanReplayerFileDownloadURL") {
				fields = append(fields, zap.ByteString("body", r.Body()))
			}
			log.Debug("Called nextgen proxy http", fields...)

			if !r.IsSuccess() {
				err := fmt.Errorf("status: %s, body: %s", r.Status(), string(r.Body()))
				return err
			}
			return nil
		})

	return &Client{
		client: client,
	}
}

func (g *Client) GetSharedPool(ctx context.Context, sharedPoolID uint64) (*SharedPool, error) {
	result := &SharedPool{}
	_, err := g.client.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"shared_pool_id": strconv.FormatUint(sharedPoolID, 10),
		}).
		SetResult(result).
		Get("/proxy/nextgen/resourcePools/{shared_pool_id}/api/premium/v1/sharedPools/{shared_pool_id}")
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (g *Client) ListTiDBGroups(ctx context.Context, clusterID uint64) ([]*TiDBGroup, error) {
	result := &ListTiDBGroupsResp{}
	_, err := g.client.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"cluster_id": strconv.FormatUint(clusterID, 10),
		}).
		SetResult(result).
		Get("/proxy/nextgen/clusters/{cluster_id}/api/premium/v1/clusters/{cluster_id}/tidbGroups")
	if err != nil {
		return nil, err
	}

	return result.TidbGroups, nil
}

func (g *Client) ListPlanReplayerFiles(
	ctx context.Context,
	clusterID uint64,
	pageSize int,
	pageToken string,
) (*ListPlanReplayerFilesResponse, error) {
	result := &ListPlanReplayerFilesResponse{}
	queryParams := map[string]string{}
	if pageSize > 0 {
		queryParams["pageSize"] = strconv.Itoa(pageSize)
	}
	if pageToken != "" {
		queryParams["pageToken"] = pageToken
	}

	_, err := g.client.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"cluster_id": strconv.FormatUint(clusterID, 10),
		}).
		SetQueryParams(queryParams).
		SetResult(result).
		Get("/proxy/nextgen/clusters/{cluster_id}/cluster-ng-server/api/v1beta/clusters/{cluster_id}/planReplayerFiles")
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (g *Client) GeneratePlanReplayerFileDownloadURL(
	ctx context.Context,
	clusterID uint64,
	req *GeneratePlanReplayerFileDownloadURLRequest,
) (*GeneratePlanReplayerFileDownloadURLResponse, error) {
	result := &GeneratePlanReplayerFileDownloadURLResponse{}
	_, err := g.client.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"cluster_id": strconv.FormatUint(clusterID, 10),
		}).
		SetHeader("Content-Type", "application/json").
		SetBody(req).
		SetResult(result).
		Post("/proxy/nextgen/clusters/{cluster_id}/cluster-ng-server/api/v1beta/clusters/{cluster_id}:generatePlanReplayerFileDownloadURL")
	if err != nil {
		return nil, err
	}

	return result, nil
}
