const { getRedisClient } = require("./client") const { getOrCacheDeal, getDealIdByCommentId, ensureMinDealTtl } = require("./dealCache.service") const DEFAULT_TTL_SECONDS = 15 * 60 const DEAL_KEY_PREFIX = "data:deals:" const COMMENT_LOOKUP_KEY = "data:comments:lookup" const COMMENT_IDS_KEY = "data:comments:ids" function createRedisClient() { return getRedisClient() } function normalizeParentId(value) { if (value === undefined || value === null || value === "" || value === "null") return null const n = Number(value) return Number.isInteger(n) && n > 0 ? n : null } function pickComments(deal, parentId) { const list = Array.isArray(deal?.comments) ? deal.comments : [] return list.filter( (c) => (normalizeParentId(c.parentId) ?? null) === (normalizeParentId(parentId) ?? null) && !c.deletedAt ) } function sortComments(list, sort) { const mode = String(sort || "NEW").toUpperCase() if (mode === "TOP") { return list.sort((a, b) => { const diff = Number(b.likeCount || 0) - Number(a.likeCount || 0) if (diff !== 0) return diff return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) } return list.sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) } async function updateDealCommentsInRedis(dealId, comments, commentCount) { const redis = createRedisClient() try { const pipeline = redis.pipeline() pipeline.call("JSON.SET", `${DEAL_KEY_PREFIX}${dealId}`, "$.comments", JSON.stringify(comments)) if (typeof commentCount === "number") { pipeline.call("JSON.SET", `${DEAL_KEY_PREFIX}${dealId}`, "$.commentCount", Number(commentCount)) } await pipeline.exec() } finally {} } async function getCommentsForDeal({ dealId, deal = null, parentId = null, page = 1, limit = 10, sort = "NEW", viewerId = null, } = {}) { const resolvedDeal = deal || (await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS })) if (!resolvedDeal) return { page: 1, total: 0, totalPages: 0, results: [] } const normalizedLimit = Math.max(1, Math.min(Number(limit) || 10, 50)) const normalizedPage = Math.max(1, Number(page) || 1) let comments = pickComments(resolvedDeal, parentId) comments = sortComments(comments, sort) const total = comments.length const totalPages = Math.ceil(total / normalizedLimit) const start = (normalizedPage - 1) * normalizedLimit const results = comments.slice(start, start + normalizedLimit).map((comment) => { const likes = Array.isArray(comment.likes) ? comment.likes : [] const myLike = viewerId ? likes.some((l) => Number(l.userId) === Number(viewerId)) : false return { ...comment, myLike, } }) return { page: normalizedPage, total, totalPages, results } } async function addCommentToRedis(comment, { ttlSeconds = DEFAULT_TTL_SECONDS } = {}) { const deal = await getOrCacheDeal(comment.dealId, { ttlSeconds }) if (!deal) return { added: false } const comments = Array.isArray(deal.comments) ? deal.comments : [] const newComment = { id: comment.id, dealId: comment.dealId, userId: comment.userId, text: comment.text, createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt, parentId: normalizeParentId(comment.parentId), likeCount: Number.isFinite(comment.likeCount) ? comment.likeCount : 0, repliesCount: Number.isFinite(comment.repliesCount) ? comment.repliesCount : 0, deletedAt: null, user: comment.user ? { id: comment.user.id, username: comment.user.username, avatarUrl: comment.user.avatarUrl ?? null, } : null, likes: [], } comments.unshift(newComment) if (newComment.parentId) { const parent = comments.find((c) => Number(c.id) === Number(newComment.parentId)) if (parent) { parent.repliesCount = Number.isFinite(parent.repliesCount) ? parent.repliesCount + 1 : 1 } } const commentCount = Number.isFinite(deal.commentCount) ? deal.commentCount + 1 : 1 await updateDealCommentsInRedis(comment.dealId, comments, commentCount) await ensureMinDealTtl(comment.dealId, { minSeconds: DEFAULT_TTL_SECONDS }) const redis = createRedisClient() try { await redis.hset(COMMENT_LOOKUP_KEY, String(comment.id), String(comment.dealId)) await redis.sadd(COMMENT_IDS_KEY, String(comment.id)) } finally {} return { added: true } } async function removeCommentFromRedis({ commentId, dealId }) { const deal = await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS }) if (!deal) return { removed: false } const comments = Array.isArray(deal.comments) ? deal.comments : [] const target = comments.find((c) => Number(c.id) === Number(commentId)) if (!target) return { removed: false } if (target.deletedAt) return { removed: false, alreadyDeleted: true } target.deletedAt = new Date().toISOString() const commentCount = Math.max(0, Number(deal.commentCount || 0) - 1) if (target.parentId) { const parent = comments.find((c) => Number(c.id) === Number(target.parentId)) if (parent && Number.isFinite(parent.repliesCount)) { parent.repliesCount = Math.max(0, parent.repliesCount - 1) } } await updateDealCommentsInRedis(dealId, comments, commentCount) await ensureMinDealTtl(dealId, { minSeconds: DEFAULT_TTL_SECONDS }) return { removed: true } } async function updateCommentLikeInRedisByDeal({ dealId, commentId, userId, like }) { const deal = await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS }) if (!deal) return { liked: Boolean(like), delta: 0, likeCount: 0 } const comments = Array.isArray(deal.comments) ? deal.comments : [] const target = comments.find((c) => Number(c.id) === Number(commentId)) if (!target || target.deletedAt) return { liked: Boolean(like), delta: 0, likeCount: 0 } let likes = Array.isArray(target.likes) ? target.likes : [] const exists = likes.some((l) => Number(l.userId) === Number(userId)) let delta = 0 if (like) { if (!exists) { likes = [...likes, { userId: Number(userId) }] delta = 1 } } else if (exists) { likes = likes.filter((l) => Number(l.userId) !== Number(userId)) delta = -1 } target.likes = likes if (delta !== 0) { target.likeCount = Math.max(0, Number(target.likeCount || 0) + delta) } else { target.likeCount = Number.isFinite(target.likeCount) ? target.likeCount : likes.length } await updateDealCommentsInRedis(dealId, comments) await ensureMinDealTtl(dealId, { minSeconds: DEFAULT_TTL_SECONDS }) return { liked: Boolean(like), delta, likeCount: Number(target.likeCount || 0) } } async function updateCommentLikeInRedis({ commentId, userId, like }) { const dealId = await getDealIdByCommentId(commentId) if (!dealId) return { liked: Boolean(like), delta: 0, likeCount: 0 } return updateCommentLikeInRedisByDeal({ dealId, commentId, userId, like }) } module.exports = { getCommentsForDeal, addCommentToRedis, removeCommentFromRedis, updateCommentLikeInRedis, updateCommentLikeInRedisByDeal, }