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// FindByURL retrieves a history entry by its URL.
79func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
80 log := logging.FromContext(ctx)
81 log.Debug().Str("url", url).Msg("finding history entry by URL")
82
83 entry, err := uc.historyRepo.FindByURL(ctx, url)
84 if err != nil {
85 return nil, fmt.Errorf("failed to find history entry: %w", err)
86 }
87
88 if entry == nil {
89 log.Debug().Str("url", url).Msg("history entry not found")
90 }
91
92 return entry, nil
93}
94
95// ClearOlderThan deletes history entries older than the specified time.
96func (uc *SearchHistoryUseCase) ClearOlderThan(ctx context.Context, before time.Time) error {
97 log := logging.FromContext(ctx)
98 log.Debug().Time("before", before).Msg("clearing old history")
99
100 if err := uc.historyRepo.DeleteOlderThan(ctx, before); err != nil {
101 return fmt.Errorf("failed to clear history: %w", err)
102 }
103
104 log.Info().Time("before", before).Msg("old history cleared")
105 return nil
106}
107
108// ClearAll deletes all history entries.
109func (uc *SearchHistoryUseCase) ClearAll(ctx context.Context) error {
110 log := logging.FromContext(ctx)
111 log.Debug().Msg("clearing all history")
112
113 if err := uc.historyRepo.DeleteAll(ctx); err != nil {
114 return fmt.Errorf("failed to clear all history: %w", err)
115 }
116
117 log.Info().Msg("all history cleared")
118 return nil
119}
120
121// Delete removes a single history entry by ID.
122func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error {
123 log := logging.FromContext(ctx)
124 log.Debug().Int64("id", id).Msg("deleting history entry")
125
126 if err := uc.historyRepo.Delete(ctx, id); err != nil {
127 return fmt.Errorf("failed to delete history entry: %w", err)
128 }
129
130 log.Debug().Int64("id", id).Msg("history entry deleted")
131 return nil
132}
133
134// DeleteByDomain removes all history entries for a domain.
135func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error {
136 log := logging.FromContext(ctx)
137 log.Debug().Str("domain", domain).Msg("deleting history by domain")
138
139 if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil {
140 return fmt.Errorf("failed to delete history by domain: %w", err)
141 }
142
143 log.Info().Str("domain", domain).Msg("history deleted for domain")
144 return nil
145}
146
147// GetDomainStats retrieves per-domain visit statistics.
148func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
149 log := logging.FromContext(ctx)
150 log.Debug().Int("limit", limit).Msg("getting domain stats")
151
152 if limit <= 0 {
153 limit = 20
154 }
155
156 return uc.historyRepo.GetDomainStats(ctx, limit)
157}
158
159// GetAnalytics retrieves aggregated history analytics for the homepage.
160func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) {
161 log := logging.FromContext(ctx)
162 log.Debug().Msg("getting history analytics")
163
164 // Get basic stats
165 stats, err := uc.historyRepo.GetStats(ctx)
166 if err != nil {
167 return nil, fmt.Errorf("failed to get history stats: %w", err)
168 }
169
170 // Get top domains
171 domains, err := uc.historyRepo.GetDomainStats(ctx, 10)
172 if err != nil {
173 return nil, fmt.Errorf("failed to get domain stats: %w", err)
174 }
175
176 // Get daily visits for last 30 days
177 dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days")
178 if err != nil {
179 return nil, fmt.Errorf("failed to get daily visits: %w", err)
180 }
181
182 // Get hourly distribution
183 hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx)
184 if err != nil {
185 return nil, fmt.Errorf("failed to get hourly distribution: %w", err)
186 }
187
188 return &entity.HistoryAnalytics{
189 TotalEntries: stats.TotalEntries,
190 TotalVisits: stats.TotalVisits,
191 UniqueDays: stats.UniqueDays,
192 TopDomains: domains,
193 DailyVisits: dailyVisits,
194 HourlyDistribution: hourlyDist,
195 }, nil
196}