package handlers import ( "bookhoard/internal/database" "net/http" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) type SyncHandler struct { db *database.Queries } func NewSyncHandler(db *database.Queries) *SyncHandler { return &SyncHandler{db: db} } type UnlinkedBookResponse struct { Unlinked []UnlinkedBookItem `json:"unlinked"` Total int `json:"total"` } type UnlinkedBookItem struct { UnlinkedBookID string `json:"unlinked_book_id"` DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` DeviceType string `json:"device_type"` ContentId string `json:"content_id"` FilePath string `json:"file_path,omitempty"` Title string `json:"title,omitempty"` Author string `json:"author,omitempty"` ConfidenceScore float64 `json:"confidence_score"` LastSeenAt time.Time `json:"last_seen_at"` CreatedAt time.Time `json:"created_at"` } type LinkBookRequest struct { UnlinkedBookID string `json:"unlinked_book_id"` MediaItemID string `json:"media_item_id"` ConfidenceScore float64 `json:"confidence_score"` } type LinkBookResponse struct { Status string `json:"status"` UnlinkedBookID string `json:"unlinked_book_id"` MediaItemID string `json:"media_item_id"` Message string `json:"message"` } // GetUnlinkedBooks returns all unlinked books for a user's devices // GET /api/sync/unlinked-books func (h *SyncHandler) GetUnlinkedBooks(c *echo.Context) error { // Get user from context (assuming auth middleware sets this) user := c.Get("user") if user == nil { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "unauthorized", }) } userID := user.(database.Users).ID // Get all devices for this user devices, err := h.db.ListDevicesByUser(c.Request().Context(), pgtype.UUID{Bytes: userID.Bytes, Valid: true}) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "failed to fetch devices", }) } var allUnlinkedBooks []UnlinkedBookItem // For each device, get unlinked books for _, device := range devices { unlinkedBooks, err := h.db.GetUnlinkedBooksByDevice(c.Request().Context(), pgtype.UUID{Bytes: device.ID.Bytes, Valid: true}) if err != nil { continue } for _, ub := range unlinkedBooks { allUnlinkedBooks = append(allUnlinkedBooks, UnlinkedBookItem{ UnlinkedBookID: uuid.UUID(ub.ID.Bytes).String(), DeviceID: uuid.UUID(ub.DeviceID.Bytes).String(), DeviceName: ub.DeviceName, DeviceType: ub.DeviceType, ContentId: ub.ContentID, FilePath: ub.FilePath.String, Title: ub.Title.String, Author: ub.Author.String, ConfidenceScore: ub.ConfidenceScore.Float64, LastSeenAt: ub.LastSeenAt.Time, CreatedAt: ub.CreatedAt.Time, }) } } return c.JSON(http.StatusOK, UnlinkedBookResponse{ Unlinked: allUnlinkedBooks, Total: len(allUnlinkedBooks), }) } // LinkBook manually links an unlinked book to a media item // POST /api/sync/link-book func (h *SyncHandler) LinkBook(c *echo.Context) error { var req LinkBookRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{ "error": "invalid request format", }) } // Parse UUIDs unlinkedBookID, err := uuid.Parse(req.UnlinkedBookID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{ "error": "invalid unlinked_book_id", }) } mediaItemID, err := uuid.Parse(req.MediaItemID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{ "error": "invalid media_item_id", }) } // Get the unlinked books to find the one we're looking for // We need to use GetAllUnlinkedBooks or create a new query // For now, let's link directly using LinkUnlinkedBook _, err = h.db.LinkUnlinkedBook(c.Request().Context(), database.LinkUnlinkedBookParams{ ID: pgtype.UUID{Bytes: unlinkedBookID, Valid: true}, MediaItemID: pgtype.UUID{Bytes: mediaItemID, Valid: true}, ConfidenceScore: pgtype.Float8{Float64: req.ConfidenceScore, Valid: true}, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "failed to link book", }) } // Note: We can't create a device catalog entry without knowing the device_id and content_id // In a real implementation, we'd first fetch the unlinked book, then create the catalog entry return c.JSON(http.StatusOK, LinkBookResponse{ Status: "linked", UnlinkedBookID: req.UnlinkedBookID, MediaItemID: req.MediaItemID, Message: "Book successfully linked", }) }