1package usecase
2
3import (
4 "context"
5 "fmt"
6 "net/url"
7 "sort"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/bnema/dumber/internal/application/dto"
13 "github.com/bnema/dumber/internal/application/port"
14 "github.com/bnema/dumber/internal/domain/entity"
15 "github.com/bnema/dumber/internal/domain/repository"
16 domainurl "github.com/bnema/dumber/internal/domain/url"
17 "github.com/bnema/dumber/internal/logging"
18)
19
20// Compile-time check: ManageFavoritesUseCase must satisfy port.HomepageFavorites.
21var _ port.HomepageFavorites = (*ManageFavoritesUseCase)(nil)
22
23// ManageFavoritesUseCase handles favorite, folder, and tag operations.
24type ManageFavoritesUseCase struct {
25 favoriteRepo repository.FavoriteRepository
26 folderRepo repository.FolderRepository
27 tagRepo repository.TagRepository
28
29 cacheMu sync.Mutex
30 favoritesCache []*entity.Favorite
31 favoriteURLsCache map[string]struct{}
32 cacheTime time.Time
33}
34
35const favoritesCacheTTL = 2 * time.Second
36
37// NewManageFavoritesUseCase creates a new favorites management use case.
38func NewManageFavoritesUseCase(
39 favoriteRepo repository.FavoriteRepository,
40 folderRepo repository.FolderRepository,
41 tagRepo repository.TagRepository,
42) *ManageFavoritesUseCase {
43 return &ManageFavoritesUseCase{
44 favoriteRepo: favoriteRepo,
45 folderRepo: folderRepo,
46 tagRepo: tagRepo,
47 }
48}
49
50// AddFavoriteInput contains parameters for adding a favorite.
51type AddFavoriteInput struct {
52 URL string
53 Title string
54 FaviconURL string
55 FolderID *entity.FolderID
56 Tags []entity.TagID
57}
58
59// Add creates a new favorite.
60func (uc *ManageFavoritesUseCase) Add(ctx context.Context, input AddFavoriteInput) (*entity.Favorite, error) {
61 log := logging.FromContext(ctx)
62 log.Debug().Str("url", logging.RedactURL(input.URL)).Msg("adding favorite")
63
64 // Check if already favorited
65 existing, err := uc.favoriteRepo.FindByURL(ctx, input.URL)
66 if err != nil {
67 return nil, fmt.Errorf("failed to check existing favorite: %w", err)
68 }
69 if existing != nil {
70 log.Debug().Str("url", logging.RedactURL(input.URL)).Msg("URL already favorited")
71 return existing, nil
72 }
73
74 return uc.addNewFavorite(ctx, input)
75}
76
77func (uc *ManageFavoritesUseCase) addNewFavorite(ctx context.Context, input AddFavoriteInput) (*entity.Favorite, error) {
78 log := logging.FromContext(ctx)
79 fav := entity.NewFavorite(input.URL, input.Title)
80 fav.FaviconURL = input.FaviconURL
81 fav.FolderID = input.FolderID
82
83 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
84 return nil, fmt.Errorf("failed to save favorite: %w", err)
85 }
86
87 // Assign tags
88 for _, tagID := range input.Tags {
89 if err := uc.tagRepo.AssignToFavorite(ctx, tagID, fav.ID); err != nil {
90 log.Warn().
91 Int64("tag_id", int64(tagID)).
92 Int64("favorite_id", int64(fav.ID)).
93 Err(err).
94 Msg("failed to assign tag")
95 }
96 }
97
98 log.Info().Str("url", logging.RedactURL(input.URL)).Int64("id", int64(fav.ID)).Msg("favorite added")
99 uc.invalidateCache()
100 return fav, nil
101}
102
103// AddFavorite creates a favorite from the UI-facing application port.
104func (uc *ManageFavoritesUseCase) AddFavorite(ctx context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {
105 for _, tagID := range input.Tags {
106 if tagID <= 0 {
107 return nil, fmt.Errorf("favorite tag id must be positive, got %d", tagID)
108 }
109 }
110 favoriteURL, err := normalizeFavoriteURL(input.URL)
111 if err != nil {
112 return nil, err
113 }
114 if existing, err := uc.findFavoriteByCanonicalOrRawURL(ctx, favoriteURL, input.URL); err != nil {
115 return nil, err
116 } else if existing != nil {
117 return existing, nil
118 }
119 tags := append([]entity.TagID(nil), input.Tags...)
120 title := strings.TrimSpace(input.Title)
121 if title == "" {
122 title = favoriteURL
123 }
124 return uc.addNewFavorite(ctx, AddFavoriteInput{
125 URL: favoriteURL,
126 Title: title,
127 FaviconURL: strings.TrimSpace(input.FaviconURL),
128 FolderID: input.FolderID,
129 Tags: tags,
130 })
131}
132
133func (uc *ManageFavoritesUseCase) findFavoriteByCanonicalOrRawURL(
134 ctx context.Context,
135 canonicalURL string,
136 rawURL string,
137) (*entity.Favorite, error) {
138 existing, err := uc.favoriteRepo.FindByURL(ctx, canonicalURL)
139 if err != nil {
140 return nil, fmt.Errorf("failed to check existing favorite: %w", err)
141 }
142 if existing != nil {
143 return existing, nil
144 }
145 rawURL = strings.TrimSpace(rawURL)
146 if rawURL == "" || rawURL == canonicalURL {
147 return nil, nil
148 }
149 existing, err = uc.favoriteRepo.FindByURL(ctx, rawURL)
150 if err != nil {
151 return nil, fmt.Errorf("failed to check legacy favorite: %w", err)
152 }
153 return existing, nil
154}
155
156func normalizeFavoriteURL(raw string) (string, error) {
157 favoriteURL := domainurl.Normalize(strings.TrimSpace(raw))
158 if favoriteURL == "" {
159 return "", fmt.Errorf("favorite URL is required")
160 }
161 parsed, err := url.Parse(favoriteURL)
162 if err != nil || parsed.Scheme == "" {
163 return "", fmt.Errorf("favorite URL must be absolute")
164 }
165 switch strings.ToLower(parsed.Scheme) {
166 case "http", "https":
167 if parsed.Host == "" {
168 return "", fmt.Errorf("favorite URL host is required")
169 }
170 case "dumb":
171 if parsed.Host != "" {
172 return favoriteURL, nil
173 }
174 if parsed.Opaque != "" {
175 return "dumb://" + strings.TrimPrefix(parsed.Opaque, "//"), nil
176 }
177 return "", fmt.Errorf("favorite URL host is required")
178 default:
179 return "", fmt.Errorf("favorite URL must use http, https, or dumb scheme")
180 }
181 return favoriteURL, nil
182}
183
184// UpdateFavorite updates editable favorite metadata from the UI-facing application port.
185func (uc *ManageFavoritesUseCase) UpdateFavorite(ctx context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {
186 if input.ID <= 0 {
187 return nil, fmt.Errorf("favorite ID is required")
188 }
189 if input.ShortcutKey != nil && (*input.ShortcutKey < 1 || *input.ShortcutKey > 9) {
190 return nil, fmt.Errorf("shortcut key must be 1-9, got %d", *input.ShortcutKey)
191 }
192
193 fav, err := uc.favoriteRepo.FindByID(ctx, input.ID)
194 if err != nil {
195 return nil, fmt.Errorf("failed to find favorite: %w", err)
196 }
197 if fav == nil {
198 return nil, fmt.Errorf("favorite %d not found", input.ID)
199 }
200
201 title := strings.TrimSpace(input.Title)
202 if title == "" {
203 return nil, fmt.Errorf("favorite title is required")
204 }
205 fav.Title = title
206 fav.FaviconURL = strings.TrimSpace(input.FaviconURL)
207 fav.FolderID = input.FolderID
208 fav.ShortcutKey = input.ShortcutKey
209
210 if err := uc.Update(ctx, fav); err != nil {
211 return nil, err
212 }
213 return fav, nil
214}
215
216// DeleteFavorite deletes a favorite from the UI-facing application port.
217func (uc *ManageFavoritesUseCase) DeleteFavorite(ctx context.Context, id entity.FavoriteID) error {
218 return uc.Remove(ctx, id)
219}
220
221// Remove deletes a favorite by ID.
222func (uc *ManageFavoritesUseCase) Remove(ctx context.Context, id entity.FavoriteID) error {
223 log := logging.FromContext(ctx)
224 log.Debug().Int64("id", int64(id)).Msg("removing favorite")
225
226 if err := uc.favoriteRepo.Delete(ctx, id); err != nil {
227 return fmt.Errorf("failed to delete favorite: %w", err)
228 }
229
230 log.Info().Int64("id", int64(id)).Msg("favorite removed")
231 uc.invalidateCache()
232 return nil
233}
234
235// RemoveByURL deletes a favorite by its URL.
236func (uc *ManageFavoritesUseCase) RemoveByURL(ctx context.Context, favoriteURL string) error {
237 log := logging.FromContext(ctx)
238 log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("removing favorite by URL")
239
240 fav, err := uc.favoriteRepo.FindByURL(ctx, favoriteURL)
241 if err != nil {
242 return fmt.Errorf("failed to find favorite: %w", err)
243 }
244 if fav == nil {
245 log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("favorite not found")
246 return nil
247 }
248
249 return uc.Remove(ctx, fav.ID)
250}
251
252// Update modifies a favorite's metadata.
253func (uc *ManageFavoritesUseCase) Update(ctx context.Context, fav *entity.Favorite) error {
254 log := logging.FromContext(ctx)
255 log.Debug().Int64("id", int64(fav.ID)).Msg("updating favorite")
256
257 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
258 return fmt.Errorf("failed to update favorite: %w", err)
259 }
260
261 log.Info().Int64("id", int64(fav.ID)).Msg("favorite updated")
262 uc.invalidateCache()
263 return nil
264}
265
266// Move changes a favorite's folder.
267func (uc *ManageFavoritesUseCase) Move(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
268 log := logging.FromContext(ctx)
269 log.Debug().
270 Int64("favorite_id", int64(id)).
271 Msg("moving favorite to folder")
272
273 if err := uc.favoriteRepo.SetFolder(ctx, id, folderID); err != nil {
274 return fmt.Errorf("failed to move favorite: %w", err)
275 }
276
277 log.Info().Int64("id", int64(id)).Msg("favorite moved")
278 uc.invalidateCache()
279 return nil
280}
281
282// SetShortcut assigns or removes a keyboard shortcut (1-9).
283func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
284 log := logging.FromContext(ctx)
285
286 if key != nil && (*key < 1 || *key > 9) {
287 return fmt.Errorf("shortcut key must be 1-9, got %d", *key)
288 }
289
290 log.Debug().
291 Int64("id", int64(id)).
292 Msg("setting favorite shortcut")
293
294 if err := uc.favoriteRepo.SetShortcut(ctx, id, key); err != nil {
295 return fmt.Errorf("failed to set shortcut: %w", err)
296 }
297
298 uc.invalidateCache()
299 return nil
300}
301
302// GetByShortcut finds the favorite with the given shortcut key.
303func (uc *ManageFavoritesUseCase) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
304 log := logging.FromContext(ctx)
305 log.Debug().Int("key", key).Msg("getting favorite by shortcut")
306
307 if key < 1 || key > 9 {
308 return nil, fmt.Errorf("shortcut key must be 1-9, got %d", key)
309 }
310
311 return uc.favoriteRepo.GetByShortcut(ctx, key)
312}
313
314// GetByURL finds a favorite by its URL.
315func (uc *ManageFavoritesUseCase) GetByURL(ctx context.Context, favoriteURL string) (*entity.Favorite, error) {
316 log := logging.FromContext(ctx)
317 log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("getting favorite by URL")
318
319 return uc.favoriteRepo.FindByURL(ctx, favoriteURL)
320}
321
322// GetAll retrieves all favorites.
323func (uc *ManageFavoritesUseCase) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
324 log := logging.FromContext(ctx)
325 log.Debug().Msg("getting all favorites")
326
327 uc.cacheMu.Lock()
328 if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoritesCache != nil {
329 cached := uc.favoritesCache
330 uc.cacheMu.Unlock()
331 return cached, nil
332 }
333 uc.cacheMu.Unlock()
334
335 favs, err := uc.favoriteRepo.GetAll(ctx)
336 if err != nil {
337 return nil, fmt.Errorf("failed to get favorites: %w", err)
338 }
339
340 uc.cacheMu.Lock()
341 if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoritesCache != nil {
342 cached := uc.favoritesCache
343 uc.cacheMu.Unlock()
344 return cached, nil
345 }
346 uc.favoritesCache = favs
347 uc.favoriteURLsCache = buildFavoriteURLSet(favs)
348 uc.cacheTime = time.Now()
349 uc.cacheMu.Unlock()
350
351 log.Debug().Int("count", len(favs)).Msg("retrieved favorites")
352 return favs, nil
353}
354
355// FilterForOmnibox returns favorites filtered and ranked for omnibox display.
356func (uc *ManageFavoritesUseCase) FilterForOmnibox(ctx context.Context, query string) ([]*entity.Favorite, error) {
357 favs, err := uc.GetAll(ctx)
358 if err != nil {
359 return nil, err
360 }
361
362 queryLower := strings.ToLower(strings.TrimSpace(query))
363 if queryLower == "" {
364 return favs, nil
365 }
366
367 filtered := make([]*entity.Favorite, 0, len(favs))
368 for _, fav := range favs {
369 if fav == nil {
370 continue
371 }
372 titleLower := strings.ToLower(fav.Title)
373 urlLower := strings.ToLower(fav.URL)
374 if !strings.Contains(titleLower, queryLower) && !strings.Contains(urlLower, queryLower) {
375 continue
376 }
377 filtered = append(filtered, fav)
378 }
379
380 sort.SliceStable(filtered, func(i, j int) bool {
381 leftScore := omniboxFavoriteScore(filtered[i], queryLower)
382 rightScore := omniboxFavoriteScore(filtered[j], queryLower)
383 if leftScore != rightScore {
384 return leftScore > rightScore
385 }
386 if filtered[i].Position != filtered[j].Position {
387 return filtered[i].Position < filtered[j].Position
388 }
389 return filtered[i].URL < filtered[j].URL
390 })
391
392 return filtered, nil
393}
394
395// IsFavorite checks if a URL is favorited.
396func (uc *ManageFavoritesUseCase) IsFavorite(ctx context.Context, favoriteURL string) (bool, error) {
397 fav, err := uc.favoriteRepo.FindByURL(ctx, favoriteURL)
398 if err != nil {
399 return false, fmt.Errorf("failed to check favorite: %w", err)
400 }
401 return fav != nil, nil
402}
403
404func omniboxFavoriteScore(fav *entity.Favorite, queryLower string) int {
405 if fav == nil || queryLower == "" {
406 return 0
407 }
408
409 const (
410 favoriteExactMatchScore = 600
411 favoriteHostPrefixScore = 520
412 favoriteTitlePrefixScore = 440
413 favoriteTitleContainsScore = 320
414 favoriteURLContainsScore = 280
415 )
416
417 urlLower := strings.ToLower(fav.URL)
418 strippedURLLower := strings.ToLower(strings.TrimPrefix(domainurl.ExtractDomain(fav.URL), "www."))
419 if strippedURLLower == "" {
420 strippedURLLower = strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(fav.URL, "https://"), "http://"))
421 }
422 titleLower := strings.ToLower(fav.Title)
423
424 switch {
425 case titleLower == queryLower || urlLower == queryLower:
426 return favoriteExactMatchScore
427 case strings.HasPrefix(strippedURLLower, queryLower) || strings.HasPrefix(urlLower, queryLower):
428 return favoriteHostPrefixScore
429 case strings.HasPrefix(titleLower, queryLower):
430 return favoriteTitlePrefixScore
431 case strings.Contains(titleLower, queryLower):
432 return favoriteTitleContainsScore
433 case strings.Contains(urlLower, queryLower):
434 return favoriteURLContainsScore
435 default:
436 return 0
437 }
438}
439
440// ToggleResult indicates the result of a toggle operation.
441type ToggleResult struct {
442 Added bool // true if favorite was added, false if removed
443 URL string
444 Title string
445 Message string // User-friendly message
446}
447
448// Toggle adds or removes a URL from favorites based on current state.
449// If the URL is already a favorite, it removes it. Otherwise, it adds it.
450func (uc *ManageFavoritesUseCase) Toggle(ctx context.Context, rawURL, title string) (*ToggleResult, error) {
451 favoriteURL, err := normalizeFavoriteURL(rawURL)
452 if err != nil {
453 return nil, err
454 }
455 title = strings.TrimSpace(title)
456
457 log := logging.FromContext(ctx)
458 log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("toggling favorite")
459
460 // Check if already favorited, including legacy rows stored before URL canonicalization.
461 existing, err := uc.findFavoriteByCanonicalOrRawURL(ctx, favoriteURL, rawURL)
462 if err != nil {
463 return nil, err
464 }
465
466 if existing != nil {
467 // Remove favorite
468 if err := uc.favoriteRepo.Delete(ctx, existing.ID); err != nil {
469 return nil, fmt.Errorf("failed to remove favorite: %w", err)
470 }
471 log.Info().Str("url", logging.RedactURL(favoriteURL)).Int64("id", int64(existing.ID)).Msg("favorite removed via toggle")
472 uc.invalidateCache()
473 return &ToggleResult{
474 Added: false,
475 URL: favoriteURL,
476 Title: existing.Title,
477 Message: "Favorite removed",
478 }, nil
479 }
480
481 // Add favorite
482 fav := entity.NewFavorite(favoriteURL, title)
483 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
484 return nil, fmt.Errorf("failed to add favorite: %w", err)
485 }
486 log.Info().Str("url", logging.RedactURL(favoriteURL)).Int64("id", int64(fav.ID)).Msg("favorite added via toggle")
487 uc.invalidateCache()
488 return &ToggleResult{
489 Added: true,
490 URL: favoriteURL,
491 Title: title,
492 Message: "Favorite added",
493 }, nil
494}
495
496// GetAllURLs returns a set of all favorited URLs for efficient batch lookup.
497func (uc *ManageFavoritesUseCase) GetAllURLs(ctx context.Context) (map[string]struct{}, error) {
498 log := logging.FromContext(ctx)
499 log.Debug().Msg("getting all favorite URLs")
500
501 uc.cacheMu.Lock()
502 if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoriteURLsCache != nil {
503 cached := uc.favoriteURLsCache
504 uc.cacheMu.Unlock()
505 return cached, nil
506 }
507 uc.cacheMu.Unlock()
508
509 favs, err := uc.GetAll(ctx)
510 if err != nil {
511 return nil, fmt.Errorf("failed to get favorites: %w", err)
512 }
513
514 urls := buildFavoriteURLSet(favs)
515
516 log.Debug().Int("count", len(urls)).Msg("retrieved favorite URLs")
517 return urls, nil
518}
519
520func buildFavoriteURLSet(favs []*entity.Favorite) map[string]struct{} {
521 urls := make(map[string]struct{}, len(favs))
522 for _, fav := range favs {
523 if fav == nil || fav.URL == "" {
524 continue
525 }
526 urls[fav.URL] = struct{}{}
527 }
528 return urls
529}
530
531func (uc *ManageFavoritesUseCase) invalidateCache() {
532 uc.cacheMu.Lock()
533 defer uc.cacheMu.Unlock()
534 uc.favoritesCache = nil
535 uc.favoriteURLsCache = nil
536 uc.cacheTime = time.Time{}
537}
538
539// GetTree builds a complete hierarchical view of folders and favorites.
540func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
541 log := logging.FromContext(ctx)
542 log.Debug().Msg("building favorite tree")
543
544 tree := entity.NewFavoriteTree()
545
546 // Get all folders
547 folders, err := uc.folderRepo.GetAll(ctx)
548 if err != nil {
549 return nil, fmt.Errorf("failed to get folders: %w", err)
550 }
551
552 // Build folder map and organize by parent
553 for _, folder := range folders {
554 tree.FolderMap[folder.ID] = folder
555 if folder.ParentID == nil {
556 tree.RootFolders = append(tree.RootFolders, folder)
557 } else {
558 tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder)
559 }
560 }
561
562 // Get all favorites
563 favorites, err := uc.favoriteRepo.GetAll(ctx)
564 if err != nil {
565 return nil, fmt.Errorf("failed to get favorites: %w", err)
566 }
567
568 // Organize favorites by folder
569 for _, fav := range favorites {
570 if fav.FolderID == nil {
571 tree.RootFavorites = append(tree.RootFavorites, fav)
572 } else {
573 tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav)
574 }
575 }
576
577 log.Debug().
578 Int("folders", len(folders)).
579 Int("favorites", len(favorites)).
580 Msg("favorite tree built")
581
582 return tree, nil
583}
584
585// CreateFolder creates a new folder.
586func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name, icon string, parentID *entity.FolderID) (*entity.Folder, error) {
587 name = strings.TrimSpace(name)
588 icon = strings.TrimSpace(icon)
589 if name == "" {
590 return nil, fmt.Errorf("folder name is required")
591 }
592 log := logging.FromContext(ctx)
593 log.Debug().Str("name", name).Msg("creating folder")
594
595 folder := entity.NewFolder(name)
596 folder.Icon = icon
597 folder.ParentID = parentID
598
599 if err := uc.folderRepo.Save(ctx, folder); err != nil {
600 return nil, fmt.Errorf("failed to create folder: %w", err)
601 }
602
603 log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created")
604 return folder, nil
605}
606
607// DeleteFolder removes a folder. Favorites in the folder are moved to root.
608func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error {
609 log := logging.FromContext(ctx)
610 log.Debug().Int64("id", int64(id)).Msg("deleting folder")
611
612 // Move favorites from this folder to root (or parent)
613 folder, err := uc.folderRepo.FindByID(ctx, id)
614 if err != nil {
615 return fmt.Errorf("failed to find folder: %w", err)
616 }
617 if folder == nil {
618 log.Debug().Int64("id", int64(id)).Msg("folder not found")
619 return nil
620 }
621
622 // Get favorites in this folder
623 favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id)
624 if err != nil {
625 return fmt.Errorf("failed to get folder favorites: %w", err)
626 }
627
628 // Move favorites to parent folder (or root)
629 for _, fav := range favorites {
630 if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil {
631 log.Warn().
632 Int64("favorite_id", int64(fav.ID)).
633 Err(setErr).
634 Msg("failed to move favorite from deleted folder")
635 }
636 }
637 uc.invalidateCache()
638
639 // Get child folders
640 children, err := uc.folderRepo.GetChildren(ctx, &id)
641 if err != nil {
642 return fmt.Errorf("failed to get child folders: %w", err)
643 }
644
645 // Recursively delete child folders
646 for _, child := range children {
647 if err := uc.DeleteFolder(ctx, child.ID); err != nil {
648 log.Warn().
649 Int64("child_id", int64(child.ID)).
650 Err(err).
651 Msg("failed to delete child folder")
652 }
653 }
654
655 // Delete the folder itself
656 if err := uc.folderRepo.Delete(ctx, id); err != nil {
657 return fmt.Errorf("failed to delete folder: %w", err)
658 }
659
660 log.Info().Int64("id", int64(id)).Msg("folder deleted")
661 return nil
662}
663
664// GetAllFolders retrieves all folders.
665func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) {
666 log := logging.FromContext(ctx)
667 log.Debug().Msg("getting all folders")
668
669 return uc.folderRepo.GetAll(ctx)
670}
671
672// AddTag creates a new tag.
673func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
674 log := logging.FromContext(ctx)
675 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")
676
677 // Check if tag already exists
678 existing, err := uc.tagRepo.FindByName(ctx, name)
679 if err != nil {
680 return nil, fmt.Errorf("failed to check existing tag: %w", err)
681 }
682 if existing != nil {
683 log.Debug().Str("name", name).Msg("tag already exists")
684 return existing, nil
685 }
686
687 tag := entity.NewTag(name)
688 if color != "" {
689 tag.Color = color
690 }
691
692 if err := uc.tagRepo.Save(ctx, tag); err != nil {
693 return nil, fmt.Errorf("failed to create tag: %w", err)
694 }
695
696 log.Info().Str("name", name).Int64("id", int64(tag.ID)).Msg("tag created")
697 return tag, nil
698}
699
700// DeleteTag removes a tag.
701func (uc *ManageFavoritesUseCase) DeleteTag(ctx context.Context, id entity.TagID) error {
702 log := logging.FromContext(ctx)
703 log.Debug().Int64("id", int64(id)).Msg("deleting tag")
704
705 if err := uc.tagRepo.Delete(ctx, id); err != nil {
706 return fmt.Errorf("failed to delete tag: %w", err)
707 }
708
709 log.Info().Int64("id", int64(id)).Msg("tag deleted")
710 uc.invalidateCache()
711 return nil
712}
713
714// GetAllTags retrieves all tags.
715func (uc *ManageFavoritesUseCase) GetAllTags(ctx context.Context) ([]*entity.Tag, error) {
716 log := logging.FromContext(ctx)
717 log.Debug().Msg("getting all tags")
718
719 return uc.tagRepo.GetAll(ctx)
720}
721
722// TagFavorite assigns a tag to a favorite.
723func (uc *ManageFavoritesUseCase) TagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
724 log := logging.FromContext(ctx)
725 log.Debug().
726 Int64("favorite_id", int64(favID)).
727 Int64("tag_id", int64(tagID)).
728 Msg("tagging favorite")
729
730 if err := uc.tagRepo.AssignToFavorite(ctx, tagID, favID); err != nil {
731 return fmt.Errorf("failed to tag favorite: %w", err)
732 }
733 uc.invalidateCache()
734
735 return nil
736}
737
738// UntagFavorite removes a tag from a favorite.
739func (uc *ManageFavoritesUseCase) UntagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
740 log := logging.FromContext(ctx)
741 log.Debug().
742 Int64("favorite_id", int64(favID)).
743 Int64("tag_id", int64(tagID)).
744 Msg("untagging favorite")
745
746 if err := uc.tagRepo.RemoveFromFavorite(ctx, tagID, favID); err != nil {
747 return fmt.Errorf("failed to untag favorite: %w", err)
748 }
749 uc.invalidateCache()
750
751 return nil
752}
753
754// GetTagsForFavorite retrieves all tags for a favorite.
755func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
756 log := logging.FromContext(ctx)
757 log.Debug().Int64("favorite_id", int64(favID)).Msg("getting tags for favorite")
758
759 return uc.tagRepo.GetForFavorite(ctx, favID)
760}
761
762// UpdateFolder updates a folder's name and icon.
763func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
764 log := logging.FromContext(ctx)
765 log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
766
767 folder, err := uc.folderRepo.FindByID(ctx, id)
768 if err != nil {
769 return fmt.Errorf("failed to find folder: %w", err)
770 }
771 if folder == nil {
772 return fmt.Errorf("folder %d not found", id)
773 }
774
775 if name != "" {
776 folder.Name = name
777 }
778 folder.Icon = icon
779
780 if err := uc.folderRepo.Save(ctx, folder); err != nil {
781 return fmt.Errorf("failed to update folder: %w", err)
782 }
783
784 log.Info().Int64("id", int64(id)).Msg("folder updated")
785 return nil
786}
787
788// UpdateTag updates a tag's name and color.
789func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
790 log := logging.FromContext(ctx)
791 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
792
793 tag, err := uc.tagRepo.FindByID(ctx, id)
794 if err != nil {
795 return fmt.Errorf("failed to find tag: %w", err)
796 }
797 if tag == nil {
798 return fmt.Errorf("tag %d not found", id)
799 }
800
801 if name != "" {
802 tag.Name = name
803 }
804 if color != "" {
805 tag.Color = color
806 }
807
808 if err := uc.tagRepo.Save(ctx, tag); err != nil {
809 return fmt.Errorf("failed to update tag: %w", err)
810 }
811
812 log.Info().Int64("id", int64(id)).Msg("tag updated")
813 uc.invalidateCache()
814 return nil
815}