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// GetTree builds a complete hierarchical view of folders and favorites.
200func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
201 log := logging.FromContext(ctx)
202 log.Debug().Msg("building favorite tree")
203
204 tree := entity.NewFavoriteTree()
205
206 // Get all folders
207 folders, err := uc.folderRepo.GetAll(ctx)
208 if err != nil {
209 return nil, fmt.Errorf("failed to get folders: %w", err)
210 }
211
212 // Build folder map and organize by parent
213 for _, folder := range folders {
214 tree.FolderMap[folder.ID] = folder
215 if folder.ParentID == nil {
216 tree.RootFolders = append(tree.RootFolders, folder)
217 } else {
218 tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder)
219 }
220 }
221
222 // Get all favorites
223 favorites, err := uc.favoriteRepo.GetAll(ctx)
224 if err != nil {
225 return nil, fmt.Errorf("failed to get favorites: %w", err)
226 }
227
228 // Organize favorites by folder
229 for _, fav := range favorites {
230 if fav.FolderID == nil {
231 tree.RootFavorites = append(tree.RootFavorites, fav)
232 } else {
233 tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav)
234 }
235 }
236
237 log.Debug().
238 Int("folders", len(folders)).
239 Int("favorites", len(favorites)).
240 Msg("favorite tree built")
241
242 return tree, nil
243}
244
245// CreateFolder creates a new folder.
246func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name string, parentID *entity.FolderID) (*entity.Folder, error) {
247 log := logging.FromContext(ctx)
248 log.Debug().Str("name", name).Msg("creating folder")
249
250 folder := entity.NewFolder(name)
251 folder.ParentID = parentID
252
253 if err := uc.folderRepo.Save(ctx, folder); err != nil {
254 return nil, fmt.Errorf("failed to create folder: %w", err)
255 }
256
257 log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created")
258 return folder, nil
259}
260
261// DeleteFolder removes a folder. Favorites in the folder are moved to root.
262func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error {
263 log := logging.FromContext(ctx)
264 log.Debug().Int64("id", int64(id)).Msg("deleting folder")
265
266 // Move favorites from this folder to root (or parent)
267 folder, err := uc.folderRepo.FindByID(ctx, id)
268 if err != nil {
269 return fmt.Errorf("failed to find folder: %w", err)
270 }
271 if folder == nil {
272 log.Debug().Int64("id", int64(id)).Msg("folder not found")
273 return nil
274 }
275
276 // Get favorites in this folder
277 favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id)
278 if err != nil {
279 return fmt.Errorf("failed to get folder favorites: %w", err)
280 }
281
282 // Move favorites to parent folder (or root)
283 for _, fav := range favorites {
284 if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil {
285 log.Warn().
286 Int64("favorite_id", int64(fav.ID)).
287 Err(setErr).
288 Msg("failed to move favorite from deleted folder")
289 }
290 }
291
292 // Get child folders
293 children, err := uc.folderRepo.GetChildren(ctx, &id)
294 if err != nil {
295 return fmt.Errorf("failed to get child folders: %w", err)
296 }
297
298 // Recursively delete child folders
299 for _, child := range children {
300 if err := uc.DeleteFolder(ctx, child.ID); err != nil {
301 log.Warn().
302 Int64("child_id", int64(child.ID)).
303 Err(err).
304 Msg("failed to delete child folder")
305 }
306 }
307
308 // Delete the folder itself
309 if err := uc.folderRepo.Delete(ctx, id); err != nil {
310 return fmt.Errorf("failed to delete folder: %w", err)
311 }
312
313 log.Info().Int64("id", int64(id)).Msg("folder deleted")
314 return nil
315}
316
317// GetAllFolders retrieves all folders.
318func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) {
319 log := logging.FromContext(ctx)
320 log.Debug().Msg("getting all folders")
321
322 return uc.folderRepo.GetAll(ctx)
323}
324
325// AddTag creates a new tag.
326func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
327 log := logging.FromContext(ctx)
328 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")
329
330 // Check if tag already exists
331 existing, err := uc.tagRepo.FindByName(ctx, name)
332 if err != nil {
333 return nil, fmt.Errorf("failed to check existing tag: %w", err)
334 }
335 if existing != nil {
336 log.Debug().Str("name", name).Msg("tag already exists")
337 return existing, nil
338 }
339
340 tag := entity.NewTag(name)
341 if color != "" {
342 tag.Color = color
343 }
344
345 if err := uc.tagRepo.Save(ctx, tag); err != nil {
346 return nil, fmt.Errorf("failed to create tag: %w", err)
347 }
348
349 log.Info().Str("name", name).Int64("id", int64(tag.ID)).Msg("tag created")
350 return tag, nil
351}
352
353// DeleteTag removes a tag.
354func (uc *ManageFavoritesUseCase) DeleteTag(ctx context.Context, id entity.TagID) error {
355 log := logging.FromContext(ctx)
356 log.Debug().Int64("id", int64(id)).Msg("deleting tag")
357
358 if err := uc.tagRepo.Delete(ctx, id); err != nil {
359 return fmt.Errorf("failed to delete tag: %w", err)
360 }
361
362 log.Info().Int64("id", int64(id)).Msg("tag deleted")
363 return nil
364}
365
366// GetAllTags retrieves all tags.
367func (uc *ManageFavoritesUseCase) GetAllTags(ctx context.Context) ([]*entity.Tag, error) {
368 log := logging.FromContext(ctx)
369 log.Debug().Msg("getting all tags")
370
371 return uc.tagRepo.GetAll(ctx)
372}
373
374// TagFavorite assigns a tag to a favorite.
375func (uc *ManageFavoritesUseCase) TagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
376 log := logging.FromContext(ctx)
377 log.Debug().
378 Int64("favorite_id", int64(favID)).
379 Int64("tag_id", int64(tagID)).
380 Msg("tagging favorite")
381
382 if err := uc.tagRepo.AssignToFavorite(ctx, tagID, favID); err != nil {
383 return fmt.Errorf("failed to tag favorite: %w", err)
384 }
385
386 return nil
387}
388
389// UntagFavorite removes a tag from a favorite.
390func (uc *ManageFavoritesUseCase) UntagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error {
391 log := logging.FromContext(ctx)
392 log.Debug().
393 Int64("favorite_id", int64(favID)).
394 Int64("tag_id", int64(tagID)).
395 Msg("untagging favorite")
396
397 if err := uc.tagRepo.RemoveFromFavorite(ctx, tagID, favID); err != nil {
398 return fmt.Errorf("failed to untag favorite: %w", err)
399 }
400
401 return nil
402}
403
404// GetTagsForFavorite retrieves all tags for a favorite.
405func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
406 log := logging.FromContext(ctx)
407 log.Debug().Int64("favorite_id", int64(favID)).Msg("getting tags for favorite")
408
409 return uc.tagRepo.GetForFavorite(ctx, favID)
410}
411
412// UpdateFolder updates a folder's name and icon.
413func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
414 log := logging.FromContext(ctx)
415 log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
416
417 folder, err := uc.folderRepo.FindByID(ctx, id)
418 if err != nil {
419 return fmt.Errorf("failed to find folder: %w", err)
420 }
421 if folder == nil {
422 return fmt.Errorf("folder %d not found", id)
423 }
424
425 if name != "" {
426 folder.Name = name
427 }
428 folder.Icon = icon
429
430 if err := uc.folderRepo.Save(ctx, folder); err != nil {
431 return fmt.Errorf("failed to update folder: %w", err)
432 }
433
434 log.Info().Int64("id", int64(id)).Msg("folder updated")
435 return nil
436}
437
438// UpdateTag updates a tag's name and color.
439func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
440 log := logging.FromContext(ctx)
441 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
442
443 tag, err := uc.tagRepo.FindByID(ctx, id)
444 if err != nil {
445 return fmt.Errorf("failed to find tag: %w", err)
446 }
447 if tag == nil {
448 return fmt.Errorf("tag %d not found", id)
449 }
450
451 if name != "" {
452 tag.Name = name
453 }
454 if color != "" {
455 tag.Color = color
456 }
457
458 if err := uc.tagRepo.Save(ctx, tag); err != nil {
459 return fmt.Errorf("failed to update tag: %w", err)
460 }
461
462 log.Info().Int64("id", int64(id)).Msg("tag updated")
463 return nil
464}