import { StudyItem } from '../models/StudyItem.js'; import { ReviewLog } from '../models/ReviewLog.js'; import { getDaysDiff } from '../utils/dateUtils.js'; export const getUserStats = async (user) => { const userId = user._id; const srsCounts = await StudyItem.aggregate([ { $match: { userId: userId } }, { $group: { _id: "$srsLevel", count: { $sum: 1 } } } ]); const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; srsCounts.forEach(g => { if (dist[g._id] !== undefined) dist[g._id] = g.count; }); const now = new Date(); const next24h = new Date(now.getTime() + 24 * 60 * 60 * 1000); const upcoming = await StudyItem.find({ userId: userId, srsLevel: { $lt: 6, $gt: 0 }, nextReview: { $lte: next24h } }).select('nextReview'); const forecast = new Array(24).fill(0); upcoming.forEach(item => { const diff = Math.floor((new Date(item.nextReview) - now) / 3600000); if (diff < 0) forecast[0]++; else if (diff < 24) forecast[diff]++; }); const queueItems = await StudyItem.find({ userId: userId, srsLevel: { $lt: 6, $gt: 0 }, nextReview: { $lte: now } }).select('srsLevel'); const queueCount = queueItems.length; const lessonCount = await StudyItem.countDocuments({ userId: userId, srsLevel: 0 }); let hasLowerLevels = false; let lowerLevelCount = 0; if (queueCount > 0) { const levels = queueItems.map(i => i.srsLevel); const minSrs = Math.min(...levels); const maxSrs = Math.max(...levels); if (minSrs < maxSrs) { hasLowerLevels = true; lowerLevelCount = queueItems.filter(i => i.srsLevel === minSrs).length; } } const oneYearAgo = new Date(); oneYearAgo.setDate(oneYearAgo.getDate() - 365); const dateStr = oneYearAgo.toISOString().split('T')[0]; const logs = await ReviewLog.find({ userId: userId, date: { $gte: dateStr } }); const heatmap = {}; logs.forEach(l => { heatmap[l.date] = l.count; }); const todayStr = new Date().toISOString().split('T')[0]; const lastStudyStr = user.stats.lastStudyDate || null; let displayStreak = user.stats.currentStreak || 0; if (lastStudyStr) { const diff = getDaysDiff(lastStudyStr, todayStr); if (diff > 1) displayStreak = 0; } const lastFreeze = user.stats.lastFreezeDate ? new Date(user.stats.lastFreezeDate) : null; let daysSinceFreeze = 999; if (lastFreeze) { daysSinceFreeze = getDaysDiff(lastFreeze.toISOString().split('T')[0], todayStr); } const shieldReady = daysSinceFreeze >= 7; const shieldCooldown = shieldReady ? 0 : (7 - daysSinceFreeze); const history7Days = []; for (let i = 6; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const dStr = d.toISOString().split('T')[0]; const count = heatmap[dStr] || 0; history7Days.push({ date: dStr, active: count > 0 }); } const allGhosts = await StudyItem.find({ userId: userId, 'stats.total': { $gte: 2 } }) .select('char meaning stats srsLevel'); const sortedGhosts = allGhosts.map(item => ({ ...item.toObject(), accuracy: Math.round((item.stats.correct / item.stats.total) * 100) })).filter(i => i.accuracy < 85) .sort((a, b) => a.accuracy - b.accuracy) .slice(0, 10); return { distribution: dist, forecast: forecast, queueLength: queueCount, lessonCount: lessonCount, hasLowerLevels, lowerLevelCount, heatmap: heatmap, ghosts: sortedGhosts, streak: { current: displayStreak, best: user.stats.maxStreak || 0, shield: { ready: shieldReady, cooldown: Math.max(0, shieldCooldown) }, history: history7Days }, accuracy: { total: user.stats.totalReviews || 0, correct: user.stats.correctReviews || 0 } }; };