package metrics

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

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

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/metrics"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-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"
const O11yHeaderRegionIdKey = "X-Region-Id"

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

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

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"`
}

// getClientToken get client token of o11y global server from auth0
func (m *metricsApiClient) getClientToken() (string, error) {
	o11yConfig := m.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
}

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

	var domain = o11yConfig.GlobalServerDomain
	var bizType = m.cfg.ServerConfig.Env + "-tidbcloud-nextgen"
	if resourcePool.ServicePlan == globalcommonv1.ServicePlan_BYOC {
		bizType = "byoc"
		domain = o11yConfig.GlobalServerDomainForByoc
	}

	resp, err := httputil.DoRequest(
		ctx,
		m.client,
		http.MethodGet,
		fmt.Sprintf("https://%s", domain),
		"/api/v1/clusters/",
		func(req *resty.Request) {
			req.SetAuthToken(token)
			req.ForceContentType("application/json")
			req.SetHeader(O11yHeaderOrgIdKey, strconv.FormatUint(resourcePool.OwnerId, 10))
			req.SetHeader(O11yHeaderBizTypeKey, bizType)
			req.SetQueryParams(map[string]string{
				"vendor":        resourcePool.ProviderId.String(),
				"region":        resourcePool.ResourcePool.RegionId.String(),
				"tenantID":      strconv.FormatUint(resourcePool.OwnerId, 10),
				"projectID":     "",
				"bizType":       bizType,
				"applicationID": resourcePool.DataplaneId,
			})
		},
	)
	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: %v", 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 (m *metricsApiClient) queryResourcePoolMetrics(
	ctx context.Context,
	resourcePool *entity.ResourcePool,
	expr, queryUri string,
	startTime int64,
	endTime int64,
) ([]byte, bizerr.BizError) {
	path := "query_range"
	bizType := m.cfg.ServerConfig.Env + "-tidbcloud-nextgen"
	if resourcePool.ServicePlan == globalcommonv1.ServicePlan_BYOC {
		bizType = "byoc"
	}
	regionID := resourcePool.ResourcePool.RegionId.String()
	providerPrefix := resourcePool.ProviderId.String() + "-"
	if !strings.HasPrefix(regionID, providerPrefix) {
		regionID = providerPrefix + regionID
	}
	resp, err := httputil.DoRequest(ctx, m.client, http.MethodGet, queryUri, path, func(req *resty.Request) {
		req.SetHeader("Content-Type", "application/json")
		req.SetHeader(O11yHeaderOrgIdKey, strconv.FormatUint(resourcePool.OwnerId, 10))
		req.SetHeader(O11yHeaderBizTypeKey, bizType)
		req.SetHeader(O11yHeaderRegionIdKey, regionID)
		req.SetQueryParams(map[string]string{
			"query": expr,
			"start": strconv.FormatInt(startTime, 10),
			"end":   strconv.FormatInt(endTime, 10),
			"step":  "5m",
		})
	})
	if err != nil {
		tilog.Error(ctx, "failed to query metrics",
			zap.Error(err),
			zap.String("queryUri", queryUri),
			zap.Uint64("resource_pool_id", resourcePool.Id))
		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 (m *metricsApiClient) QueryResourcePoolMetrics(ctx context.Context,
	resourcePool *entity.ResourcePool,
	expr string,
	startTime int64,
	endTime int64) ([]byte, bizerr.BizError) {
	o11yClusterQueryUri, err := m.queryO11yClusterQueryUri(ctx, resourcePool)
	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")
	}
	tilog.Info(ctx, "successfully get o11y cluster query uri", zap.String("queryUri", o11yClusterQueryUri))

	result, err := m.queryResourcePoolMetrics(ctx, resourcePool, expr, o11yClusterQueryUri, startTime, endTime)
	if err != nil {
		return nil, err
	}

	return result, nil
}
