//revive:disable:var-naming

package grpc_http

//revive:enable:var-naming

import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"reflect"
	"regexp"
	"strings"
	"time"
	"unsafe"

	"github.com/gin-gonic/gin"
	"github.com/gogo/grpc-example/insecure"
	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/rs/cors"
	pkglog "github.com/tidbcloud/metrics-integration/global/pkg/log"
	"github.com/tidbcloud/metrics-integration/global/pkg/metrics"
	"github.com/tidbcloud/metrics-integration/global/pkg/redact"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

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

func clientAlwaysLog(_ context.Context, _ string) bool {
	return true
}

// DialLogOptions logs the requests and response.
func DialLogOptions(logger *zap.Logger, opts GatewayOptions) []grpc.DialOption {
	var unary []grpc.UnaryClientInterceptor
	var stream []grpc.StreamClientInterceptor
	if opts.LogPayload {
		unary = append(unary,
			grpc_zap.PayloadUnaryClientInterceptor(logger, clientAlwaysLog),
		)
		stream = append(stream,
			grpc_zap.PayloadStreamClientInterceptor(logger, clientAlwaysLog),
		)
	}

	return []grpc.DialOption{
		grpc.WithUnaryInterceptor(
			grpc_middleware.ChainUnaryClient(unary...),
		),
		grpc.WithStreamInterceptor(
			grpc_middleware.ChainStreamClient(stream...),
		),
	}
}

// GatewayOptions has configuration options for the GRPC gateway
type GatewayOptions struct {
	LogPayload bool
}

// LocalGatewayOptions has Default GatewayOptions for local development
var LocalGatewayOptions = GatewayOptions{LogPayload: true}

// ProdGatewayOptions has default GatewayOptions for production
var ProdGatewayOptions = GatewayOptions{LogPayload: false}

// GatewayConn create a connection that can be used by a gateway
func GatewayConn(logger *zap.Logger, addr string, opts GatewayOptions) (*grpc.ClientConn, error) {
	// See https://github.com/grpc/grpc/blob/master/doc/naming.md
	// for gRPC naming standard information.
	dialAddr := fmt.Sprintf("passthrough://0.0.0.0/%s", addr)
	dialOpts := append(DialLogOptions(logger, opts),
		grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(insecure.CertPool, "")),
		grpc.WithBlock(),
		grpc.WithChainUnaryInterceptor(
			otelgrpc.UnaryClientInterceptor(),
		),
	)
	return grpc.DialContext(
		context.Background(),
		dialAddr,
		// grpc.WithInsecure(),
		dialOpts...,
	)
}

// MakeGatewayMux returns a mux for a GRPC gatewawy
func MakeGatewayMux(fn runtime.ErrorHandlerFunc) *runtime.ServeMux {
	jsonpb := &runtime.JSONPb{
		MarshalOptions: protojson.MarshalOptions{
			UseProtoNames:   true,
			EmitUnpopulated: true,
		},
		UnmarshalOptions: protojson.UnmarshalOptions{
			DiscardUnknown: true,
		},
	}
	// This marshaler enables support to return raw response using HttpBody.
	// Note that this behavior is enabled by default in grpc-gateway v2.
	marshaler := &runtime.HTTPBodyMarshaler{
		Marshaler: jsonpb,
	}
	mux := runtime.NewServeMux(
		runtime.WithMarshalerOption(runtime.MIMEWildcard, marshaler),
		runtime.WithIncomingHeaderMatcher(AllHeaderMatcher),
		runtime.WithMetadata(AddHttpDetail),
		runtime.WithOutgoingHeaderMatcher(AddHeaderInResponseMatcher),

		// This is claimged to be necessary to get error details properly in unary requests
		// Right now it seems to just remove the "error" field
		runtime.WithErrorHandler(fn),
	)
	return mux
}

func MakeGatewayMuxOpenAPI(fn runtime.ErrorHandlerFunc) *runtime.ServeMux {
	jsonpb := &runtime.JSONPb{
		MarshalOptions: protojson.MarshalOptions{
			EmitUnpopulated: true,
		},
		UnmarshalOptions: protojson.UnmarshalOptions{
			DiscardUnknown: true,
		},
	}
	// This marshaler enables support to return raw response using HttpBody.
	// Note that this behavior is enabled by default in grpc-gateway v2.
	marshaler := &runtime.HTTPBodyMarshaler{
		Marshaler: jsonpb,
	}
	mux := runtime.NewServeMux(
		runtime.WithMarshalerOption(runtime.MIMEWildcard, marshaler),
		runtime.WithIncomingHeaderMatcher(AllHeaderMatcher),
		runtime.WithMetadata(AddHttpDetail),
		runtime.WithOutgoingHeaderMatcher(AddHeaderInResponseMatcher),

		// This is claimged to be necessary to get error details properly in unary requests
		// Right now it seems to just remove the "error" field
		runtime.WithErrorHandler(fn),
	)
	return mux
}

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 ServerlessOpenApiErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
	s := status.Convert(err)
	if s == nil {
		runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
	}
	// serverless openapi returns the standard http status code
	httpStatus := bizCode2HTTPCode(bizerr.ErrCode(s.Code()))
	newS := status.New(codes.Code(httpStatus), s.Message())
	newError := runtime.HTTPStatusError{
		HTTPStatus: httpStatus,
		Err:        newS.Err(),
	}
	runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, &newError)
}

