package o11ymeta

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

	"github.com/ajg/form"
	"github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"github.com/pkg/errors"
	"github.com/tidbcloud/observability/o11ymetering/pkg/common/config"
	"github.com/tidbcloud/observability/o11ymetering/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/configs"
	o11yconstants "github.com/tidbcloud/observability/pkg/common/constants"
	o11ymessage "github.com/tidbcloud/observability/pkg/common/message"
	o11ystructs "github.com/tidbcloud/observability/pkg/common/structs"
)

type O11yMetaAPI struct {
	token           string
	tokenUpdateTime time.Time
}

var queryClusterUrlTemplate = "https://www.gs.us-west-2.aws.%s/api/v1/clusters/?vendor=%s&region=%s"
var regionalServiceUrl = "http://regionalservice.observability:8081"

var ProdDomainPostfix = "observability.tidbcloud.com"
var DevDomainPostfix = "observability-dev.pingcap.cloud"
var StagingDomainPostfix = "observability-staging.pingcap.cloud"
var urlAppendParam = "%s&%s=%s"

//var cachedInfraClusters = make(map[string]o11ystructs.ClusterBaseInfo)

func domainPostfix() string {
	switch configs.GetServiceInfo().Env {
	case o11yconstants.EnvDev:
		return DevDomainPostfix
	case o11yconstants.EnvStaging:
		return StagingDomainPostfix
	case o11yconstants.EnvProd:
		return ProdDomainPostfix
	}
	panic(fmt.Sprintf("unknown configs.GetServiceInfo().Env %s", configs.GetServiceInfo().Env))
}

func (o *O11yMetaAPI) GetDedicatedSeedCluster(ctx context.Context, env constants.BizEnv) (o11ystructs.ClusterBaseInfo, error) {
	cacheKey := fmt.Sprintf("dedicated-seed-%s", env)

	if cached, ok := cachedClusters.Get(cacheKey); ok {
		return cached.(o11ystructs.ClusterBaseInfo), nil
	}

	url := fmt.Sprintf(queryClusterUrlTemplate, domainPostfix(),
		configs.GetServiceInfo().Vendor,
		configs.GetServiceInfo().Region)

	url = fmt.Sprintf(urlAppendParam, url, "bizType", dedicatedBizType(env))
	url = fmt.Sprintf(urlAppendParam, url, "projectID", "seed")
	url = fmt.Sprintf(urlAppendParam, url, "tenantID", fmt.Sprintf("infra-%s", env))

	resp, err := o.queryCluster(ctx, url)
	if err != nil {
		return o11ystructs.ClusterBaseInfo{}, errors.New(fmt.Sprintf("GetDedicatedSeedCluster failed, env = %s", env))
	}

	if len(resp.Clusters) == 0 {
		log.Error(fmt.Sprintf("GetDedicatedSeedCluster result does not exist, url = %s", url))
		return o11ystructs.ClusterBaseInfo{}, errors.New("GetDedicatedSeedCluster result does not exist")
	}

	cachedClusters.Set(cacheKey, resp.Clusters[0], time.Hour*24)
	return resp.Clusters[0], nil
}

func (o *O11yMetaAPI) GetGlobalBaseCluster(ctx context.Context, env constants.BizEnv) (o11ystructs.ClusterBaseInfo, error) {
	cacheKey := fmt.Sprintf("dedicated-base-%s", env)
	if cached, ok := cachedClusters.Get(cacheKey); ok {
		return cached.(o11ystructs.ClusterBaseInfo), nil
	}

	url := fmt.Sprintf(queryClusterUrlTemplate, domainPostfix(),
		configs.GetServiceInfo().Vendor,
		configs.GetServiceInfo().Region)

	url = fmt.Sprintf(urlAppendParam, url, "bizType", dedicatedBizType(env))
	url = fmt.Sprintf(urlAppendParam, url, "applicationID", "base")
	url = fmt.Sprintf(urlAppendParam, url, "projectID", "base")
	url = fmt.Sprintf(urlAppendParam, url, "tenantID", fmt.Sprintf("infra-%s", env))

	resp, err := o.queryCluster(ctx, url)
	if err != nil {
		return o11ystructs.ClusterBaseInfo{}, errors.New(fmt.Sprintf("GetDedicatedSeedCluster failed, env = %s", env))
	}

	if len(resp.Clusters) == 0 {
		log.Error(fmt.Sprintf("GetDedicatedSeedCluster result does not exist, url = %s", url))
		return o11ystructs.ClusterBaseInfo{}, errors.New("GetDedicatedSeedCluster result does not exist")
	}

	cachedClusters.Set(cacheKey, resp.Clusters[0], time.Hour*24)
	return resp.Clusters[0], nil
}

