// Package stats implements the read-only /api/v1/stats aggregation API. It
// bundles six queries into one round-trip and fills deterministic time windows
// (daily, heatmap, hour-of-day) so missing slots appear as zeros on the wire.
package stats
import "time"
// DailyWindow returns days+1 unix seconds (at UTC midnight), oldest first,
// ending today. Example: days=7 returns 8 entries — today plus the 7 prior
// days. Each pair is exactly 86400 seconds apart.
//
// The boundary rule: time.Unix(now,0).UTC().Truncate(24h) gives today's
// midnight in UTC.
func DailyWindow(now int64, days int) []int64 {
today := time.Unix(now, 0).UTC().Truncate(24 * time.Hour).Unix()
out := make([]int64, days+1)
for i := 0; i <= days; i++ {
// oldest first: index 0 is `days` days before today
out[i] = today - int64(days-i)*86400
}
return out
}
// FillDaily left-joins rows onto the window. Each window slot appears exactly
// once in the output (oldest first). Missing slots get PerTool: map[string]int64{}
// (empty map, not nil — so JSON encodes as {}). Rows whose DateUnix does not
// match any window slot are silently dropped (they are outside the range).
func FillDaily(window []int64, rows []DailyBucket) []DailyBucket {
// Build a lookup map from the rows.
rowByDate := make(map[int64]DailyBucket, len(rows))
for _, r := range rows {
rowByDate[r.DateUnix] = r
}
out := make([]DailyBucket, len(window))
for i, ts := range window {
if row, ok := rowByDate[ts]; ok {
out[i] = row
} else {
out[i] = DailyBucket{DateUnix: ts, PerTool: map[string]int64{}}
}
}
return out
}
// HeatmapWindow returns 84 unix seconds (at UTC midnight), oldest first,
// ending today — 12 weeks × 7 days. The length is fixed regardless of the
// request's range.
func HeatmapWindow(now int64) []int64 {
const cells = 84 // 12 weeks × 7 days
today := time.Unix(now, 0).UTC().Truncate(24 * time.Hour).Unix()
out := make([]int64, cells)
for i := 0; i < cells; i++ {
out[i] = today - int64(cells-1-i)*86400
}
return out
}
// HourWindow returns 24 zero-initialised HourBucket values, one per hour
// (Hour 0..23), in ascending order.
func HourWindow() []HourBucket {
out := make([]HourBucket, 24)
for i := range out {
out[i] = HourBucket{Hour: i, Count: 0}
}
return out
}