package rpcserver

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"sync"

	"github.com/go-logr/logr"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

type GatewayHandler interface {
	Name() string
	RegisterGateway(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error
}

type HTTPMiddleware func(http.Handler) http.Handler

type RPCGatewayServerParam struct {
	// Address is the listening address for the gRPC Gateway server, e.g. ":8080".
	Address string
	// RPCAddress is the upstream RPC server address the gateway calls, e.g. ":9090".
	RPCAddress string
	// Logger is used to log the RPC Gateway server internal info, e.g. error and warning.
	Logger logr.Logger
	// ErrorHandler handles gRPC Gateway errors.
	ErrorHandler runtime.ErrorHandlerFunc
	// TraceName enables HTTP and gRPC client tracing for the gateway when set.
	TraceName string
	// Handlers is the list of handlers that will be registered to the RPC Gateway server.
	Handlers []GatewayHandler
	// Middlewares is the list of HTTP middlewares applied to the RPC Gateway server.
	Middlewares []HTTPMiddleware
	// MuxOptions configures the grpc-gateway request multiplexer.
	MuxOptions []runtime.ServeMuxOption
}

type RPCGatewayServer struct {
	server     *http.Server
	conn       *grpc.ClientConn
	cancel     context.CancelFunc
	closeOnce  sync.Once
	closeError error
	logger     logr.Logger
}

func NewRPCGatewayServer(param RPCGatewayServerParam) (*RPCGatewayServer, error) {
	ctx, cancel := context.WithCancel(context.Background())
	clientOptions := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	if param.TraceName != "" {
		clientOptions = append(clientOptions, grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
	}
	conn, err := grpc.NewClient(param.RPCAddress, clientOptions...)
	if err != nil {
		cancel()
		return nil, fmt.Errorf("create rpc gateway client failed: %s", err)
	}

	s := &RPCGatewayServer{
		conn:   conn,
		cancel: cancel,
		logger: param.Logger.WithName("RPCGatewayServer"),
	}

	muxOptions := append([]runtime.ServeMuxOption(nil), param.MuxOptions...)
	if param.ErrorHandler != nil {
		muxOptions = append(muxOptions, runtime.WithErrorHandler(param.ErrorHandler))
	}
	mux := runtime.NewServeMux(muxOptions...)
	for _, handler := range param.Handlers {
		s.logger.Info("register a gateway handler", "handler", handler.Name())
		if err := handler.RegisterGateway(ctx, mux, conn); err != nil {
			_ = s.closeClient()
			return nil, fmt.Errorf("register gateway handler %s failed: %s", handler.Name(), err)
		}
	}

	gatewayHandler := http.Handler(mux)
	for i := len(param.Middlewares) - 1; i >= 0; i-- {
		gatewayHandler = param.Middlewares[i](gatewayHandler)
	}
	if param.TraceName != "" {
		gatewayHandler = otelhttp.NewHandler(gatewayHandler, param.TraceName)
	}
	s.server = &http.Server{
		Addr:    param.Address,
		Handler: gatewayHandler,
	}
	return s, nil
}

// Run runs the RPC Gateway server and blocks until the server exits.
func (s *RPCGatewayServer) Run() error {
	err := s.server.ListenAndServe()
	if err != nil && !errors.Is(err, http.ErrServerClosed) {
		s.logger.Error(err, "rpc gateway server is shutdown but got a error")
		return fmt.Errorf("failed to run rpc gateway server: %s", err)
	}

	s.logger.Info("rpc gateway server is shutdown")
	return nil
}

// Shutdown stops the RPC Gateway server and waits for server to exit.
func (s *RPCGatewayServer) Shutdown(ctx context.Context) error {
	s.logger.Info("rpc gateway server is shutting down by caller")
	return errors.Join(s.server.Shutdown(ctx), s.closeClient())
}

func (s *RPCGatewayServer) closeClient() error {
	s.closeOnce.Do(func() {
		s.cancel()
		s.closeError = s.conn.Close()
	})
	return s.closeError
}