// ConfigureGatewayTLS converts a mux to a http server for a GRPC gateway
func ConfigureGatewayTLS(mux http.Handler) http.Server {
	cfg := &tls.Config{}
	return http.Server{
		Handler:      mux,
		Addr:         "0.0.0.0:443",
		TLSConfig:    cfg,
		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
	}
}

// RegisterHandler registers for the GRPC HTTP Gateway
type RegisterHandler func(context.Context, *runtime.ServeMux, *grpc.ClientConn) error

var corsOptsAllowAll = cors.Options{
	AllowedOrigins: []string{"*"},
	AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
	AllowedHeaders: []string{"*"},
	// Not sure how to get the frontend to not send the Authorization header
	// AllowCredentials: true,
	// AllowedHeaders: []string{"Authorization", "Content-Type", "Authorization", "Accept", "Origin", "Referer", "User-Agent"},
}

// CorsAllowAll is Cors configured to allow all headers, origins, methods, and allow credentials
var CorsAllowAll = cors.New(corsOptsAllowAll)

var DebugAPICors = cors.New(cors.Options{
	// Whitelist allowed origins to migrate CSRF risks from XHR.
	AllowedOrigins: []string{
		"https://debug.tidbcloud.com",
		"https://nightly.debug.tidbcloud.com",
		"https://staging.debug.tidbcloud.com",
		"http://localhost:3000",
	},
	AllowedMethods:   []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
	AllowedHeaders:   []string{"*"},
	AllowCredentials: true,
	MaxAge:           24 * 60 * 60, // Cache preflight requests
})

// PortToAddress converts an int to a string
func PortToAddress(grpcPort int) string {
	return fmt.Sprintf("0.0.0.0:%d", grpcPort)
}

// Middleware is an HTTP middleware
type Middleware = func(http.Handler) http.Handler

// ServeGateway serves the GRPC gateway
type ServeGateway struct {
	eng *gin.Engine
}

// HTTP serves as HTTP
func (s *ServeGateway) HTTP() (string, *gin.Engine) {
	gatewayPort := 11000
	gatewayAddr := fmt.Sprintf("0.0.0.0:%d", gatewayPort)

	return gatewayAddr, s.eng
}

func setTraceIdMiddleware(c *gin.Context) {
	spanctx := trace.SpanContextFromContext(c.Request.Context())
	traceId := spanctx.TraceID().String()

	c.Header("x-pingcap-traceid", traceId)

	c.Next()
}

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"
)

// 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 Query(ctx context.Context, name string) string {
	return FirstHeader(ctx, strings.ToLower(queryHeaderPrefix+name))
}

func Cookie(ctx context.Context, name string) string {
	return FirstHeader(ctx, strings.ToLower(cookieHeaderPrefix+name))
}

func SetHeader(grpcCtx context.Context, name string, value string) error {
	return grpc.SetHeader(grpcCtx, metadata.New(map[string]string{
		setHeaderPrefix + name: value,
	}))
}

func HttpMethod(ctx context.Context) string {
	return FirstHeader(ctx, httpMethod)
}

func HttpURI(ctx context.Context) string {
	return FirstHeader(ctx, httpURI)
}

func HttpPath(ctx context.Context) string {
	return FirstHeader(ctx, httpPath)
}

// LogUnaryServerInterceptor returns a new unary server interceptors that performs per-request log.
func LogUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		logger := pkglog.GetLogger(ctx)
		startTime := time.Now()
		method := strings.ToUpper(HttpMethod(ctx))
		ret, err := handler(ctx, req)
		if time.Since(startTime).Milliseconds() > 5000 {
			logger.Warn("slow api call",
				zap.String("method", method), zap.String("url", HttpURI(ctx)),
				zap.Int64("time", time.Since(startTime).Milliseconds()))
		} else {
			logger.Info("api call", zap.String("method", method), zap.String("url", HttpURI(ctx)))
		}

		httpCode := 0
		if ec, ok := err.(errcode.ErrorCode); ok {
			httpCode = ec.Code().HTTPCode()
		}
		if berr, ok := err.(bizerr.BizError); ok {
			httpCode = berr.HttpStatus()
		}

		fields := func() []zap.Field {
			redaction := redact.PasswordFields
			payload, reqErr := redact.RedactKeysToJSON(redaction, req)
			if reqErr != nil {
				logger.Error("error during request redaction", zap.Error(reqErr))
			}
			resp, respErr := redact.RedactKeysToJSON(redaction, ret)
			if respErr != nil {
				logger.Error("error during response redaction", zap.Error(respErr))
			}
			return []zap.Field{
				zap.String("method", HttpMethod(ctx)),
				zap.Int("httpCode", httpCode),
				zap.String("url", HttpURI(ctx)),
				zap.Any("user", ctx.Value(auth0.AuthUserIDKey)),
				zap.String("msg", fmt.Sprintf("%T", req)),
				zap.String("request", string(payload)),
				zap.String("response", string(resp)),
			}
		}

		if err != nil {
			if httpCode == 0 {
				httpCode = 500
			}

			grpcErr, ok := status.FromError(err)
			userCanceledRequest := ok && grpcErr.Code() == codes.Canceled

			if httpCode >= 500 && !userCanceledRequest {
				logger.Error("call API failed", append(fields(), zap.Error(err))...)
			} else {
				logger.Info("call API failed", append(fields(), zap.Error(err))...)
			}
		} else {
			if httpCode == 0 {
				httpCode = 200
			}
			if method != "" && method != "GET" && method != "OPTION" {
				logger.Info("mutation api call", fields()...)
			} else {
				logger.Info("query api call", fields()...)
			}
		}
		return ret, err
	}
}

