package metrics

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/clustermetric"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/httputil"
)

const O11yHeaderOrgIdKey = "X-Org-Id"
const O11yHeaderBizTypeKey = "X-Biz-Type"

type metricsApiClient struct {
	client *resty.Client
	cfg    *config.Config
}

func NewMetricsClient(cfg *config.Config) clustermetric.Client {
	client := resty.New().
		SetTimeout(time.Duration(cfg.RegionServerConfig.RequestTimeoutInSec) * time.Second).
		SetRetryWaitTime(time.Duration(cfg.RegionServerConfig.RetryWaitTimeoutInSec) * time.Second).
		SetRetryCount(cfg.RegionServerConfig.RetryCount)
	imp := &metricsApiClient{
		client: client,
		cfg:    cfg,
	}
	return imp
}

// getClientToken get client token of o11y global server from auth0
func (api *metricsApiClient) getClientToken() (string, error) {
	o11yConfig := api.cfg.ServerConfig.DependentSvcAddresses.O11yConfig
	auth0ClientSecret := o11yConfig.Auth0Secret

	token, err := auth0.GetClientToken(o11yConfig.Auth0Domain, o11yConfig.Auth0ClientID,
		auth0ClientSecret, o11yConfig.Auth0Audience)
	if err != nil {
		return "", err
	}

	return token, nil
}

type O11yCluster struct {
	ApplicationID string `json:"applicationID"`
	BizType       string `json:"bizType"`
	ID            string `json:"id"`
	ProjectID     string `json:"projectID"`
	Region        string `json:"region"`
	TenantID      string `json:"tenantID"`
	Vendor        string `json:"vendor"`
	InterReadUri  string `json:"internalReadUri"`
}

type O11yClusterResponse struct {
	Code int  `json:"code"`
	Data Data `json:"data"`
}

type Data struct {
	Clusters []O11yCluster `json:"clusters"`
}

func (api *metricsApiClient) queryO11yClusterQueryUri(
	ctx context.Context,
	cluster *entity.Cluster,
) (string, bizerr.BizError) {
	token, err := api.getClientToken()
	if err != nil {
		return "", bizerr.New(bizerr.ErrCodeSvcO11y, "failed to get auth0 token, %s", err.Error())
	}
	o11yConfig := api.cfg.ServerConfig.DependentSvcAddresses.O11yConfig

	resp, err := httputil.DoRequest(
		ctx,
		api.client,
		http.MethodGet,
		fmt.Sprintf("https://%s", o11yConfig.GlobalServerDomain),
		"/api/v1/clusters/",
		func(req *resty.Request) {
			req.SetAuthToken(token)
			req.ForceContentType("application/json")
			req.SetHeader(O11yHeaderOrgIdKey, strconv.FormatUint(cluster.OrganizationId, 10))
			req.SetHeader(O11yHeaderBizTypeKey, o11yConfig.BizType)
			req.SetQueryParams(map[string]string{
				"vendor":        cluster.CloudProviderId.String(),
				"region":        cluster.RegionId.String(),
				"tenantID":      strconv.FormatUint(cluster.OrganizationId, 10),
				"projectID":     strconv.FormatUint(cluster.ProjectId, 10),
				"bizType":       o11yConfig.BizType,
				"applicationID": strconv.FormatUint(cluster.ClusterId, 10),
			})
		},
	)
	if err != nil {
		return "", bizerr.New(bizerr.ErrCodeSvcO11y, "failed to query o11y cluster, %s", err.Error())
	}
	if resp.StatusCode() != http.StatusOK {
		return "", bizerr.New(bizerr.ErrCodeSvcO11y, "failed to query o11y cluster, body: %s", resp.String())
	}

	result := &O11yClusterResponse{}
	if err := json.Unmarshal(resp.Body(), result); err != nil {
		return "", bizerr.New(bizerr.ErrCodeSvcO11y, "failed to unmarshal response body: %s", err)
	}

	if len(result.Data.Clusters) == 0 {
		return "", bizerr.New(bizerr.ErrCodeSvcO11y, "failed to query o11y cluster, no cluster found")
	}

	return result.Data.Clusters[0].InterReadUri, nil
}

func (api *metricsApiClient) queryClusterMetrics(
	ctx context.Context,
	cluster *entity.Cluster,
	expr, queryUri string,
) ([]byte, bizerr.BizError) {
	path := "query"
	resp, err := httputil.DoRequest(ctx, api.client, http.MethodGet, queryUri, path, func(req *resty.Request) {
		req.SetHeader("Content-Type", "application/json")
		req.SetQueryParams(map[string]string{
			"query": expr,
		})
	})
	if err != nil {
		tilog.Error(ctx, "failed to query metrics",
			zap.Error(err),
			zap.String("queryUri", queryUri),
			zap.Uint64("cluster_id", cluster.ClusterId))
		return nil, bizerr.New(bizerr.ErrCodeSvcO11y, "failed to query metrics, %s", err.Error())
	}
	if resp.StatusCode() != http.StatusOK {
		return nil, bizerr.New(bizerr.ErrCodeSvcO11y, "failed to query metrics, body: %s", resp.String())
	}

	return resp.Body(), nil
}

func (api *metricsApiClient) QueryClusterMetrics(
	ctx context.Context,
	cluster *entity.Cluster,
	expr string,
) ([]byte, bizerr.BizError) {
	o11yClusterQueryUri, err := api.queryO11yClusterQueryUri(ctx, cluster)
	if err != nil {
		return nil, err
	}
	if o11yClusterQueryUri == "" {
		tilog.Error(ctx, "o11y cluster query uri is empty")
		return nil, bizerr.New(bizerr.ErrCodeSvcO11y, "fail to get o11y data query uri")
	}

	result, err := api.queryClusterMetrics(ctx, cluster, expr, o11yClusterQueryUri)
	if err != nil {
		return nil, err
	}

	return result, nil
}
