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