type httpMetricsHandler struct {
	handlers map[string][]Handler
}

func newHttpMetricsHandler(mux *runtime.ServeMux) *httpMetricsHandler {
	pointerVal := reflect.ValueOf(mux)
	val := reflect.Indirect(pointerVal)
	member := val.FieldByName("handlers")
	ptrToHandlers := unsafe.Pointer(member.UnsafeAddr()) //nolint:gosec
	handlers := (*map[string][]Handler)(ptrToHandlers)
	return &httpMetricsHandler{*handlers}
}

func (hd *httpMetricsHandler) Handler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tBegin := time.Now()
		rl := &ResponseLogger{W: w, Status: 200}
		h.ServeHTTP(rl, r)
		path := hd.getURLPattern(r)
		if path != "" {
			method := strings.ToLower(r.Method)
			statusCodeStr := fmt.Sprintf("%d", rl.Status)
			duration := float64(time.Since(tBegin)) / float64(time.Millisecond)
			metrics.HttpRequestDuration.WithLabelValues(path, method, statusCodeStr).Observe(duration)
		}
	})
}

func (hd *httpMetricsHandler) getURLPattern(r *http.Request) string {
	path := r.URL.Path
	if strings.HasPrefix(path, "/") {
		components := strings.Split(path[1:], "/")
		l := len(components)
		var verb string
		if idx := strings.LastIndex(components[l-1], ":"); idx > 0 {
			c := components[l-1]
			components[l-1], verb = c[:idx], c[idx+1:]
		}
		for _, h := range hd.handlers[r.Method] {
			_, err := h.Pattern.Match(components, verb)
			if err != nil {
				continue
			}
			return h.Pattern.String()
		}
	}
	return ""
}

type Handler struct {
	Pattern     runtime.Pattern
	HandlerFunc runtime.HandlerFunc
}

// ResponseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP Status code
type ResponseLogger struct {
	W      http.ResponseWriter
	Status int
}

func (l *ResponseLogger) Header() http.Header {
	return l.W.Header()
}

func (l *ResponseLogger) Write(b []byte) (int, error) {
	size, err := l.W.Write(b)
	return size, err
}

func (l *ResponseLogger) WriteHeader(s int) {
	l.W.WriteHeader(s)
	l.Status = s
}

func (l *ResponseLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	if h, ok := l.W.(http.Hijacker); ok {
		return h.Hijack()
	}
	return nil, nil, errors.New("the internal writer is not a http.Hijacker instance")
}

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

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,
}

var bizCode2HTTPStatusMapping = map[bizerr.ErrCode]int{
	bizerr.ErrCodeUnknown:          http.StatusInternalServerError,
	bizerr.ErrCodeOK:               http.StatusOK,
	bizerr.ErrCodeParams:           http.StatusBadRequest,
	bizerr.ErrCodeTimeout:          http.StatusGatewayTimeout,
	bizerr.ErrCodeRecordNotFound:   http.StatusNotFound,
	bizerr.ErrCodeRecordExist:      http.StatusConflict,
	bizerr.ErrCodePermission:       http.StatusForbidden,
	bizerr.ErrCodeTooManyRequests:  http.StatusTooManyRequests,
	bizerr.ErrCodeAuth:             http.StatusUnauthorized,
	bizerr.ErrCodeUnderMaintenance: http.StatusServiceUnavailable,
}

// todo: refactor
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)
	}
}

func bizCode2HTTPCode(bizcode bizerr.ErrCode) int {
	// grpc error code
	if bizcode <= MaxGrpcErrCode {
		return runtime.HTTPStatusFromCode(codes.Code(bizcode))
	}
	// customized error code
	httpStatus, ok := bizCode2HTTPStatusMapping[bizcode]
	if ok {
		return httpStatus
	}
	// biz error code
	return bizerr.New(bizcode, "").HttpStatus()
}
