import { db } from "@/lib/prisma"; type RecommendedCourse = { courseId: string; slug: string; title: string; level: string; reason: string; priority: number; }; type RecommendationPrismaClient = { miniGameAttempt: { findMany: (args: object) => Promise<{ miniGameId: string; scorePercent: number }[]>; }; studyRecommendation: { updateMany: (args: object) => Promise; createMany: (args: object) => Promise; findMany: (args: object) => Promise< { courseId: string; reason: string; priority: number; course: { title: unknown; slug: string; level: string }; }[] >; }; }; function getText(value: unknown): string { if (!value) return ""; if (typeof value === "string") return value; if (typeof value === "object") { const record = value as Record; if (typeof record.en === "string") return record.en; if (typeof record.es === "string") return record.es; } return ""; } function targetLevelByGrade(grade: number): "BEGINNER" | "INTERMEDIATE" | "ADVANCED" { if (grade < 60) return "BEGINNER"; if (grade < 80) return "INTERMEDIATE"; return "ADVANCED"; } export async function getMiniGameGrade(userId: string): Promise { let attempts: { miniGameId: string; scorePercent: number }[] = []; const prismaAny = db as unknown as RecommendationPrismaClient; try { attempts = await prismaAny.miniGameAttempt.findMany({ where: { userId }, orderBy: { completedAt: "desc" }, select: { miniGameId: true, scorePercent: true }, }); } catch { return 0; } const latestByGame = new Map(); for (const attempt of attempts) { if (!latestByGame.has(attempt.miniGameId)) { latestByGame.set(attempt.miniGameId, attempt.scorePercent); } } const latest = [...latestByGame.values()]; if (latest.length === 0) return 0; return Math.round(latest.reduce((acc, value) => acc + value, 0) / latest.length); } export async function refreshStudyRecommendations(userId: string) { const grade = await getMiniGameGrade(userId); const targetLevel = targetLevelByGrade(grade); const [courses, enrollments] = await Promise.all([ db.course.findMany({ where: { status: "PUBLISHED", }, include: { modules: { include: { lessons: { select: { id: true }, }, }, }, }, }), db.enrollment.findMany({ where: { userId }, select: { courseId: true }, }), ]); const enrolledSet = new Set(enrollments.map((enrollment) => enrollment.courseId)); const completedProgress = await db.userProgress.findMany({ where: { userId, isCompleted: true, lesson: { module: { courseId: { in: courses.map((course) => course.id), }, }, }, }, select: { lesson: { select: { module: { select: { courseId: true }, }, }, }, }, }); const completedByCourse = new Map(); for (const item of completedProgress) { const courseId = item.lesson.module.courseId; completedByCourse.set(courseId, (completedByCourse.get(courseId) ?? 0) + 1); } const recommendations: RecommendedCourse[] = []; for (const course of courses) { const totalLessons = course.modules.reduce((acc, module) => acc + module.lessons.length, 0); const completedLessons = completedByCourse.get(course.id) ?? 0; const isCompleted = totalLessons > 0 && completedLessons >= totalLessons; if (isCompleted) continue; const isEnrolled = enrolledSet.has(course.id); const levelMatch = course.level === targetLevel; let priority = 20; let reason = `Aligned with your current level focus (${targetLevel.toLowerCase()}).`; if (isEnrolled) { priority = 5; reason = "You already started this course and can keep progressing."; } else if (!levelMatch) { priority = 40; reason = "Useful as a secondary recommendation outside your current level target."; } recommendations.push({ courseId: course.id, slug: course.slug, title: getText(course.title) || "Untitled course", level: course.level, reason, priority, }); } const sorted = recommendations.sort((a, b) => a.priority - b.priority).slice(0, 5); const prismaAny = db as unknown as RecommendationPrismaClient; try { await prismaAny.studyRecommendation.updateMany({ where: { userId, isActive: true }, data: { isActive: false }, }); if (sorted.length > 0) { await prismaAny.studyRecommendation.createMany({ data: sorted.map((item) => ({ userId, courseId: item.courseId, reason: item.reason, priority: item.priority, })), }); } } catch { return sorted; } return sorted; } export async function getActiveRecommendations(userId: string) { let existing: | { courseId: string; reason: string; priority: number; course: { title: unknown; slug: string; level: string }; }[] | null = null; try { existing = await (db as unknown as RecommendationPrismaClient).studyRecommendation.findMany({ where: { userId, isActive: true }, include: { course: { select: { title: true, slug: true, level: true, }, }, }, orderBy: { priority: "asc" }, take: 5, }); } catch { return refreshStudyRecommendations(userId); } if (!existing || existing.length === 0) { return refreshStudyRecommendations(userId); } return existing.map((item) => ({ courseId: item.courseId, slug: item.course.slug, title: getText(item.course.title) || "Untitled course", level: item.course.level, reason: item.reason, priority: item.priority, })); }