package http_gateway

import (
	"context"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"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/runtime"
	"github.com/pingcap/errors"
	"github.com/rs/cors"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
)

const (
	setHeaderPrefix    = "x-set-header_"
	queryHeaderPrefix  = "x-query-header_"
	cookieHeaderPrefix = "x-cookie-header_"
	httpMethod         = "x-http-method"
	httpURI            = "x-http-uri"
	csrfToken          = "x-csrf-token"
)

var ProdGatewayOptions = GatewayOptions{LogPayload: false}

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

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

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

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

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

type HTTPBodyMarshaler struct {
	runtime.Marshaler
}

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

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

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

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
	Debug:            true,
})

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

// 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("/debug-api/v1/*any", gin.WrapH(m.Handler(DebugAPICors.Handler(gwmux))))

	return nil
}

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) {
		rl := &ResponseLogger{W: w, Status: 200}
		h.ServeHTTP(rl, r)
	})
}

// 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 := make([]grpc.DialOption, 2)
	//dialOpts = append(dialOpts,
	//	grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(insecure.CertPool, "")),
	//	grpc.WithBlock(),
	//)

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

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

// 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 := &HTTPBodyMarshaler{
	//	Marshaler: jsonpb,
	//}
	m := &runtime.JSONPb{EmitDefaults: true, OrigName: true}
	mux := runtime.NewServeMux(
		//runtime.WithMarshalerOption(runtime.MIMEWildcard, marshaler),
		//runtime.WithIncomingHeaderMatcher(AllHeaderMatcher),
		runtime.WithMarshalerOption(runtime.MIMEWildcard, m),
		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.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),
	)
	return mux
}

// AllHeaderMatcher add all headers to grpc
func AllHeaderMatcher(key string) (string, bool) {
	return key, true
}

func AddHeaderInResponseMatcher(key string) (string, bool) {
	if strings.HasPrefix(key, setHeaderPrefix) {
		return strings.ToLower(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
	}
	m[csrfToken] = r.Header.Get(csrfToken)
	return metadata.New(m)
}

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

// FirstHeader retrives the header content from the context.
// If multiples of the header were given, it gives the first.
func FirstHeader(ctx context.Context, header string) string {
	headers, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ""
	}
	headerTexts := headers[strings.ToLower(header)]
	if len(headerTexts) < 1 {
		return ""
	}
	return headerTexts[0]
}

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

func PortToAddress(grpcPort int) string {
	return fmt.Sprintf("0.0.0.0:%d", grpcPort)
}
