//revive:disable:var-naming

package grpc_http

//revive:enable:var-naming

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

	"github.com/gin-gonic/gin"
	"github.com/gogo/gateway"
	"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/runtime"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/rs/cors"
	"github.com/tidbcloud/metrics-server/central/metrics"
	"github.com/tidbcloud/metrics-server/pkg/auth/authuser"
	pkg_http "github.com/tidbcloud/metrics-server/pkg/grpc/http"
	"github.com/tidbcloud/metrics-server/pkg/redact"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/ctxinfo"
	httpmiddleware "github.com/tidbcloud/pkgv2/http/middleware"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"
	"github.com/tidbcloud/tiinf/sdk/rbac/helperimpl"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"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/credentials"
	"google.golang.org/grpc/metadata"
)

func init() {
	helperimpl.DefaultCachedPermissionHelper.SetReadTimeout(10 * time.Second)
}

// ServeOpenAPI serves an OpenAPI UI on /openapi-ui/
// Adapted from https://github.com/philips/grpc-gateway-example/blob/a269bcb5931ca92be0ceae6130ac27ae89582ecc/cmd/serve.go#L63
func ServeOpenAPI(eng *gin.Engine, root http.FileSystem) error {
	err := mime.AddExtensionType(".svg", "image/svg+xml")
	if err != nil {
		return err
	}

	// Expose files in static on <host>/openapi-ui
	fileServer := http.FileServer(root)
	prefix := "/openapi-ui/"

	eng.Any(path.Join(prefix, "*any"), gin.WrapH(http.StripPrefix(prefix, pkg_http.NoCache(fileServer))))
	return nil
}

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() *runtime.ServeMux {
	jsonpb := &runtime.JSONPb{
		// prettify JSON in development
		// TODO: turn off for production
		EmitDefaults: true,
		Indent:       "  ",
		OrigName:     true,
	}
	// This marshaler enables support to return raw response using HttpBody.
	// Note that this behavior is enabled by default in grpc-gateway v2.
	marshaler := &gateway.HTTPBodyMarshaler{
		Marshaler: jsonpb,
	}
	mux := runtime.NewServeMux(
		runtime.WithMarshalerOption(runtime.MIMEWildcard, marshaler),
		runtime.WithIncomingHeaderMatcher(AllHeaderMatcher),
		runtime.WithMetadata(AddHttpDetail),
		// transfer http metadata to grpc context (rbac need)
		runtime.WithMetadata(useadapter.AddHttpMetadata2GrpcContext),
		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.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),
	)
	return mux
}

// 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
})

// RouteGateway use the eng connected to the grpc server
// addr is the address to the running GRPC server
func RouteGateway(eng *gin.Engine, logger *zap.Logger, addr string, gwOpts GatewayOptions, registerHandler RegisterHandler) error {
	conn, err := GatewayConn(logger, addr, gwOpts)
	if err != nil {
		return errors.Annotate(err, "failed to dial server:")
	}
	gwmux := MakeGatewayMux()
	err = registerHandler(context.Background(), gwmux, conn)
	if err != nil {
		return errors.Annotate(err, "failed to register gateway:")
	}
	m := newHttpMetricsHandler(gwmux)
	eng.Any("/api/v1/*any", gin.WrapH(m.Handler(CorsAllowAll.Handler(gwmux))))

	return nil
}

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

// ServeHTTPGateway is a standard way of running a GRPC service with an HTTP gateway
// The API is a bit complex because it lets the caller log and use go routines
// RegisterServer is a function that takes a runner which will block
// The given middleware will be applied to
func ServeHTTPGateway(logger *zap.Logger, addr string, gwOpts GatewayOptions,
	registerHandler RegisterHandler) (*ServeGateway, error) {

	eng := gin.Default()
	eng.Use(
		ctxinfo.ExtractCtxInfoMW,
		logid.InjectLogid4GRPCGatewayMW,
		otelgin.Middleware("central"),
		setTraceIdMiddleware,
		httpmiddleware.TraceRequest,
	)
	// setup gateway
	err := RouteGateway(eng, logger, addr, gwOpts, registerHandler)
	if err != nil {
		return nil, errors.Annotate(err, "Error routing gateway")
	}
	serve := &ServeGateway{eng: eng}

	return serve, nil
}

// TODO(chenfei): make it better
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"
)

// 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)
}

