package o11y

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

	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/cache"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/config"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	clinictracing "github.com/pingcap-inc/clinic/clinic_data_service/pkg/tracing"
	"go.uber.org/zap"
)

// type Vendor string
// type BizType string
// type ApplicationID string

// const (
// 	VendorAWS             Vendor        = "aws"
// 	VendorAliCloud        Vendor        = "alicloud"
// 	BizTypeTidbCloud      BizType       = "tidbcloud"
// 	BizTypeNextGen        BizType       = "prod-tidbcloud-nextgen"
// 	ApplicationIDK8sInfra ApplicationID = "k8s-infra"
// )

// func (v Vendor) IsValid() bool        { return v == VendorAWS || v == VendorAliCloud }
// func (b BizType) IsValid() bool       { return b == BizTypeTidbCloud || b == BizTypeNextGen }
// func (a ApplicationID) IsValid() bool { return a != "" }

type ClusterEntity struct {
	ID                 string   `json:"id"`
	ApplicationID      string   `json:"applicationID"`
	InternalReadUri    string   `json:"internalReadUri"`
	OpenReadUri        string   `json:"openReadUri,omitempty"`
	ProjectID          string   `json:"projectID,omitempty"`
	Region             string   `json:"region,omitempty"`
	Vendor             string   `json:"vendor,omitempty"`
	BizType            string   `json:"bizType,omitempty"`
	ShareMode          string   `json:"shareMode,omitempty"`
	Note               string   `json:"note,omitempty"`
	Version            string   `json:"version,omitempty"`
	DataPropertyRights int      `json:"dataPropertyRights,omitempty"`
	Tags               []string `json:"tags,omitempty"`
	TenantID           string   `json:"tenantID,omitempty"`
}

type clustersResp struct {
	Code int `json:"code"`
	Data struct {
		Clusters []ClusterEntity `json:"clusters"`
	} `json:"data"`
	Message string `json:"message,omitempty"`
}

func doGET(ctx context.Context, u string, headers map[string]string) ([]byte, int, error) {
	client := &http.Client{
		Timeout:   30 * time.Second,
		Transport: clinictracing.NewHTTPTransport(nil),
	}
	req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
	if err != nil {
		return nil, 0, err
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, 0, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, 0, err
	}
	return body, resp.StatusCode, nil
}

type QueryClusterReq struct {
	Vendor        string
	Region        string
	BizType       string
	TenantID      string
	ApplicationID string
	ProjectID     string
}

func GetPrometheusURL(ctx context.Context, p QueryClusterReq) (string, error) {

	reqURL, headerMap := buildClustersParams(p)
	cacheKey := "url" + reqURL
	prometheusURL, err := cache.GetWithCache(ctx, cache.ClusterCache, cacheKey, func() (string, error) {
		return GetPrometheusURLByReqURL(ctx, reqURL, p.ApplicationID, headerMap)
	})
	if err != nil {
		log.Error(ctx, fmt.Sprintf("get [%s] from cache failed", reqURL), zap.Error(err))
		return "", err
	}
	return prometheusURL, nil
}
func GetCluster(ctx context.Context, p QueryClusterReq) (ClusterEntity, error) {

	reqURL, headerMap := buildClustersParams(p)

	clusterEntity, err := cache.GetWithCache(ctx, cache.ClusterCache, reqURL, func() (ClusterEntity, error) {
		return getClusterEntity(ctx, reqURL, p.ApplicationID, headerMap)
	})
	if err != nil {
		log.Error(ctx, fmt.Sprintf("get [%s] from cache failed", reqURL), zap.Error(err))
		return ClusterEntity{}, err
	}
	return clusterEntity, nil
}

func GetO11yToken(ctx context.Context) (string, error) {
	o11y := &config.AppConfig.O11yConfig
	client := &http.Client{
		Timeout:   30 * time.Second,
		Transport: clinictracing.NewHTTPTransport(nil),
	}
	data := url.Values{}
	data.Set("grant_type", "client_credentials")
	data.Set("client_id", o11y.Auth0ClientId)
	data.Set("client_secret", o11y.Auth0ClientSecret)
	data.Set("audience", o11y.Auth0Audience)
	req, err := http.NewRequestWithContext(ctx, "POST", o11y.Auth0TokenUrl, strings.NewReader(data.Encode()))
	if err != nil {
		log.Error(ctx, "create token request failed", zap.Error(err))
		return "", err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	// log.PlainInfo("get o11y token", zap.String("url", o11y.Auth0TokenUrl))
	resp, err := client.Do(req)
	if err != nil {
		log.Error(ctx, "get o11y token failed", zap.Error(err))
		return "", err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	if resp.StatusCode != http.StatusOK {
		log.Error(ctx, "token request non-200", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(body)))
		return "", fmt.Errorf("token request failed: %d", resp.StatusCode)
	}
	var tokenResp map[string]interface{}
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return "", err
	}
	v, ok := tokenResp["access_token"].(string)
	if !ok || v == "" {
		return "", fmt.Errorf("access_token not found")
	}
	return v, nil
}