func (o *O11yMetaAPI) GetServerlessInfraCluster(ctx context.Context, env constants.BizEnv) (o11ystructs.ClusterBaseInfo, error) {
	cacheKey := fmt.Sprintf("serverless-infra-%s", env)
	if cached, ok := cachedClusters.Get(cacheKey); ok {
		return cached.(o11ystructs.ClusterBaseInfo), nil
	}

	url := fmt.Sprintf(queryClusterUrlTemplate, domainPostfix(),
		configs.GetServiceInfo().Vendor,
		configs.GetServiceInfo().Region)

	url = fmt.Sprintf(urlAppendParam, url, "bizType", serverlessBizType(env))
	url = fmt.Sprintf(urlAppendParam, url, "applicationID", "devtier-infra")
	url = fmt.Sprintf(urlAppendParam, url, "tenantID", fmt.Sprintf("%s-devtier", env))

	resp, err := o.queryCluster(ctx, url)
	if err != nil {
		return o11ystructs.ClusterBaseInfo{}, errors.New(fmt.Sprintf("GetServerlessInfraCluster failed, env = %s", env))
	}

	if len(resp.Clusters) == 0 {
		log.Error(fmt.Sprintf("GetServerlessInfraCluster result does not exist, url = %s", url))
		return o11ystructs.ClusterBaseInfo{}, errors.New("GetServerlessInfraCluster result does not exist")
	}

	cachedClusters.Set(cacheKey, resp.Clusters[0], time.Hour*24)
	return resp.Clusters[0], nil
}

func (o *O11yMetaAPI) queryCluster(ctx context.Context, url string) (o11ymessage.QueryClustersResp, error) {
	request, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return o11ymessage.QueryClustersResp{}, err
	}
	err = o.WithAuth0Header(ctx, request)
	if err != nil {
		return o11ymessage.QueryClustersResp{}, err
	}

	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		return o11ymessage.QueryClustersResp{}, err
	}
	defer func() {
		resp.Body.Close()
	}()
	if resp.StatusCode/100 != 2 {
		return o11ymessage.QueryClustersResp{}, errors.New(fmt.Sprintf("queryCluster failed, url = %s, status = %d", url, resp.StatusCode))
	}
	respContent, err := io.ReadAll(resp.Body)

	if err != nil {
		return o11ymessage.QueryClustersResp{}, err
	}
	o11yResp := o11ymessage.QueryClustersResp{}
	err = UnmarshalRespData(respContent, &o11yResp)
	if err != nil {
		return o11ymessage.QueryClustersResp{}, err
	}
	return o11yResp, nil
}

var cachedClusters = cache.New(5*time.Minute, 10*time.Minute)

func (o *O11yMetaAPI) ListO11yDedicatedClusters(ctx context.Context, env constants.BizEnv) ([]o11ystructs.ClusterBaseInfo, error) {
	cacheKey := fmt.Sprintf("dedicated-clusters-%s", env)

	if cached, ok := cachedClusters.Get(cacheKey); ok {
		return cached.([]o11ystructs.ClusterBaseInfo), nil
	}
	clusters := make([]o11ystructs.ClusterBaseInfo, 0)
	url := fmt.Sprintf(queryClusterUrlTemplate, domainPostfix(),
		configs.GetServiceInfo().Vendor,
		configs.GetServiceInfo().Region)

	url = fmt.Sprintf(urlAppendParam, url, "bizType", dedicatedBizType(env))

	resp, err := o.queryCluster(ctx, url)
	if err != nil {
		return clusters, errors.New(fmt.Sprintf("ListO11yDedicatedClusters failed, env = %s, err = %s", env, err.Error()))
	}

	if len(resp.Clusters) == 0 {
		log.Info(fmt.Sprintf("ListO11yDedicatedClusters result does not exist, url = %s", url))
		return clusters, nil
	}

	for _, c := range resp.Clusters {
		if isDedicatedApplicationID(c.ApplicationID) {
			clusters = append(clusters, c)
		}
	}

	cachedClusters.Set(cacheKey, clusters, time.Minute*1)

	return clusters, nil
}

