package server

import (
	"context"
	"fmt"
	"mime"
	"net/http"
	"path"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/tidbcloud/account/commons/config"

	"github.com/tidbcloud/tiinf/sdk/cors"

	"github.com/tidbcloud/tiinf/sdk/auth"

	"github.com/tidbcloud/tiinf/tiinf"

	"github.com/gin-gonic/gin"
	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/errors"
	"github.com/pingcap/log"
	pkg_http "github.com/tidbcloud/account/pkg/grpc/http"
	grpc_errcode "github.com/tidbcloud/account/pkg/grpc/middleware/errcode"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/server"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/encoding/protojson"
)

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

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

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

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

// ServeGateway serves the GRPC gateway
type ServeGateway struct {
	eng  *gin.Engine
	conn *grpc.ClientConn
	mux  *runtime.ServeMux
	m    *httpMetricsHandler
}

// Engine returns the embedded gin engine
func (s *ServeGateway) Engine() *gin.Engine {
	return s.eng
}

func (s *ServeGateway) Mux() *runtime.ServeMux {
	return s.mux
}

func (s *ServeGateway) HttpMetricsHandler() *httpMetricsHandler {
	return s.m
}

func (s *ServeGateway) Register(ctx context.Context, registerFuncs ...RegisterHandler) error {
	for _, registerFunc := range registerFuncs {
		if err := registerFunc(ctx, s.mux, s.conn); err != nil {
			return err
		}
	}
	return nil
}

// newHTTPGateway 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 newHTTPGateway(inf tiinf.TiInf, addr string, gwOpts GatewayOptions, errorFormat grpc_errcode.ErrorFormat) (*ServeGateway, error) {
	tilog.Info(context.Background(), "Initializing HTTP gRPC-Gateway", zap.String("address", addr))

	eng := inf.GinEngine()

	// 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(log.L(), gwOpts),
		grpc.WithInsecure(),
		grpc.WithChainUnaryInterceptor(
			otelgrpc.UnaryClientInterceptor(),
		),
	)
	// create a connection that can be used by a gateway
	conn, err := grpc.DialContext(
		context.Background(),
		dialAddr,
		dialOpts...,
	)
	if err != nil {
		return nil, errors.Annotate(err, "failed to dial server:")
	}

	useProtoNames := true
	errorHandler := runtime.DefaultHTTPErrorHandler
	if errorFormat == grpc_errcode.ErrorFormatOpenAPI {
		errorHandler = OpenAPIHTTPErrorHandler
		useProtoNames = false
	}

	mux := runtime.NewServeMux(
		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
			MarshalOptions: protojson.MarshalOptions{
				UseProtoNames:   useProtoNames,
				EmitUnpopulated: true,
			},
			UnmarshalOptions: protojson.UnmarshalOptions{
				DiscardUnknown: true,
			},
		}),
		runtime.WithIncomingHeaderMatcher(allHeaderMatcher),
		runtime.WithMetadata(pkg_http.AddHttpDetail),
		runtime.WithOutgoingHeaderMatcher(pkg_http.AddHeaderInResponseMatcher),
		runtime.WithErrorHandler(errorHandler),
		runtime.WithForwardResponseOption(tigrpc.GRPCGatewayHTTPResponseModifier),
		//runtime.WithMetadata(useadapter.AddHttpMetadata2GrpcContext),
		// Add http metadata for auth access check
		runtime.WithMetadata(auth.AddHttpMetadata2GrpcContext),
		//runtime.WithMetadata(grpcGatewayReceiveHttpHeader),
		runtime.WithRoutingErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) {
			// change DefaultRoutingErrorHandler: http.StatusMethodNotAllowed -> codes.NotFound, avoid 501 error alert
			sterr := status.Error(codes.Internal, "Unexpected routing error")
			switch httpStatus {
			case http.StatusBadRequest:
				sterr = status.Error(codes.InvalidArgument, http.StatusText(httpStatus))
			case http.StatusMethodNotAllowed:
				sterr = status.Error(codes.NotFound, http.StatusText(httpStatus))
			case http.StatusNotFound:
				sterr = status.Error(codes.NotFound, http.StatusText(httpStatus))
			}
			errorHandler(ctx, mux, marshaler, w, r, sterr)
		}),
	)

	m := newHttpMetricsHandler(mux)
	eng.Any("/api/v1/*any", gin.WrapH(m.Handler(CorsAllowAll.Handler(stripBracketsFromQueryKeys(mux)))))
	eng.Any("/api/v1beta/*any", gin.WrapH(m.Handler(CorsAllowAll.Handler(stripBracketsFromQueryKeys(mux)))))
	eng.Any("/external-api/v1/*any", gin.WrapH(m.Handler(CorsAllowAll.Handler(stripBracketsFromQueryKeys(mux)))))
	eng.Any("/inner/account-central/*any", gin.WrapH(m.Handler(CorsAllowAll.Handler(stripBracketsFromQueryKeys(mux)))))

	serve := &ServeGateway{
		eng:  eng,
		conn: conn,
		mux:  mux,
		m:    m,
	}

	return serve, nil
}

func stripBracketsFromQueryKeys(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		newQuery := make(map[string][]string)

		for key, vals := range query {
			newKey := strings.TrimSuffix(key, "[]")
			newQuery[newKey] = append(newQuery[newKey], vals...)
		}
		q := r.URL.Query()
		for k := range q {
			q.Del(k)
		}
		for k, v := range newQuery {
			for _, vv := range v {
				q.Add(k, vv)
			}
		}
		r.URL.RawQuery = q.Encode()
		next.ServeHTTP(w, r)
	})
}

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

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