// Package http add by blacknc <blacknc@163.com> at 2023/04/13 17:36:36
package http

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

	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/database"
	"dataservices/commons/logger"
	"dataservices/commons/metrics"
	"dataservices/commons/utils"

	dataservice_bizerr "github.com/tidbcloud/dataservices/commons/bizerr"
	"github.com/tidbcloud/pkgv2/bizerr"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/crossregion"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.uber.org/zap"
)

type BaseResponse interface {
	GetErrCode() int
	GetErrMsg() string
}

type option struct {
	Timeout time.Duration
}

type Option func(opt *option)

func WithTimeout(timeout time.Duration) Option {
	return func(opt *option) {
		opt.Timeout = timeout
	}
}

func defaultOption() option {
	return option{
		Timeout: 2 * time.Minute,
	}
}

func NewBasicAuthClient(opts ...Option) *crossregion.CrossRegionClient {

	o := getOption(opts...)
	env := crossregion.EnvType(config.Global().ServerConf.Env)
	authKey := config.Global().ServerConf.CrossRegionToken
	timeout := o.Timeout

	tilog.Info(context.TODO(), "create cross region client", logger.Redact("token", authKey), zap.Duration("timeout", timeout))
	return crossregion.NewBasicAuthClient(env, authKey, timeout)
}

func postRequestToGlobal(ctx context.Context, svc, url string, data interface{}, result interface{}, opts ...Option) (err error) {
	start := time.Now()
	var httpCode int
	tilog.Info(ctx, "PostRequestToGlobal begin",
		zap.String("service", svc),
		zap.String("url", url),
		logger.Redact("data", data))
	defer func() {
		tilog.Info(ctx, "PostRequestToGlobal end",
			zap.String("service", svc),
			zap.String("url", url),
			zap.String("cost", time.Since(start).String()),
			zap.Int("http_code", httpCode),
			zap.Error(err))
	}()

	cli := NewBasicAuthClient(opts...)
	resp, httpCode, err := cli.PostGlobal(ctx, url, data)
	return parseResp(ctx, resp, httpCode, err, result)
}

func getRequestToGlobal(ctx context.Context, svc, url string, data map[string]string, result interface{}) (err error) {
	start := time.Now()
	var httpCode int
	tilog.Info(ctx, "getRequestToGlobal begin",
		zap.String("service", svc),
		zap.String("url", url),
		logger.Redact("data", data))
	defer func() {
		tilog.Info(ctx, "getRequestToGlobal end",
			zap.String("service", svc),
			zap.String("url", url),
			zap.String("cost", time.Since(start).String()),
			zap.Int("http_code", httpCode),
			zap.Error(err))
	}()

	cli := NewBasicAuthClient()
	resp, httpCode, err := cli.GetGlobal(ctx, url, &data)
	return parseResp(ctx, resp, httpCode, err, result)
}

func postRequestToO11yRegion(ctx context.Context, svc, region, url string, data interface{}, result interface{}) (err error) {
	if !config.IsProd() {
		region = constants.DefaultAWSRegion
	}
	start := time.Now()
	var httpCode int
	tilog.Info(ctx, "postRequestToO11yRegion begin",
		zap.String("service", svc),
		zap.String("region", region),
		zap.String("url", url),
		logger.Redact("data", data))
	defer func() {
		tilog.Info(ctx, "postRequestToO11yRegion end",
			zap.String("service", svc),
			zap.String("region", region),
			zap.String("url", url),
			zap.String("cost", time.Since(start).String()),
			zap.Int("http_code", httpCode),
			zap.Error(err))
	}()

	cli := NewBasicAuthClient()
	resp, httpCode, err := cli.PostO11yRegion(ctx, region, url, data)
	return parseResp(ctx, resp, httpCode, err, result)
}

