package apigateway

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

	"github.com/tidbcloud/metering/pkg/common/constants"
	"github.com/tidbcloud/metering/pkg/common/metrics"
	models "github.com/tidbcloud/metering/pkg/common/utils/apigateway/models"
	"go.uber.org/zap"
)

var GlobalInnerDomainMap = map[string]string{
	constants.EnvDev:     "https://inner-central.dev.tidbcloud.com",
	constants.EnvStaging: "https://inner-central.staging.tidbcloud.com",
	constants.EnvProd:    "https://inner-central.prod.tidbcloud.com",
}

func getGlobalInnerDomain(envType string) (string, error) {
	if val, ok := GlobalInnerDomainMap[envType]; ok {
		return val, nil
	}
	return "", fmt.Errorf("can not find globaldomain by env_type: %s", envType)
}

type ApiGateway interface {
	CountBillableClusters(ctx context.Context, organizationId string, projectid string, startTS time.Time, endTS time.Time) (int64, error)
	ShowComponentNodeInfo(ctx context.Context, clusterId string) (*models.V1ShowComponentNodeInfoResponse, error)
}

type ApiGatewayClient struct {
	envType    string
	authKey    string
	httpClient *http.Client
	vendor     string
	log        *zap.Logger
}

func NewApiGatewayClient(log *zap.Logger, vendor string, envType string, authKey string, timeout time.Duration) *ApiGatewayClient {
	return &ApiGatewayClient{
		envType:    envType,
		authKey:    authKey,
		httpClient: &http.Client{Timeout: timeout, Transport: http.DefaultTransport},
		log:        log.With(zap.String("apigateway", "ApiGatewayClient")),
		vendor:     vendor,
	}
}
func (client *ApiGatewayClient) RequestGlobal(ctx context.Context, method string, url string, queryString *map[string]string, header *map[string]string, payload interface{}) (string, int, error) {
	globalDomain, err := getGlobalInnerDomain(client.envType)
	if err != nil {
		return "", 0, err
	}
	globalUrl := globalDomain + url
	return client.requestWithHeader(ctx, method, globalUrl, queryString, header, payload)
}

func (client *ApiGatewayClient) PostGlobal(ctx context.Context, url string, data interface{}) (string, int, error) {
	globalDomain, err := getGlobalInnerDomain(client.envType)
	if err != nil {
		return "", 0, err
	}
	globalUrl := globalDomain + url
	return client.postWithHeader(ctx, globalUrl, data, nil)
}

// Get request by url
func (client *ApiGatewayClient) postWithHeader(ctx context.Context, url string, data interface{}, header *map[string]string) (string, int, error) {
	return client.requestWithHeader(ctx, http.MethodPost, url, nil, header, data)
}

