package api import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/insertr/insertr/internal/auth" "github.com/insertr/insertr/internal/content" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" ) // ContentHandler handles all content-related HTTP requests type ContentHandler struct { repository db.ContentRepository authService *auth.AuthService siteManager *content.SiteManager engine *engine.ContentEngine } // NewContentHandler creates a new content handler func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler { // Create repository for all database operations repository := database.NewContentRepository() return &ContentHandler{ repository: repository, authService: authService, siteManager: nil, // Will be set via SetSiteManager engine: engine.NewContentEngine(repository), } } // SetSiteManager sets the site manager for file enhancement func (h *ContentHandler) SetSiteManager(siteManager *content.SiteManager) { h.siteManager = siteManager } // GetContent handles GET /api/content/{id} func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) { contentID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } content, err := h.repository.GetContent(context.Background(), siteID, contentID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Content not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(content) } // GetAllContent handles GET /api/content with site_id parameter func (h *ContentHandler) GetAllContent(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } contentMap, err := h.repository.GetAllContent(context.Background(), siteID) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } // Convert map to slice for consistent API response var content []db.ContentItem for _, item := range contentMap { content = append(content, item) } response := db.ContentResponse{Content: content} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // GetBulkContent handles POST /api/content/bulk for batch fetching func (h *ContentHandler) GetBulkContent(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site_id") contentIDs := r.URL.Query()["ids"] if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } if len(contentIDs) == 0 { http.Error(w, "at least one content ID is required", http.StatusBadRequest) return } contentMap, err := h.repository.GetBulkContent(context.Background(), siteID, contentIDs) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } // Convert map to slice for consistent API response var content []db.ContentItem for _, item := range contentMap { content = append(content, item) } response := db.ContentResponse{Content: content} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // CreateOrUpdateContent handles POST /api/content for creating/updating content func (h *ContentHandler) CreateOrUpdateContent(w http.ResponseWriter, r *http.Request) { userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } var req CreateContentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } // Use engine to generate ID from HTML structure result, err := h.engine.ProcessContent(engine.ContentInput{ HTML: []byte(req.HTMLMarkup), FilePath: req.FilePath, SiteID: req.SiteID, Mode: engine.IDGeneration, }) if err != nil { http.Error(w, fmt.Sprintf("Failed to process content: %v", err), http.StatusInternalServerError) return } if len(result.Elements) == 0 { http.Error(w, "No content elements found in markup", http.StatusBadRequest) return } contentID := result.Elements[0].ID // Check if content already exists existingContent, _ := h.repository.GetContent(context.Background(), req.SiteID, contentID) var content *db.ContentItem if existingContent == nil { // Create new content content, err = h.repository.CreateContent( context.Background(), req.SiteID, contentID, req.HTMLContent, req.OriginalTemplate, userInfo.ID, ) } else { // Update existing content - this would need UpdateContent method in repository // For now, we'll return the existing content content = existingContent } if err != nil { http.Error(w, fmt.Sprintf("Failed to save content: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(content) } // DeleteContent handles DELETE /api/content/{id} func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) { _, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } contentID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } // Note: DeleteContent method would need to be added to repository interface // For now, return not implemented _ = contentID // Suppress unused variable warning http.Error(w, "Delete operation not yet implemented", http.StatusNotImplemented) } // GetCollection handles GET /api/collections/{id} func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } collection, err := h.repository.GetCollection(context.Background(), siteID, collectionID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Collection not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(collection) } // GetCollectionItems handles GET /api/collections/{id}/items func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } items, err := h.repository.GetCollectionItems(context.Background(), siteID, collectionID) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(items) } // CreateCollectionItem handles POST /api/collections/{id}/items func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) { userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } collectionID := chi.URLParam(r, "id") var req CreateCollectionItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } if req.SiteID == "" || req.CollectionID == "" { req.SiteID = r.URL.Query().Get("site_id") req.CollectionID = collectionID } if req.SiteID == "" { http.Error(w, "site_id is required", http.StatusBadRequest) return } if req.TemplateID == 0 { req.TemplateID = 1 // Default to first template } // Use atomic collection item creation from repository createdItem, err := h.repository.CreateCollectionItemAtomic( context.Background(), req.SiteID, req.CollectionID, req.TemplateID, userInfo.ID, ) if err != nil { http.Error(w, fmt.Sprintf("Failed to create collection item: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(createdItem) } // RegisterRoutes registers all the content API routes func (h *ContentHandler) RegisterRoutes(r chi.Router) { r.Route("/api", func(r chi.Router) { // Content routes r.Get("/content/{id}", h.GetContent) r.Get("/content", h.GetAllContent) r.Post("/content/bulk", h.GetBulkContent) r.Post("/content", h.CreateOrUpdateContent) r.Delete("/content/{id}", h.DeleteContent) // Collection routes r.Get("/collections/{id}", h.GetCollection) r.Get("/collections/{id}/items", h.GetCollectionItems) r.Post("/collections/{id}/items", h.CreateCollectionItem) }) }