func postRequestToProviderO11yRegion(ctx context.Context, svc, region, provider, url string, data interface{}, result interface{}, opts ...Option) (err error) {
	if config.IsLocalMode() {
		return postRequestToLocalDataappGateway(ctx, svc, region, provider, url, data, result, opts...)
	}
	// An empty provider preserves the existing AWS host rule. When provider is set,
	// AliCloud uses a new regional domain and sends requests through an upstream
	// gateway route with the extra /dataservice prefix. The upstream gateway strips
	// that prefix before forwarding, so the regional service still handles the
	// legacy /inner/dataservice paths unchanged.
	// This helper exists because the shared crossregion client only knows the
	// legacy {region}.inner-region.o11y host format.
	if provider == "" {
		return postRequestToO11yRegion(ctx, svc, region, url, data, result)
	}
	start := time.Now()
	var httpCode int
	tilog.Info(ctx, "postRequestToProviderO11yRegion begin",
		zap.String("service", svc),
		zap.String("provider", provider),
		zap.String("region", region),
		zap.String("url", url),
		logger.Redact("data", data))
	defer func() {
		tilog.Info(ctx, "postRequestToProviderO11yRegion end",
			zap.String("service", svc),
			zap.String("provider", provider),
			zap.String("region", region),
			zap.String("url", url),
			zap.String("cost", time.Since(start).String()),
			zap.Int("http_code", httpCode),
			zap.Error(err))
	}()

	env := crossregion.EnvType(config.Global().ServerConf.Env)
	baseURL, domainErr := O11yRegionInnerDomainWithHostPrefix(env, region, provider)
	if domainErr != nil {
		return domainErr
	}
	routedURL := baseURL + ProviderScopedInnerDataServicePath(provider, url)
	resp, httpCode, err := postRequestWithBasicAuth(ctx, routedURL, data, opts...)
	return parseResp(ctx, resp, httpCode, err, result)
}

func postRequestToLocalDataappGateway(ctx context.Context, svc, region, provider, url string, data interface{}, result interface{}, opts ...Option) (err error) {
	start := time.Now()
	var httpCode int
	tilog.Info(ctx, "postRequestToLocalDataappGateway begin",
		zap.String("service", svc),
		zap.String("provider", provider),
		zap.String("region", region),
		zap.String("url", url),
		logger.Redact("data", data))
	defer func() {
		tilog.Info(ctx, "postRequestToLocalDataappGateway end",
			zap.String("service", svc),
			zap.String("provider", provider),
			zap.String("region", region),
			zap.String("url", url),
			zap.String("cost", time.Since(start).String()),
			zap.Int("http_code", httpCode),
			zap.Error(err))
	}()

	env := crossregion.EnvType(config.Global().ServerConf.Env)
	host, hostErr := ProviderScopedInnerDataServiceHost(env, region, provider)
	if hostErr != nil {
		return hostErr
	}
	baseURL := "http://" + config.Global().GrpcConf.DataserviceGatewayAddr
	resp, httpCode, err := postRequestWithBasicAuthAndHost(ctx, baseURL+url, host, data, opts...)
	return parseResp(ctx, resp, httpCode, err, result)
}

func postRequestWithBasicAuth(ctx context.Context, url string, data interface{}, opts ...Option) (string, int, error) {
	return postRequestWithBasicAuthAndHost(ctx, url, "", data, opts...)
}

func postRequestWithBasicAuthAndHost(ctx context.Context, url, host string, data interface{}, opts ...Option) (string, int, error) {
	o := getOption(opts...)

	var body io.Reader
	if data != nil {
		payload, err := json.Marshal(data)
		if err != nil {
			return "", 0, err
		}
		body = bytes.NewReader(payload)
	}

	// This is the minimal transport used by provider-scoped regional calls. It
	// reuses the existing cross-region basic auth token and keeps the request
	// contract identical to the legacy regional HTTP path.
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
	if err != nil {
		return "", 0, err
	}
	req.Header.Set("Authorization", "Basic "+config.Global().ServerConf.CrossRegionToken)
	req.Header.Set("Content-Type", "application/json")
	if host != "" {
		req.Host = host
	}

	client := &http.Client{
		Timeout:   o.Timeout,
		Transport: otelhttp.NewTransport(http.DefaultTransport),
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", 0, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	return string(respBody), resp.StatusCode, err
}

func recordHttpCounterMetric(svc string, err error) {
	if metrics.DownstreamHttpCounter == nil {
		return
	}
	code := "0"
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			code = strconv.FormatInt(tiErr.BizCode(), 10)
		} else if bizErr, ok := err.(bizerr.BizError); ok {
			code = strconv.FormatInt(int64(bizErr.ErrCode()), 10)
		} else {
			code = "-1"
		}
	}
	metrics.DownstreamHttpCounter.WithLabelValues(svc, getFuncName(6, true), code).Inc()
}