func (o *O11yMetaAPI) ListDedicatedK8sInfraClusters(ctx context.Context, env constants.BizEnv) ([]o11ystructs.ClusterBaseInfo, error) {
	cacheKey := fmt.Sprintf("dedicated-k8s-infra-clusters-%s", env)

	if cached, ok := cachedClusters.Get(cacheKey); ok {
		return cached.([]o11ystructs.ClusterBaseInfo), nil
	}
	clusters := make([]o11ystructs.ClusterBaseInfo, 0)
	url := fmt.Sprintf("%s/api/v1/clusters/?vendor=%s&region=%s&bizType=%s&applicationID=%s",
		regionalServiceUrl,
		configs.GetServiceInfo().Vendor,
		configs.GetServiceInfo().Region,
		dedicatedBizType(env),
		"k8s-infra",
	)

	resp, err := o.queryCluster(ctx, url)
	if err != nil {
		log.Info(fmt.Sprintf("ListO11yDedicatedK8sInfraClusters failed, url = %s, err = %s", url, err.Error()))
		return clusters, errors.New(fmt.Sprintf("ListO11yDedicatedK8sInfraClusters failed, env = %s, err = %s", env, err.Error()))
	}

	if len(resp.Clusters) == 0 {
		log.Info(fmt.Sprintf("ListO11yDedicatedK8sInfraClusters result does not exist, url = %s", url))
		return clusters, nil
	}

	for _, c := range resp.Clusters {
		clusters = append(clusters, c)
	}

	cachedClusters.Set(cacheKey, clusters, time.Minute*1)
	return clusters, nil
}

func isDedicatedApplicationID(name string) bool {
	switch name {
	case "base", "seed", "dataapp", "k8s-infra":
		return false
	}

	return true
}

func UnmarshalRespData(data []byte, v interface{}) error {
	type Result struct {
		Code int         `json:"code"`
		Data interface{} `json:"data"`
	}

	result := &Result{
		Data: v,
	}

	return json.Unmarshal(data, result)
}

func serverlessBizType(env constants.BizEnv) string {
	switch env {
	case constants.Dev:
		return "dev-devtier"
	case constants.Staging:
		return "staging-devtier"
	case constants.Prod:
		return "prod-devtier"
	}
	log.Error(fmt.Sprintf("get serverlessBizType failed, unsupported BizEnv %s", env))
	return "unsupported"
}

func dedicatedBizType(env constants.BizEnv) string {
	switch env {
	case constants.Dev:
		return "dev-tidbcloud"
	case constants.Staging:
		return "staging-tidbcloud"
	case constants.Prod:
		return "tidbcloud"
	}

	log.Error(fmt.Sprintf("get dedicatedBizType failed, unsupported BizEnv %s", env))
	return "unsupported"
}

func (o *O11yMetaAPI) getAuth0Token(ctx context.Context) (string, error) {
	if o.token == "" || time.Now().After(o.tokenUpdateTime.Add(1*time.Hour)) {
		err := o.updateAuth0Token(ctx)
		if err != nil {
			return "", err
		}
	}
	return o.token, nil
}

type Auth0Resp struct {
	AccessToken string `json:"access_token" form:"access_token"`
}

func (o *O11yMetaAPI) updateAuth0Token(ctx context.Context) error {
	body, err := form.EncodeToString(config.GetAuth0Config())
	if err != nil {
		return err
	}
	resp, err := http.Post("https://tidb-soc2.us.auth0.com/oauth/token",
		"application/x-www-form-urlencoded",
		strings.NewReader(body))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	auth0Resp := &Auth0Resp{}
	err = json.Unmarshal(respContent, auth0Resp)
	if err != nil {
		return err
	}
	o.token = auth0Resp.AccessToken
	o.tokenUpdateTime = time.Now()
	return nil
}

func (o *O11yMetaAPI) WithAuth0Header(ctx context.Context, request *http.Request) error {
	request.Header.Set("Content-Type", "application/json")
	auth0Token, err := o.getAuth0Token(ctx)
	if err != nil {
		return err
	}

	request.Header.Set(o11yconstants.AuthorizationHeader, fmt.Sprintf("%s %s", o11yconstants.BearerPrefix, auth0Token))
	return nil
}
