package bootstrap

import (
	"context"
	"net/http"
	"regexp"
	"strings"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/tidbcloud/metrics-integration/regional/pkg/metrics"
	"github.com/tidbcloud/pkgv2/bizerr"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

const (
	MinCustomizedBizErrCode = 40000000
	MaxGrpcErrCode          = bizerr.ErrCode(codes.Unauthenticated)
)

var digitOnlyRegex = regexp.MustCompile("^[0-9]+$")

const (
	setHeaderPrefix    = "x-set-header_"
	queryHeaderPrefix  = "x-query-header_"
	cookieHeaderPrefix = "x-cookie-header_"
	httpMethod         = "x-http-method"
	httpURI            = "x-http-uri"
	httpPath           = "x-forwarded-path"
)

var errCode2BizErrCodeMapping = map[codes.Code]bizerr.ErrCode{
	codes.Internal:           bizerr.ErrCodeUnknown,
	codes.OK:                 bizerr.ErrCodeOK,
	codes.InvalidArgument:    bizerr.ErrCodeParams,
	codes.DeadlineExceeded:   bizerr.ErrCodeTimeout,
	codes.NotFound:           bizerr.ErrCodeRecordNotFound,
	codes.AlreadyExists:      bizerr.ErrCodeRecordExist,
	codes.PermissionDenied:   bizerr.ErrCodePermission,
	codes.ResourceExhausted:  bizerr.ErrCodeTooManyRequests,
	codes.Unauthenticated:    bizerr.ErrCodeAuth,
	codes.FailedPrecondition: bizerr.ErrCodeGeneralBusiness,
}

// AllHeaderMatcher add all headers to grpc
func AllHeaderMatcher(key string) (string, bool) {
	// We can not pass ”Connection: keep-alive“ to grpc server, which can lead to a 500 error.
	// The example response is listed below:
	// HTTP/1.1 500 Internal Server Error
	// ...
	// {
	// 	"code": 13,
	// 	"message": "stream terminated by RST_STREAM with error code: PROTOCOL_ERROR",
	// 	"details": [
	// 	]
	// }
	if strings.ToLower(key) == "connection" {
		return "", false
	}
	return key, true
}

func AddHeaderInResponseMatcher(key string) (string, bool) {
	if strings.HasPrefix(key, setHeaderPrefix) {
		return strings.TrimPrefix(key, setHeaderPrefix), true
	}
	return key, false
}

func AddHttpDetail(_ context.Context, r *http.Request) metadata.MD {
	m := make(map[string]string)
	m[httpURI] = r.RequestURI
	m[httpMethod] = r.Method
	for key, value := range r.URL.Query() {
		m[queryHeaderPrefix+key] = value[0]
	}
	for _, c := range r.Cookies() {
		m[cookieHeaderPrefix+c.Name] = c.Value
	}
	return metadata.New(m)
}

func HTTPErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	s := status.Convert(err)
	functionName := getFunctionNameFromURI(r.RequestURI)
	// TODO: Separate invalid input and internal error
	if s == nil {
		metrics.HttpErrorResultCounter.WithLabelValues(functionName, r.Method, "-1").Inc()
		runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
	}
	newS := grpcErr2BizErr(s)
	metrics.HttpErrorResultCounter.WithLabelValues(
		functionName, r.Method, s.Code().String()).Inc()
	newError := runtime.HTTPStatusError{
		HTTPStatus: bizerr.Restore(bizerr.ErrCode(newS.Code()), newS.Message()).HttpStatus(),
		Err:        newS.Err(),
	}
	runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, &newError)
}

func getFunctionNameFromURI(URI string) string {
	withoutParameter := strings.Split(URI, "?")[0]
	paths := strings.Split(withoutParameter, "/")
	length := len(paths)
	functionName := ""
	pathLength := 0

	for i := length - 1; i >= 0; i-- {
		if !digitOnlyRegex.MatchString(paths[i]) {
			functionName = paths[i] + "/" + functionName
			pathLength++
			if pathLength == 2 {
				return functionName
			}
		}
	}

	return functionName
}

func grpcErr2BizErr(s *status.Status) *status.Status {
	errCode := s.Code()
	message := s.Message()
	bizErrCode, ok := errCode2BizErrCodeMapping[errCode]
	if ok {
		return status.New(codes.Code(bizErrCode), message)
	} else if errCode >= MinCustomizedBizErrCode {
		return status.New(errCode, message)
	} else {
		return status.New(codes.Code(bizerr.ErrCodeUnknown), message)
	}
}
