package http

import (
	"context"
	"fmt"
	"io"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/gogo/grpc-example/insecure"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/juju/errors"
	httpmiddleware "github.com/tidbcloud/pkgv2/http/middleware"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/utils"
	"github.com/tidbcloud/tiinf/sdk/cors"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/grpclog"
	grpc_status "google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

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

func SetupHTTPGateway(
	eng *gin.Engine,
	consoleRegisterHandlers, v1betaRegisterHandlers, v1beta1RegisterHandlers []RegisterHandler,
) error {
	eng.Use(
		logid.InjectLogid4GRPCGatewayMW,
		tigin.RecordMetrics,
		otelgin.Middleware("serverless-svc"),
		httpmiddleware.TraceRequest,
		tigin.SetResponseTraceID,
		gin.Recovery(),
	)

	if err := setupConsoleRoute(eng, consoleRegisterHandlers...); err != nil {
		return errors.Annotate(err, "setup console API routing failed.")
	}

	if err := setupV1betaRoute(eng, v1betaRegisterHandlers...); err != nil {
		return errors.Annotate(err, "setup OpenAPI v1beta routing failed.")
	}

	if err := setupV1beta1Route(eng, v1beta1RegisterHandlers...); err != nil {
		return errors.Annotate(err, "setup OpenAPI v1beta1 routing failed.")
	}
	return nil
}

func setupConsoleRoute(eng *gin.Engine, registerHandlers ...RegisterHandler) error {
	conn, err := buildConn()
	if err != nil {
		return errors.Annotate(err, "failed to dial server:")
	}
	gwmux := makeV1betaGatewayMux(runtime.DefaultHTTPErrorHandler)

	for _, registerHandler := range registerHandlers {
		err = registerHandler(context.Background(), gwmux, conn)
		if err != nil {
			return errors.Annotate(err, "failed to register gateway:")
		}
	}

	eng.Any(
		"/api/v1/serverless/*any",
		RewriteBracketParams(),
		gin.WrapH(cors.New(config.GetCorsOptions()).Handler(gwmux)),
	)
	// backward compatibility
	eng.Any("/api/v1/devtier/*any", RewriteBracketParams(), gin.WrapH(cors.New(config.GetCorsOptions()).Handler(gwmux)))
	return nil
}

func setupV1betaRoute(eng *gin.Engine, registerHandlers ...RegisterHandler) error {
	conn, err := buildConn()
	if err != nil {
		return errors.Annotate(err, "failed to dial server:")
	}
	gwmux := makeV1betaGatewayMux(v1betaHTTPErrorHandler)

	for _, registerHandler := range registerHandlers {
		err = registerHandler(context.Background(), gwmux, conn)
		if err != nil {
			return errors.Annotate(err, "failed to register gateway:")
		}
	}

	eng.Any("/api/internal/projects/*any", RewriteBracketParams(), gin.WrapH(gwmux))
	eng.Any("/api/v1beta/*any", RewriteBracketParams(), gin.WrapH(gwmux))
	return nil
}

func setupV1beta1Route(eng *gin.Engine, registerHandlers ...RegisterHandler) error {
	conn, err := buildConn()
	if err != nil {
		return errors.Annotate(err, "failed to dial server:")
	}
	gwmux := makeV1beta1GatewayMux(v1beta1HTTPErrorHandler)

	for _, registerHandler := range registerHandlers {
		err = registerHandler(context.Background(), gwmux, conn)
		if err != nil {
			return errors.Annotate(err, "failed to register gateway:")
		}
	}

	eng.Any("/v1beta1/*any", RewriteBracketParams(), gin.WrapH(gwmux))
	return nil
}

func buildConn() (*grpc.ClientConn, error) {
	dialAddr := fmt.Sprintf("localhost:%d", config.Global().ServerConfig.GrpcPort)
	dialOpts := []grpc.DialOption{
		grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(insecure.CertPool, "")),
		grpc.WithBlock(),
		grpc.WithChainUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
	}
	return grpc.DialContext(context.Background(), dialAddr, dialOpts...)
}

func makeV1betaGatewayMux(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),
		// https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/#controlling-path-parameter-unescaping
		runtime.WithUnescapingMode(runtime.UnescapingModeAllExceptSlash),
	)
	return mux
}

// makeV1beta1GatewayMux camel case json marshaler with UseProtoNames: false
func makeV1beta1GatewayMux(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),
		// https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/#controlling-path-parameter-unescaping
		runtime.WithUnescapingMode(runtime.UnescapingModeAllExceptSlash),
	)
	return mux
}

func v1beta1HTTPErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	// return Internal when Marshal failed
	const fallback = `{"code": 500, "message": "failed to marshal error message"}`

	s := grpc_status.Convert(err)
	openapiErr := &utils.V1beta1Error{
		Code:    runtime.HTTPStatusFromCode(s.Code()),
		Message: s.Message(),
		Details: s.Details(),
	}

	w.Header().Del("Trailer")
	w.Header().Del("Transfer-Encoding")
	w.Header().Set("Content-Type", "application/json")

	if s.Code() == codes.Unauthenticated {
		w.Header().Set("WWW-Authenticate", s.Message())
	}

	buf, merr := openapiErr.Marshal()
	if merr != nil {
		grpclog.Infof("Failed to marshal error message %q: %v", s, merr)
		w.WriteHeader(http.StatusInternalServerError)
		if _, err := io.WriteString(w, fallback); err != nil {
			grpclog.Infof("Failed to write response: %v", err)
		}
		return
	}

	w.WriteHeader(openapiErr.Code)
	if _, err := w.Write(buf); err != nil {
		grpclog.Infof("Failed to write response: %v", err)
	}
}

func v1betaHTTPErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	// return Internal when Marshal failed
	const fallback = `{ "error": {"code": 500, "message": "failed to marshal error message"} }`

	s := grpc_status.Convert(err)
	openapiErr := &utils.V1betaError{
		Error: &utils.ErrorDetail{
			Code:    runtime.HTTPStatusFromCode(s.Code()),
			Message: s.Message(),
			Details: s.Details(),
		},
	}

	w.Header().Del("Trailer")
	w.Header().Del("Transfer-Encoding")
	w.Header().Set("Content-Type", "application/json")

	if s.Code() == codes.Unauthenticated {
		w.Header().Set("WWW-Authenticate", s.Message())
	}

	buf, merr := openapiErr.Marshal()
	if merr != nil {
		grpclog.Infof("Failed to marshal error message %q: %v", s, merr)
		w.WriteHeader(http.StatusInternalServerError)
		if _, err := io.WriteString(w, fallback); err != nil {
			grpclog.Infof("Failed to write response: %v", err)
		}
		return
	}

	w.WriteHeader(openapiErr.Error.Code)
	if _, err := w.Write(buf); err != nil {
		grpclog.Infof("Failed to write response: %v", err)
	}
}
