package router

import (
	"context"
	"fmt"
	"net/http"
	"net/textproto"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	dmpbv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tigrpcclient "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.uber.org/zap"
	"google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

const (
	healthzEndpoint = "/healthz"
	// ResponseKeyStatusCode is the key of the HTTP status code in the response header of gRPC.
	// if we return an error in the gRPC service, the HTTP response body will be something like
	// `{"code":2, "message":"...", "details":[]}`.
	// so we choose to return a nil error and set the HTTP status code in the response header of gRPC.
	ResponseKeyStatusCode = "x-http-code"
)

var grpcGatewayPathPrefixes = []string{"/proxy/dataplanes", "/proxy/controlPlane", "/dataplanes", "/nodepoolconfigs", "/kongAuditLogSettings", "/k8sinfos"}

func setupGRPCGateway(ctx context.Context, grpcAddr string) *gwruntime.ServeMux {
	conn, err := tigrpcclient.NewClientConn(grpcAddr, tigrpcclient.WithDefault())
	if err != nil {
		tilog.Fatal(ctx, "Failed to dial gRPC server", zap.String("addr", grpcAddr), zap.Error(err))
	}
	defer func() {
		go func() {
			<-ctx.Done()
			if cerr := conn.Close(); cerr != nil {
				tilog.Info(ctx, "Failed to close conn to %s: %v", zap.String("addr", grpcAddr), zap.Error(cerr))
			}
		}()
	}()

	jsonpb := &gwruntime.JSONPb{
		MarshalOptions: protojson.MarshalOptions{
			UseProtoNames:   false,
			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 := &gwruntime.HTTPBodyMarshaler{
		Marshaler: jsonpb,
	}

	mux := gwruntime.NewServeMux(
		gwruntime.WithMarshalerOption(gwruntime.MIMEWildcard, marshaler),
		gwruntime.WithForwardResponseOption(httpResponseModifier),
		// refer to https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/
		gwruntime.WithHealthEndpointAt(grpc_health_v1.NewHealthClient(conn), healthzEndpoint),
	)

	if err := dmpbv1.RegisterDataPlaneServiceHandler(ctx, mux, conn); err != nil {
		tilog.Fatal(ctx, "Failed to register gRPC gateway for dataplane service", zap.Error(err))
	}
	if err := dmpbv1.RegisterLoadBalancerServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for lb service", zap.Error(err))
	}
	if err := dmpbv1.RegisterPrivateLinkConnectionServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for private link connection service", zap.Error(err))
	}
	if err := dmpbv1.RegisterBucketServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for bucket service", zap.Error(err))
	}
	if err := dmpbv1.RegisterKeyServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for key service", zap.Error(err))
	}
	if err := dmpbv1.RegisterKeyspaceRoleServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for keyspace role service", zap.Error(err))
	}
	if err := dmpbv1.RegisterVpcPeeringServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for vpc peering service", zap.Error(err))
	}
	if err := dmpbv1.RegisterControlPlaneK8SProxyServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for control plane proxy service", zap.Error(err))
	}
	if err := dmpbv1.RegisterDataPlaneK8SProxyServiceHandler(ctx, mux, conn); err != nil {
		tilog.Error(ctx, "Failed to register gRPC gateway for dataplane proxy service", zap.Error(err))
	}

	return mux
}

func httpResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
	md, ok := gwruntime.ServerMetadataFromContext(ctx)
	if !ok {
		return nil
	}

	// set http status code
	if vals := md.HeaderMD.Get(ResponseKeyStatusCode); len(vals) > 0 {
		code, err := strconv.Atoi(vals[0])
		if err != nil {
			return err
		}
		// delete the headers to not expose any grpc-metadata in http response
		delete(md.HeaderMD, ResponseKeyStatusCode)
		delete(w.Header(), fmt.Sprintf("Grpc-Metadata-%s", textproto.CanonicalMIMEHeaderKey(ResponseKeyStatusCode)))
		w.WriteHeader(code)
	}

	return nil
}

func ServeGrpcGatewayPath(ctx *gin.Context) {
	originalPath := ctx.Request.URL.Path
	var truePath string
	for _, prefix := range grpcGatewayPathPrefixes {
		i := strings.Index(originalPath, prefix)
		if i != -1 {
			truePath = originalPath[i:]
			break
		}
	}

	if truePath == "" {
		ctx.Next()
		return
	}

	if strings.HasSuffix(truePath, "/") {
		truePath = strings.TrimSuffix(truePath, "/")
	}
	tilog.Debug(ctx, "get true path", zap.String("originalPath", originalPath), zap.String("truePath", truePath))
	ctx.Request.URL.Path = truePath
	ctx.Next()
	ctx.Request.URL.Path = originalPath
}