var (
	CtxIdentityKey = "ctxIdentityKey"

	PathPattern2ResourceMap = map[string]string{
		"post:/api/v1/orgs/(\\d+)/projects/(\\d+)/metrics_key":                            "as/orgs/{org_id}/projects/{project_id}/create_metrics_key",
		"get:/api/v1/orgs/(\\d+)/projects/(\\d+)/metrics_key":                             "as/orgs/{org_id}/projects/{project_id}/list_metrics_key",
		"delete:/api/v1/orgs/(\\d+)/projects/(\\d+)/metrics_key/\\d+":                     "as/orgs/{org_id}/projects/{project_id}/delete_metrics_key",
		"post:/api/v1/orgs/(\\d+)/projects/(\\d+)/integrations":                           "as/orgs/{org_id}/projects/{project_id}/create_or_update_integration",
		"delete:/api/v1/orgs/(\\d+)/projects/(\\d+)/integrations":                         "as/orgs/{org_id}/projects/{project_id}/delete_integration",
		"post:/api/v1/orgs/(\\d+)/projects/(\\d+)/integrations/datadog/api_key/validate":  "as/orgs/{org_id}/projects/{project_id}/validate_datadog_integration_api_key",
		"post:/api/v1/orgs/(\\d+)/projects/(\\d+)/integrations/newrelic/api_key/validate": "as/orgs/{org_id}/projects/{project_id}/validate_newrelic_integration_api_key",
		"get:/api/v1/orgs/(\\d+)/projects/(\\d+)/integrations":                            "as/orgs/{org_id}/projects/{project_id}/get_integration",
		"get:/api/v1/orgs/(\\d+)/projects/(\\d+)/user_alert":                              "as/orgs/{org_id}/projects/{project_id}/user_alert/list",
		"post:/api/v1/orgs/(\\d+)/projects/(\\d+)/user_alert":                             "as/orgs/{org_id}/projects/{project_id}/user_alert/create",
		"delete:/api/v1/orgs/(\\d+)/projects/(\\d+)/user_alert/(\\d+)":                    "as/orgs/{org_id}/projects/{project_id}/user_alert/{user_alert_id}/delete",
	}

	PathWhitelist = []string{}
)

func ExtractIdentityAuthFunc(ctx context.Context) (context.Context, error) {
	uri := useadapter.GetHttpMetadataFromGrpcContext(ctx, common.METADATA_PATH)
	method := useadapter.GetHttpMetadataFromGrpcContext(ctx, common.METADATA_METHOD)
	log.Info(ctx, "in grpc gateway user func", zap.String("uri", uri), zap.String("method", method))

	user, err := authuser.GetUserFromContext(ctx)
	if err != nil {
		return ctx, errors.New("get user from context error")
	}

	// biz need set userID to context with the specified key
	userID := fmt.Sprintf("%d", user.ID)
	newCtx := context.WithValue(ctx, CtxIdentityKey, userID)

	return newCtx, nil
}

func Query(ctx context.Context, name string) string {
	return pkg_http.FirstHeader(ctx, strings.ToLower(queryHeaderPrefix+name))
}

func Cookie(ctx context.Context, name string) string {
	return pkg_http.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 pkg_http.FirstHeader(ctx, httpMethod)
}

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

// 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) {
		startTime := time.Now()
		method := strings.ToUpper(HttpMethod(ctx))
		ret, err := handler(ctx, req)
		if time.Since(startTime).Milliseconds() > 5000 {
			log.Warn(ctx, "slow api call", zap.String("url", HttpURI(ctx)), zap.Int64("time", time.Since(startTime).Milliseconds()))
		}

		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 {
				log.Error(ctx, "error during request redaction", zap.Error(reqErr))
			}
			resp, respErr := redact.RedactKeysToJSON(redaction, ret)
			if respErr != nil {
				log.Error(ctx, "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
			}

			if httpCode >= 500 {
				log.Error(ctx, "call central API failed", append(fields(), zap.Error(err))...)
			} else {
				log.Info(ctx, "call central API failed", append(fields(), zap.Error(err))...)
			}
		} else {
			if httpCode == 0 {
				httpCode = 200
			}
			if method != "" && method != "GET" && method != "OPTION" {
				log.Info(ctx, "mutation api call", fields()...)
			} else {
				log.Debug(ctx, "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")
}
