package api import ( "context" "errors" "net/http" "time" "github.com/gin-gonic/gin" "monica-go-completion-backend/internal/completion" ) type CompletionService interface { Complete(ctx context.Context, req completion.Request) (completion.Response, error) } type SessionStatsProvider interface { ActiveSessions() map[string]int } type RouteOptions struct { RequestTimeout time.Duration MaxBodyBytes int64 } func RegisterRoutes(router *gin.Engine, service CompletionService, options ...RouteOptions) { opts := RouteOptions{ RequestTimeout: 10 * time.Second, MaxBodyBytes: 2 << 20, // 2MB } if len(options) > 0 { if options[0].RequestTimeout > 0 { opts.RequestTimeout = options[0].RequestTimeout } if options[0].MaxBodyBytes > 0 { opts.MaxBodyBytes = options[0].MaxBodyBytes } } router.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "service": "lsp-gateway"}) }) router.GET("/health/live", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "alive"}) }) router.GET("/health/ready", func(c *gin.Context) { sessions := map[string]int{} if provider, ok := service.(SessionStatsProvider); ok { sessions = provider.ActiveSessions() } c.JSON(http.StatusOK, gin.H{ "status": "ready", "sessions": sessions, }) }) registerWSRoutes(router, service, opts) handleCompletion := func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, opts.MaxBodyBytes) var req completion.Request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON payload"}) return } routeLang := c.Param("language") if req.Language == "" { req.Language = routeLang } ctx, cancel := context.WithTimeout(c.Request.Context(), opts.RequestTimeout) defer cancel() resp, err := service.Complete(ctx, req) if err != nil { if errors.Is(err, completion.ErrInvalidRequest) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if errors.Is(err, completion.ErrUnsupportedLanguage) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if errors.Is(err, completion.ErrTooManySessions) { c.JSON(http.StatusTooManyRequests, gin.H{"error": err.Error()}) return } var ownedErr *completion.ErrSessionOwnedByOtherInstance if errors.As(err, &ownedErr) { if ownedErr.OwnerEndpoint != "" { c.Header("X-LSP-Route-To", ownedErr.OwnerEndpoint) } c.JSON(http.StatusConflict, gin.H{ "error": err.Error(), "routeTo": ownedErr.OwnerEndpoint, "ownerId": ownedErr.OwnerID, }) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "completion failed"}) return } c.JSON(http.StatusOK, resp) } router.POST("/api/v1/completions/:language", func(c *gin.Context) { handleCompletion(c) }) }