func parseResp(ctx context.Context, resp string, httpCode int, err error, target interface{}) error {
	parentFuncName := getFuncName(6, true)
	if err != nil || httpCode != 200 {
		tilog.Warn(ctx, parentFuncName+" failed", zap.String("resp", resp), zap.Int("http_code", httpCode), zap.Error(err))
	}
	if err != nil {
		return errs.NewInternalServerError(parentFuncName+" failed", err)
	}

	var (
		code    int
		message string
	)
	if target != nil {
		if resp != "" && resp[0] == '{' && resp[len(resp)-1] == '}' {
			if err = json.Unmarshal([]byte(resp), target); err != nil {
				return errs.NewInternalJsonMarshal(parentFuncName+" parse result failed", err)
			} else if result, ok := target.(BaseResponse); ok {
				code = result.GetErrCode()
				message = result.GetErrMsg()
			}
		}
	}

	if code == 0 && httpCode != 200 {
		code = httpCode
	}
	if code != 0 && code != 200 && message == "" {
		message = parentFuncName + " failed"
	}
	return parseError(ctx, httpCode, code, message)
}

func parseError(ctx context.Context, httpCode, errCode int, errMsg string) error {
	if errMsg == "" {
		errMsg = "internal server error"
	}

	// convert mysql error code to bad request
	if errCode > 1000 && errCode < 100000 {
		if errCode <= 0xffff && database.IsCausedByUser(uint16(errCode), errMsg) {
			// return errs.NewBadRequest(errMsg, fmt.Errorf("code: %d", errCode))
			return errs.NewError(dataservice_bizerr.ErrInvalidUserSql, errMsg, fmt.Errorf("code: %d", errCode))
		} else {
			tilog.Error(ctx, "parse Error Failed", zap.String("errMsg", errMsg), zap.Int("errCode", errCode))
			return errs.NewBadRequest(errMsg, fmt.Errorf("code: %d", errCode))
			// return errs.NewInternalServerError(errMsg, fmt.Errorf("code: %d", errCode))
		}
	}

	if isBizCode(errCode) {
		return errs.NewTiError(httpCode, int64(errCode), errMsg, nil)
	}

	switch int64(errCode) {
	case http.StatusOK, 0:
		// nothing to do
		return nil
	case http.StatusBadRequest:
		return errs.NewBadRequest(errMsg, nil)
	case http.StatusNotFound:
		return errs.NewNotFound(errMsg, nil)
	default:
		return errs.NewInternalServerError(errMsg, fmt.Errorf("code: %d", errCode))
	}
}

func getFuncName(skip int, isConvert bool) string {
	funcName := utils.GetFuncNameWithSkip(skip)
	nameList := strings.Split(funcName, "http.")
	if len(nameList) > 1 {
		funcName = nameList[1]
	}
	if isConvert {
		funcName = utils.CamelToSnake(funcName)
	}
	return funcName
}

func getCurrentFunc(isConvert bool) string {
	return getFuncName(4, isConvert)
}

func getParentFunc(isConvert bool) string {
	return getFuncName(5, isConvert)
}

func getGrandFunc(isConvert bool) string {
	return getFuncName(6, isConvert)
}

func isBizCode(code int) bool {
	return bizerr.IsInternalErr(bizerr.ErrCode(code)) ||
		bizerr.IsBusinessErr(bizerr.ErrCode(code)) ||
		bizerr.IsRequestErr(bizerr.ErrCode(code))
}

func getOption(opts ...Option) *option {
	o := defaultOption()
	for _, opt := range opts {
		opt(&o)
	}
	return &o
}
