1package usecase
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/dumber/internal/domain/repository"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// ManageFavoritesUseCase handles favorite, folder, and tag operations.
13type ManageFavoritesUseCase struct {
14 favoriteRepo repository.FavoriteRepository
15 folderRepo repository.FolderRepository
16 tagRepo repository.TagRepository
17}
18
19// NewManageFavoritesUseCase creates a new favorites management use case.
20func NewManageFavoritesUseCase(
21 favoriteRepo repository.FavoriteRepository,
22 folderRepo repository.FolderRepository,
23 tagRepo repository.TagRepository,
24) *ManageFavoritesUseCase {
25 return &ManageFavoritesUseCase{
26 favoriteRepo: favoriteRepo,
27 folderRepo: folderRepo,
28 tagRepo: tagRepo,
29 }
30}
31
32// AddFavoriteInput contains parameters for adding a favorite.
33type AddFavoriteInput struct {
34 URL string
35 Title string
36 FaviconURL string
37 FolderID *entity.FolderID
38 Tags []entity.TagID
39}
40
41// Add creates a new favorite.
42func (uc *ManageFavoritesUseCase) Add(ctx context.Context, input AddFavoriteInput) (*entity.Favorite, error) {
43 log := logging.FromContext(ctx)
44 log.Debug().Str("url", input.URL).Str("title", input.Title).Msg("adding favorite")
45
46 // Check if already favorited
47 existing, err := uc.favoriteRepo.FindByURL(ctx, input.URL)
48 if err != nil {
49 return nil, fmt.Errorf("failed to check existing favorite: %w", err)
50 }
51 if existing != nil {
52 log.Debug().Str("url", input.URL).Msg("URL already favorited")
53 return existing, nil
54 }
55
56 fav := entity.NewFavorite(input.URL, input.Title)
57 fav.FaviconURL = input.FaviconURL
58 fav.FolderID = input.FolderID
59
60 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
61 return nil, fmt.Errorf("failed to save favorite: %w", err)
62 }
63
64 // Assign tags
65 for _, tagID := range input.Tags {
66 if err := uc.tagRepo.AssignToFavorite(ctx, tagID, fav.ID); err != nil {
67 log.Warn().
68 Int64("tag_id", int64(tagID)).
69 Int64("favorite_id", int64(fav.ID)).
70 Err(err).
71 Msg("failed to assign tag")
72 }
73 }
74
75 log.Info().Str("url", input.URL).Int64("id", int64(fav.ID)).Msg("favorite added")
76 return fav, nil
77}
78
79// Remove deletes a favorite by ID.
80func (uc *ManageFavoritesUseCase) Remove(ctx context.Context, id entity.FavoriteID) error {
81 log := logging.FromContext(ctx)
82 log.Debug().Int64("id", int64(id)).Msg("removing favorite")
83
84 if err := uc.favoriteRepo.Delete(ctx, id); err != nil {
85 return fmt.Errorf("failed to delete favorite: %w", err)
86 }
87
88 log.Info().Int64("id", int64(id)).Msg("favorite removed")
89 return nil
90}
91
92// RemoveByURL deletes a favorite by its URL.
93func (uc *ManageFavoritesUseCase) RemoveByURL(ctx context.Context, url string) error {
94 log := logging.FromContext(ctx)
95 log.Debug().Str("url", url).Msg("removing favorite by URL")
96
97 fav, err := uc.favoriteRepo.FindByURL(ctx, url)
98 if err != nil {
99 return fmt.Errorf("failed to find favorite: %w", err)
100 }
101 if fav == nil {
102 log.Debug().Str("url", url).Msg("favorite not found")
103 return nil
104 }
105
106 return uc.Remove(ctx, fav.ID)
107}
108
109// Update modifies a favorite's metadata.
110func (uc *ManageFavoritesUseCase) Update(ctx context.Context, fav *entity.Favorite) error {
111 log := logging.FromContext(ctx)
112 log.Debug().Int64("id", int64(fav.ID)).Msg("updating favorite")
113
114 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
115 return fmt.Errorf("failed to update favorite: %w", err)
116 }
117
118 log.Info().Int64("id", int64(fav.ID)).Msg("favorite updated")
119 return nil
120}
121
122// Move changes a favorite's folder.
123func (uc *ManageFavoritesUseCase) Move(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
124 log := logging.FromContext(ctx)
125 log.Debug().
126 Int64("favorite_id", int64(id)).
127 Msg("moving favorite to folder")
128
129 if err := uc.favoriteRepo.SetFolder(ctx, id, folderID); err != nil {
130 return fmt.Errorf("failed to move favorite: %w", err)
131 }
132
133 log.Info().Int64("id", int64(id)).Msg("favorite moved")
134 return nil
135}
136
137// SetShortcut assigns or removes a keyboard shortcut (1-9).
138func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
139 log := logging.FromContext(ctx)
140
141 if key != nil && (*key < 1 || *key > 9) {
142 return fmt.Errorf("shortcut key must be 1-9, got %d", *key)
143 }
144
145 log.Debug().
146 Int64("id", int64(id)).
147 Msg("setting favorite shortcut")
148
149 if err := uc.favoriteRepo.SetShortcut(ctx, id, key); err != nil {
150 return fmt.Errorf("failed to set shortcut: %w", err)
151 }
152
153 return nil
154}
155
156// GetByShortcut finds the favorite with the given shortcut key.
157func (uc *ManageFavoritesUseCase) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
158 log := logging.FromContext(ctx)
159 log.Debug().Int("key", key).Msg("getting favorite by shortcut")
160
161 if key < 1 || key > 9 {
162 return nil, fmt.Errorf("shortcut key must be 1-9, got %d", key)
163 }
164
165 return uc.favoriteRepo.GetByShortcut(ctx, key)
166}
167
168// GetByURL finds a favorite by its URL.
169func (uc *ManageFavoritesUseCase) GetByURL(ctx context.Context, url string) (*entity.Favorite, error) {
170 log := logging.FromContext(ctx)
171 log.Debug().Str("url", url).Msg("getting favorite by URL")
172
173 return uc.favoriteRepo.FindByURL(ctx, url)
174}
175
176// GetAll retrieves all favorites.
177func (uc *ManageFavoritesUseCase) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
178 log := logging.FromContext(ctx)
179 log.Debug().Msg("getting all favorites")
180
181 favs, err := uc.favoriteRepo.GetAll(ctx)
182 if err != nil {
183 return nil, fmt.Errorf("failed to get favorites: %w", err)
184 }
185
186 log.Debug().Int("count", len(favs)).Msg("retrieved favorites")
187 return favs, nil
188}
189
190// IsFavorite checks if a URL is favorited.
191func (uc *ManageFavoritesUseCase) IsFavorite(ctx context.Context, url string) (bool, error) {
192 fav, err := uc.favoriteRepo.FindByURL(ctx, url)
193 if err != nil {
194 return false, fmt.Errorf("failed to check favorite: %w", err)
195 }
196 return fav != nil, nil
197}
198
199// ToggleResult indicates the result of a toggle operation.
200type ToggleResult struct {
201 Added bool // true if favorite was added, false if removed
202 URL string
203 Title string
204 Message string // User-friendly message
205}
206
207// Toggle adds or removes a URL from favorites based on current state.
208// If the URL is already a favorite, it removes it. Otherwise, it adds it.
209func (uc *ManageFavoritesUseCase) Toggle(ctx context.Context, url, title string) (*ToggleResult, error) {
210 log := logging.FromContext(ctx)
211 log.Debug().Str("url", url).Msg("toggling favorite")
212
213 if url == "" {
214 return nil, fmt.Errorf("URL cannot be empty")
215 }
216
217 // Check if already favorited
218 existing, err := uc.favoriteRepo.FindByURL(ctx, url)
219 if err != nil {
220 return nil, fmt.Errorf("failed to check existing favorite: %w", err)
221 }
222
223 if existing != nil {
224 // Remove favorite
225 if err := uc.favoriteRepo.Delete(ctx, existing.ID); err != nil {
226 return nil, fmt.Errorf("failed to remove favorite: %w", err)
227 }
228 log.Info().Str("url", url).Int64("id", int64(existing.ID)).Msg("favorite removed via toggle")
229 return &ToggleResult{
230 Added: false,
231 URL: url,
232 Title: existing.Title,
233 Message: "Favorite removed",
234 }, nil
235 }
236
237 // Add favorite
238 fav := entity.NewFavorite(url, title)
239 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
240 return nil, fmt.Errorf("failed to add favorite: %w", err)
241 }
242 log.Info().Str("url", url).Int64("id", int64(fav.ID)).Msg("favorite added via toggle")
243 return &ToggleResult{
244 Added: true,
245 URL: url,
246 Title: title,
247 Message: "Favorite added",
248 }, nil
249}
250
251// GetAllURLs returns a set of all favorited URLs for efficient batch lookup.
252func (uc *ManageFavoritesUseCase) GetAllURLs(ctx context.Context) (map[string]struct{}, error) {
253 log := logging.FromContext(ctx)
254 log.Debug().Msg("getting all favorite URLs")
255
256 favs, err := uc.favoriteRepo.GetAll(ctx)
257 if err != nil {
258 return nil, fmt.Errorf("failed to get favorites: %w", err)
259 }
260
261 urls := make(map[string]struct{}, len(favs))
262 for _, fav := range favs {
263 urls[fav.URL] = struct{}{}
264 }
265
266 log.Debug().Int("count", len(urls)).Msg("retrieved favorite URLs")
267 return urls, nil
268}
269
270// GetTree builds a complete hierarchical view of folders and favorites.
271func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
272 log := logging.FromContext(ctx)
273 log.Debug().Msg("building favorite tree")
274
275 tree := entity.NewFavoriteTree()
276
277 // Get all folders
278 folders, err := uc.folderRepo.GetAll(ctx)
279 if err != nil {
280 return nil, fmt.Errorf("failed to get folders: %w", err)
281 }
282
283 // Build folder map and organize by parent
284 for _, folder := range folders {
285 tree.FolderMap[folder.ID] = folder
286 if folder.ParentID == nil {
287 tree.RootFolders = append(tree.RootFolders, folder)
288 } else {
289 tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder)
290 }
291 }
292
293 // Get all favorites
294 favorites, err := uc.favoriteRepo.GetAll(ctx)
295 if err != nil {
296 return nil, fmt.Errorf("failed to get favorites: %w", err)
297 }
298
299 // Organize favorites by folder
300 for _, fav := range favorites {
301 if fav.FolderID == nil {
302 tree.RootFavorites = append(tree.RootFavorites, fav)
303 } else {
304 tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav)
305 }
306 }
307
308 log.Debug().
309 Int("folders", len(folders)).
310 Int("favorites", len(favorites)).
311 Msg("favorite tree built")
312
313 return tree, nil
314}
315
316// CreateFolder creates a new folder.
317func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name string, parentID *entity.FolderID) (*entity.Folder, error) {
318 log := logging.FromContext(ctx)
319 log.Debug().Str("name", name).Msg("creating folder")
320
321 folder := entity.NewFolder(name)
322 folder.ParentID = parentID
323
324 if err := uc.folderRepo.Save(ctx, folder); err != nil {
325 return nil, fmt.Errorf("failed to create folder: %w", err)
326 }
327
328 log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created")
329 return folder, nil
330}
331
332// DeleteFolder removes a folder. Favorites in the folder are moved to root.
333func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error {
334 log := logging.FromContext(ctx)
335 log.Debug().Int64("id", int64(id)).Msg("deleting folder")
336
337 // Move favorites from this folder to root (or parent)
338 folder, err := uc.folderRepo.FindByID(ctx, id)
339 if err != nil {
340 return fmt.Errorf("failed to find folder: %w", err)
341 }
342 if folder == nil {
343 log.Debug().Int64("id", int64(id)).Msg("folder not found")
344 return nil
345 }
346
347 // Get favorites in this folder
348 favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id)
349 if err != nil {
350 return fmt.Errorf("failed to get folder favorites: %w", err)
351 }
352
353 // Move favorites to parent folder (or root)
354 for _, fav := range favorites {
355 if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil {
356 log.Warn().
357 Int64("favorite_id", int64(fav.ID)).
358 Err(setErr).
359 Msg("failed to move favorite from deleted folder")
360 }
361 }
362
363 // Get child folders
364 children, err := uc.folderRepo.GetChildren(ctx, &id)
365 if err != nil {
366 return fmt.Errorf("failed to get child folders: %w", err)
367 }
368
369 // Recursively delete child folders
370 for _, child := range children {
371 if err := uc.DeleteFolder(ctx, child.ID); err != nil {
372 log.Warn().
373 Int64("child_id", int64(child.ID)).
374 Err(err).
375 Msg("failed to delete child folder")
376 }
377 }
378
379 // Delete the folder itself
380 if err := uc.folderRepo.Delete(ctx, id); err != nil {
381 return fmt.Errorf("failed to delete folder: %w", err)
382 }
383
384 log.Info().Int64("id", int64(id)).Msg("folder deleted")
385 return nil
386}
387
388// GetAllFolders retrieves all folders.
389func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) {
390 log := logging.FromContext(ctx)
391 log.Debug().Msg("getting all folders")
392
393 return uc.folderRepo.GetAll(ctx)
394}
395
396// AddTag creates a new tag.
397func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
398 log := logging.FromContext(ctx)
399 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")
400
401 // Check if tag already exists
402 existing, err := uc.tagRepo.FindByName(ctx, name)
403 if err != nil {
404 return nil, fmt.Errorf("failed to check existing tag: %w", err)
405 }
406 if existing != nil {
407 log.Debug().Str("name", name).Msg("tag already exists")
408 return existing, nil
409 }
410
411 tag := entity.NewTag(name)
412 if color != "" {
413 tag.Color = color
414 }
415
416 if err := uc.tagRepo.Save(ctx, tag); err != nil {
417 return nil, fmt.Errorf("failed to create tag: %w", err)
418 }
419
420 log.Info().Str("name", name).Int64("id", int64(tag.ID)).Msg("tag created")
421 return tag, nil
422}
423
424// DeleteTag removes a tag.
425func (uc *ManageFavoritesUseCase) DeleteTag(ctx context.Context, id entity.TagID) error {
426 log := logging.FromContext(ctx)
427 log.Debug().Int64("id", int64(id)).Msg("deleting tag")
428
429 if err := uc.tagRepo.Delete(ctx, id); err != nil {
430 return fmt.Errorf("failed to delete tag: %w", err)
431 }
432
433 log.Info().Int64("id", int64(id)).Msg("tag deleted")
434 return nil
435}
436
437// GetAllTags retrieves all tags.
438func (uc *ManageFavoritesUseCase) GetAllTags(ctx context.Context) ([]*entity.Tag, error) {
439 log := logging.FromContext(ctx)
440 log.Debug().Msg("getting all tags")
441
442 return uc.tagRepo.GetAll(ctx)
443}
444
445// TagFavorite assigns a tag to a favorite.
446func (uc *ManageFavoritesUseCase) TagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
447 log := logging.FromContext(ctx)
448 log.Debug().
449 Int64("favorite_id", int64(favID)).
450 Int64("tag_id", int64(tagID)).
451 Msg("tagging favorite")
452
453 if err := uc.tagRepo.AssignToFavorite(ctx, tagID, favID); err != nil {
454 return fmt.Errorf("failed to tag favorite: %w", err)
455 }
456
457 return nil
458}
459
460// UntagFavorite removes a tag from a favorite.
461func (uc *ManageFavoritesUseCase) UntagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
462 log := logging.FromContext(ctx)
463 log.Debug().
464 Int64("favorite_id", int64(favID)).
465 Int64("tag_id", int64(tagID)).
466 Msg("untagging favorite")
467
468 if err := uc.tagRepo.RemoveFromFavorite(ctx, tagID, favID); err != nil {
469 return fmt.Errorf("failed to untag favorite: %w", err)
470 }
471
472 return nil
473}
474
475// GetTagsForFavorite retrieves all tags for a favorite.
476func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
477 log := logging.FromContext(ctx)
478 log.Debug().Int64("favorite_id", int64(favID)).Msg("getting tags for favorite")
479
480 return uc.tagRepo.GetForFavorite(ctx, favID)
481}
482
483// UpdateFolder updates a folder's name and icon.
484func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
485 log := logging.FromContext(ctx)
486 log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
487
488 folder, err := uc.folderRepo.FindByID(ctx, id)
489 if err != nil {
490 return fmt.Errorf("failed to find folder: %w", err)
491 }
492 if folder == nil {
493 return fmt.Errorf("folder %d not found", id)
494 }
495
496 if name != "" {
497 folder.Name = name
498 }
499 folder.Icon = icon
500
501 if err := uc.folderRepo.Save(ctx, folder); err != nil {
502 return fmt.Errorf("failed to update folder: %w", err)
503 }
504
505 log.Info().Int64("id", int64(id)).Msg("folder updated")
506 return nil
507}
508
509// UpdateTag updates a tag's name and color.
510func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
511 log := logging.FromContext(ctx)
512 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
513
514 tag, err := uc.tagRepo.FindByID(ctx, id)
515 if err != nil {
516 return fmt.Errorf("failed to find tag: %w", err)
517 }
518 if tag == nil {
519 return fmt.Errorf("tag %d not found", id)
520 }
521
522 if name != "" {
523 tag.Name = name
524 }
525 if color != "" {
526 tag.Color = color
527 }
528
529 if err := uc.tagRepo.Save(ctx, tag); err != nil {
530 return fmt.Errorf("failed to update tag: %w", err)
531 }
532
533 log.Info().Int64("id", int64(id)).Msg("tag updated")
534 return nil
535}