1package usecase
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/domain/repository"
11 "github.com/bnema/dumber/internal/logging"
12)
13
14// Compile-time check: SearchHistoryUseCase must satisfy port.HomepageHistory.
15var _ port.HomepageHistory = (*SearchHistoryUseCase)(nil)
16
17// SearchHistoryUseCase handles history search and retrieval operations.
18type SearchHistoryUseCase struct {
19 historyRepo repository.HistoryRepository
20}
21
22// NewSearchHistoryUseCase creates a new history search use case.
23func NewSearchHistoryUseCase(historyRepo repository.HistoryRepository) *SearchHistoryUseCase {
24 return &SearchHistoryUseCase{
25 historyRepo: historyRepo,
26 }
27}
28
29// SearchInput is an alias for port.HistorySearchInput.
30type SearchInput = port.HistorySearchInput
31
32// SearchOutput is an alias for port.HistorySearchOutput.
33type SearchOutput = port.HistorySearchOutput
34
35// Search performs a full-text search on history entries using SQLite FTS5.
36// Returns only entries that actually match the query terms.
37func (uc *SearchHistoryUseCase) Search(ctx context.Context, input SearchInput) (*SearchOutput, error) {
38 log := logging.FromContext(ctx)
39
40 if input.Query == "" {
41 return &SearchOutput{Matches: []entity.HistoryMatch{}}, nil
42 }
43
44 limit := input.Limit
45 if limit <= 0 {
46 limit = 20 // Default limit
47 }
48
49 // Use repository's FTS5 search
50 matches, err := uc.historyRepo.Search(ctx, input.Query, limit)
51 if err != nil {
52 return nil, fmt.Errorf("failed to search history: %w", err)
53 }
54
55 log.Debug().
56 Str("query", input.Query).
57 Int("matches", len(matches)).
58 Msg("FTS5 search completed")
59
60 return &SearchOutput{Matches: matches}, nil
61}
62
63// GetRecent retrieves recent history entries with pagination.
64func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
65 if limit <= 0 {
66 limit = 50 // Default limit
67 }
68
69 entries, err := uc.historyRepo.GetRecent(ctx, limit, offset)
70 if err != nil {
71 return nil, fmt.Errorf("failed to get recent history: %w", err)
72 }
73
74 return entries, nil
75}
76
77// GetRecentSince retrieves history entries visited within the last N days.
78// If days is 0, returns all history entries.
79// If days is negative, defaults to 30 days.
80func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
81 if days < 0 {
82 days = 30 // Default to 30 days for negative values
83 }
84
85 var entries []*entity.HistoryEntry
86 var err error
87
88 switch days {
89 case 0:
90 entries, err = uc.historyRepo.GetAllRecentHistory(ctx)
91 default:
92 entries, err = uc.historyRepo.GetRecentSince(ctx, days)
93 }
94
95 if err != nil {
96 return nil, fmt.Errorf("failed to get recent history: %w", err)
97 }
98
99 return entries, nil
100}
101
102// GetMostVisited retrieves history entries sorted by visit count within the last N days.
103// If days is 0, returns all history entries sorted by visit count.
104// If days is negative, defaults to 30 days.
105func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
106 if days < 0 {
107 days = 30 // Default to 30 days for negative values
108 }
109
110 var entries []*entity.HistoryEntry
111 var err error
112
113 switch days {
114 case 0:
115 entries, err = uc.historyRepo.GetAllMostVisited(ctx)
116 default:
117 entries, err = uc.historyRepo.GetMostVisited(ctx, days)
118 }
119
120 if err != nil {
121 return nil, fmt.Errorf("failed to get most visited history: %w", err)
122 }
123
124 return entries, nil
125}
126
127// FindByURL retrieves a history entry by its URL.
128func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
129 log := logging.FromContext(ctx)
130 log.Debug().Str("url", url).Msg("finding history entry by URL")
131
132 entry, err := uc.historyRepo.FindByURL(ctx, url)
133 if err != nil {
134 return nil, fmt.Errorf("failed to find history entry: %w", err)
135 }
136
137 if entry == nil {
138 log.Debug().Str("url", url).Msg("history entry not found")
139 }
140
141 return entry, nil
142}
143
144// ClearOlderThan deletes history entries older than the specified time.
145func (uc *SearchHistoryUseCase) ClearOlderThan(ctx context.Context, before time.Time) error {
146 log := logging.FromContext(ctx)
147 log.Debug().Time("before", before).Msg("clearing old history")
148
149 if err := uc.historyRepo.DeleteOlderThan(ctx, before); err != nil {
150 return fmt.Errorf("failed to clear history: %w", err)
151 }
152
153 log.Info().Time("before", before).Msg("old history cleared")
154 return nil
155}
156
157// ClearAll deletes all history entries.
158func (uc *SearchHistoryUseCase) ClearAll(ctx context.Context) error {
159 log := logging.FromContext(ctx)
160 log.Debug().Msg("clearing all history")
161
162 if err := uc.historyRepo.DeleteAll(ctx); err != nil {
163 return fmt.Errorf("failed to clear all history: %w", err)
164 }
165
166 log.Info().Msg("all history cleared")
167 return nil
168}
169
170// Delete removes a single history entry by ID.
171func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
172 log := logging.FromContext(ctx)
173 log.Debug().Int64("id", id).Msg("deleting history entry")
174
175 if err := uc.historyRepo.Delete(ctx, id); err != nil {
176 return fmt.Errorf("failed to delete history entry: %w", err)
177 }
178
179 log.Debug().Int64("id", id).Msg("history entry deleted")
180 return nil
181}
182
183// DeleteByDomain removes all history entries for a domain.
184func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
185 log := logging.FromContext(ctx)
186 log.Debug().Str("domain", domain).Msg("deleting history by domain")
187
188 if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
189 return fmt.Errorf("failed to delete history by domain: %w", err)
190 }
191
192 log.Info().Str("domain", domain).Msg("history deleted for domain")
193 return nil
194}
195
196// GetDomainStats retrieves per-domain visit statistics.
197func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
198 log := logging.FromContext(ctx)
199 log.Debug().Int("limit", limit).Msg("getting domain stats")
200
201 if limit <= 0 {
202 limit = 20
203 }
204
205 return uc.historyRepo.GetDomainStats(ctx, limit)
206}
207
208// GetAnalytics retrieves aggregated history analytics for the homepage.
209func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
210 log := logging.FromContext(ctx)
211 log.Debug().Msg("getting history analytics")
212
213 // Get basic stats
214 stats, err := uc.historyRepo.GetStats(ctx)
215 if err != nil {
216 return nil, fmt.Errorf("failed to get history stats: %w", err)
217 }
218
219 // Get top domains
220 domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
221 if err != nil {
222 return nil, fmt.Errorf("failed to get domain stats: %w", err)
223 }
224
225 // Get daily visits for last 30 days
226 dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
227 if err != nil {
228 return nil, fmt.Errorf("failed to get daily visits: %w", err)
229 }
230
231 // Get hourly distribution
232 hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
233 if err != nil {
234 return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
235 }
236
237 return &entity.HistoryAnalytics{
238 TotalEntries: stats.TotalEntries,
239 TotalVisits: stats.TotalVisits,
240 UniqueDays: stats.UniqueDays,
241 TopDomains: domains,
242 DailyVisits: dailyVisits,
243 HourlyDistribution: hourlyDist,
244 }, nil
245}