func buildClustersURL(baseHost string, apiPath string, p QueryClusterReq) string {
	u, _ := url.Parse(baseHost)
	u.Path = apiPath
	q := u.Query()
	if p.Vendor != "" {
		q.Set("vendor", string(p.Vendor))
	}
	if p.Region != "" {
		q.Set("region", p.Region)
	}
	if p.BizType != "" {
		q.Set("bizType", string(p.BizType))
	}
	if p.TenantID != "" {
		q.Set("tenantID", p.TenantID)
	}
	if p.ApplicationID != "" {
		q.Set("applicationID", p.ApplicationID)
	}

	if p.ProjectID != "" {
		q.Set("projectID", p.ProjectID)
	}
	u.RawQuery = q.Encode()
	return u.String()
}

func GetPrometheusURLByReqURL(ctx context.Context, reqURL string, applicationID string, headerMap map[string]string) (string, error) {
	c, err := getClusterEntity(ctx, reqURL, applicationID, headerMap)
	if err != nil {
		return "", err
	}
	// maybe the cluster is deleted
	if c.InternalReadUri == "" {
		return "", nil
	}
	prometheusURL := strings.ReplaceAll(c.InternalReadUri, "/api/v1/", "")
	if !strings.HasPrefix(prometheusURL, "http://") && !strings.HasPrefix(prometheusURL, "https://") {
		prometheusURL = "https://" + prometheusURL
	}

	return prometheusURL, nil
}

func getClusterEntity(ctx context.Context, reqURL string, applicationID string, headerMap map[string]string) (ClusterEntity, error) {
	token, err := GetO11yToken(ctx)
	if err != nil {
		return ClusterEntity{}, err
	}
	headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)}
	for k, v := range headerMap {
		headers[k] = v
	}
	log.Info(ctx, "query o11y clusters", zap.String("url", reqURL))
	body, status, err := doGET(ctx, reqURL, headers)
	if err != nil {
		return ClusterEntity{}, fmt.Errorf("request failed: %w", err)
	}
	if status != http.StatusOK {
		log.Error(ctx, "clusters request non-200", zap.Int("statusCode", status), zap.String("response", string(body)))
		return ClusterEntity{}, fmt.Errorf("status %d: %s", status, string(body))
	}
	var resp clustersResp
	if err := json.Unmarshal(body, &resp); err != nil {
		return ClusterEntity{}, fmt.Errorf("unmarshal failed: %w", err)
	}
	log.Info(ctx, "o11y clusters response", zap.Any("resp", resp))
	if resp.Code != 0 {
		return ClusterEntity{}, fmt.Errorf("query failed: code=%d", resp.Code)
	}
	if len(resp.Data.Clusters) == 0 {
		return ClusterEntity{}, nil
	}
	log.Info(ctx, "o11y clusters ", zap.Any("clusters size", len(resp.Data.Clusters)))
	idx := 0
	if applicationID != "" {
		for i, c := range resp.Data.Clusters {
			if c.ApplicationID == applicationID {
				idx = i
				break
			}
		}
	}

	if len(resp.Data.Clusters) > 1 {
		return ClusterEntity{}, fmt.Errorf("no clusters found for applicationID=%s", applicationID)
	}
	c := resp.Data.Clusters[idx]
	if c.InternalReadUri == "" {
		return ClusterEntity{}, fmt.Errorf("internalReadUri not found")
	}
	return c, nil
}

func buildClustersParams(p QueryClusterReq) (string, map[string]string) {
	o11yGlobalServerUrl := config.AppConfig.Clinic.O11yGlobalServerUrl
	//o11yGlobalServerUrl := config.AppConfig.O11yConfig.O11yMetadataBase

	apiPath := "api/v1/clusters/"
	reqURL := buildClustersURL(o11yGlobalServerUrl, apiPath, p)
	headerMap := map[string]string{
		"X-Region-Id": p.Region,
		"X-Org-Id":    p.TenantID,
		"X-Biz-Type":  string(p.BizType),
	}
	return reqURL, headerMap
}

func getStringValue(m map[string]interface{}, key string) string {
	if val, ok := m[key].(string); ok {
		return val
	}
	return ""
}