// Get request by url
func (client *ApiGatewayClient) getWithHeader(ctx context.Context, url string, params *map[string]string, header *map[string]string) (string, int, error) {
	return client.requestWithHeader(ctx, http.MethodGet, url, params, header, nil)
}
func encodeJSON(data interface{}) io.Reader {
	b, _ := json.Marshal(data)
	return bytes.NewReader(b)
}
func (client *ApiGatewayClient) requestWithHeader(ctx context.Context, method string, url string, queryString *map[string]string, header *map[string]string, payload interface{}) (string, int, error) {
	var body io.Reader
	if payload != nil {
		body = encodeJSON(payload)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, body)
	if err != nil {
		return "", 0, err
	}
	if header != nil {
		for k, v := range *header {
			req.Header.Set(k, v)
		}
	}
	req.Header.Set("Authorization", "Basic "+client.authKey)
	if method != http.MethodGet {
		req.Header.Set("Content-Type", "application/json")
	}

	if queryString != nil {
		q := req.URL.Query()
		for k, v := range *queryString {
			q.Add(k, v)
		}
		req.URL.RawQuery = q.Encode()
	}
	resp, err := client.httpClient.Do(req)
	if nil != err {
		return "", 0, err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	return string(data), resp.StatusCode, err
}

func (client *ApiGatewayClient) GetGlobal(ctx context.Context, url string, data *map[string]string) (string, int, error) {
	globalDomain, err := getGlobalInnerDomain(client.envType)
	if err != nil {
		return "", 0, err
	}
	globalUrl := globalDomain + url
	return client.getWithHeader(ctx, globalUrl, data, nil)
}

func getGRPCErrorCode(respStr string) (int, error, bool) {
	var resp models.GooglerpcStatus

	err := json.Unmarshal([]byte(respStr), &resp)
	if err != nil {
		return 0, nil, false
	}
	if resp.Code == 0 && resp.Message == "" {
		return 0, nil, false
	}
	return int(resp.Code), errors.New(resp.Message), true
}

func getBaseRespErrorCode(respStr string) (int, string, bool) {
	var resp models.BaseRespErrorWarp

	err := json.Unmarshal([]byte(respStr), &resp)
	if err != nil {
		return 0, "", false
	}
	if resp.BaseResp == nil {
		return 0, "", false
	}
	if resp.BaseResp.ErrCode == 0 && resp.BaseResp.ErrMsg == "" {
		return 0, "", false
	}
	return resp.BaseResp.ErrCode, resp.BaseResp.ErrMsg, true
}

func (client *ApiGatewayClient) CountBillableClusters(ctx context.Context, organizationId string, projectid string, startTS time.Time, endTS time.Time) (int64, error) {
	data := models.V1CountBillableClustersRequest{
		OrganizationId: organizationId,
		ProjectId:      projectid,
		StartTime:      startTS,
		EndTime:        endTS,
	}
	metricslabels := map[string]string{
		constants.MetricsAPIName:         "CountBillableClusters",
		constants.MetricsAPIResult:       "success",
		constants.MetricsLblProviderType: client.vendor,
	}
	defer func() {
		metrics.MetricsRquestAPICount.With(metricslabels).Inc()
	}()
	respStr, httpCode, err := client.PostGlobal(ctx, "/inner/mgmt/v1/clusters:countBillableClusters", &data)
	if err != nil {
		client.log.Error("GetGlobalHTTP failed", zap.Error(err), zap.String("respStr", respStr), zap.Int("httpCode", httpCode))
		metricslabels[constants.MetricsAPIResult] = "failed"
		return 0, err
	}
	if httpCode != http.StatusOK {
		client.log.Info("GetGlobalHTTP Status not ok ", zap.String("respStr", respStr), zap.Int("httpCode", httpCode))
		if code, msg, ok := getBaseRespErrorCode(respStr); ok {

			if code == 59900104 && msg == "check dedicated allow failed: failed to get org info: the org is not exist, please check the org_id" {
				return 0, nil
			}
			client.log.Error("GetGlobalHTTP BaseRespError ", zap.Int("code", code), zap.String("msg", msg))
		}
		metricslabels[constants.MetricsAPIResult] = "failed"
		return 0, fmt.Errorf("error httpCode: %d", httpCode)
	}
	if code, err, ok := getGRPCErrorCode(respStr); ok {
		client.log.Error("GetGlobalHTTP GRPC error ", zap.Error(err), zap.String("respStr", respStr), zap.Int("code", code))
		metricslabels[constants.MetricsAPIResult] = "failed"
		return 0, err
	}
	var resp models.V1CountBillableClustersResponse
	err = json.Unmarshal([]byte(respStr), &resp)
	if err != nil {
		metricslabels[constants.MetricsAPIResult] = "failed"
		return 0, err
	}
	return resp.BillableClustersCount, nil

}

func (client *ApiGatewayClient) ShowComponentNodeInfo(ctx context.Context, clusterId string) (*models.V1ShowComponentNodeInfoResponse, error) {
	tmpl := "/inner/mgmt/v1/clusters/%s:showComponentNodeInfo"
	url := fmt.Sprintf(tmpl, clusterId)
	respStr, httpCode, err := client.GetGlobal(ctx, url, nil)
	if err != nil {
		client.log.Error("GetGlobalHTTP failed", zap.Error(err), zap.String("respStr", respStr), zap.Int("httpCode", httpCode))
		return nil, err
	}
	if httpCode != http.StatusOK {
		client.log.Info("GetGlobalHTTP Status not ok ", zap.String("respStr", respStr), zap.Int("httpCode", httpCode))
		return nil, fmt.Errorf("error httpCode: %d", httpCode)
	}
	if code, err, ok := getGRPCErrorCode(respStr); ok {
		client.log.Error("GetGlobalHTTP GRPC error ", zap.Error(err), zap.String("respStr", respStr), zap.Int("code", code))
		return nil, err
	}
	var resp models.V1ShowComponentNodeInfoResponse
	err = json.Unmarshal([]byte(respStr), &resp)
	if err != nil {
		return nil, err
	}
	return &resp, nil
}
