const dealDB = require("../../db/deal.db")
const userDB = require("../../db/user.db")
const dealAnalyticsDb = require("../../db/dealAnalytics.db")
const { getRedisClient } = require("./client")
const { mapDealToRedisJson } = require("./dealIndexing.service")
const { recordCacheMiss } = require("./cacheMetrics.service")
const {
  getUserPublicFromRedis,
  setUserPublicInRedis,
  ensureUserMinTtl,
} = require("./userPublicCache.service")

const DEAL_KEY_PREFIX = "data:deals:"
const DEAL_VOTE_HASH_PREFIX = "data:deals:votes:"
const DEAL_ANALYTICS_TOTAL_PREFIX = "data:deals:analytics:total:"
const COMMENT_LOOKUP_KEY = "data:comments:lookup"
const COMMENT_IDS_KEY = "data:comments:ids"

function createRedisClient() {
  return getRedisClient()
}

function toIso(value) {
  return value instanceof Date ? value.toISOString() : value ?? null
}

function toEpochMs(value) {
  if (value instanceof Date) return value.getTime()
  const date = new Date(value)
  return Number.isNaN(date.getTime()) ? null : date.getTime()
}

async function getAnalyticsTotalsForDeal(dealId) {
  const id = Number(dealId)
  if (!Number.isInteger(id) || id <= 0) {
    return { impressions: 0, views: 0, clicks: 0 }
  }
  await dealAnalyticsDb.ensureTotalsForDealIds([id])
  const totals = await dealAnalyticsDb.getTotalsByDealIds([id])
  const entry = totals?.[0] || { impressions: 0, views: 0, clicks: 0 }
  return {
    impressions: Number(entry.impressions) || 0,
    views: Number(entry.views) || 0,
    clicks: Number(entry.clicks) || 0,
  }
}

async function cacheVotesAndAnalytics(redis, dealId, payload, { ttlSeconds, skipDbEnsure } = {}) {
  const voteKey = `${DEAL_VOTE_HASH_PREFIX}${dealId}`
  const analyticsKey = `${DEAL_ANALYTICS_TOTAL_PREFIX}${dealId}`

  const pipeline = redis.pipeline()

  pipeline.del(voteKey)
  if (Array.isArray(payload?.votes) && payload.votes.length) {
    payload.votes.forEach((vote) => {
      if (!vote?.userId) return
      pipeline.hset(voteKey, String(vote.userId), String(vote.voteType ?? 0))
    })
  }

  const totals = skipDbEnsure
    ? { impressions: 0, views: 0, clicks: 0 }
    : await getAnalyticsTotalsForDeal(dealId)
  pipeline.hset(
    analyticsKey,
    "impressions",
    String(totals.impressions || 0),
    "views",
    String(totals.views || 0),
    "clicks",
    String(totals.clicks || 0)
  )

  if (ttlSeconds) {
    if (Array.isArray(payload?.votes) && payload.votes.length) {
      pipeline.expire(voteKey, Number(ttlSeconds))
    }
    pipeline.expire(analyticsKey, Number(ttlSeconds))
  }

  await pipeline.exec()
}

async function ensureMinDealTtl(dealId, { minSeconds = 15 * 60 } = {}) {
  const id = Number(dealId)
  if (!Number.isInteger(id) || id <= 0) return { bumped: false }
  const redis = createRedisClient()
  const key = `${DEAL_KEY_PREFIX}${id}`
  const voteKey = `${DEAL_VOTE_HASH_PREFIX}${id}`
  const analyticsKey = `${DEAL_ANALYTICS_TOTAL_PREFIX}${id}`
  const minTtl = Math.max(1, Number(minSeconds) || 15 * 60)

  try {
    const ttl = await redis.ttl(key)
    if (ttl === -2) return { bumped: false } // no key
    if (ttl === -1 || ttl < minTtl) {
      const nextTtl = minTtl
      const pipeline = redis.pipeline()
      pipeline.expire(key, nextTtl)
      pipeline.expire(voteKey, nextTtl)
      pipeline.expire(analyticsKey, nextTtl)
      await pipeline.exec()
      return { bumped: true, ttl: nextTtl }
    }
    return { bumped: false, ttl }
  } finally {}
}

async function updateDealSavesInRedis({ dealId, userId, action, createdAt, minSeconds = 15 * 60 } = {}) {
  const id = Number(dealId)
  const uid = Number(userId)
  if (!Number.isInteger(id) || id <= 0 || !Number.isInteger(uid) || uid <= 0) {
    return { updated: false }
  }
  const normalized = String(action || "SAVE").toUpperCase()
  if (!["SAVE", "UNSAVE"].includes(normalized)) return { updated: false }

  const redis = createRedisClient()
  const key = `${DEAL_KEY_PREFIX}${id}`
  try {
    const raw = await redis.call("JSON.GET", key, "$.savedBy")
    let savedBy = []
    if (raw) {
      const parsed = JSON.parse(raw)
      const arr = Array.isArray(parsed) ? parsed[0] : []
      savedBy = Array.isArray(arr) ? arr : []
    }

    const exists = savedBy.some((s) => Number(s?.userId) === uid)
    if (normalized === "SAVE" && !exists) {
      savedBy = [
        { userId: uid, createdAt: createdAt ? toIso(createdAt) : new Date().toISOString() },
        ...savedBy,
      ]
    } else if (normalized === "UNSAVE" && exists) {
      savedBy = savedBy.filter((s) => Number(s?.userId) !== uid)
    }

    await redis.call("JSON.SET", key, "$.savedBy", JSON.stringify(savedBy))
    await ensureMinDealTtl(id, { minSeconds })
    return { updated: true }
  } finally {}
}

async function getDealFromRedis(dealId) {
  const redis = createRedisClient()
  try {
    const key = `${DEAL_KEY_PREFIX}${dealId}`
    const raw = await redis.call("JSON.GET", key)
    if (!raw) {
      await recordCacheMiss({ key })
      return null
    }
    const deal = JSON.parse(raw)
    if (!deal?.user && deal?.userId) {
      const cachedUser = await getUserPublicFromRedis(deal.userId)
      if (cachedUser) {
        deal.user = cachedUser
      } else {
        const user = await userDB.findUser(
          { id: Number(deal.userId) },
          {
            select: {
              id: true,
              username: true,
              avatarUrl: true,
              userBadges: {
                orderBy: { earnedAt: "desc" },
                select: {
                  earnedAt: true,
                  badge: { select: { id: true, name: true, iconUrl: true, description: true } },
                },
              },
            },
          }
        )
        if (user) {
          const ttl = await redis.ttl(key)
          const ttlSeconds = ttl > 0 ? ttl : undefined
          await setUserPublicInRedis(user, { ttlSeconds })
          deal.user = user
        }
      }
    }
    return deal
  } finally {}
}

async function cacheDealFromDb(dealId, { ttlSeconds = 1800 } = {}) {
  const deal = await dealDB.findDeal(
    { id: Number(dealId) },
    {
      include: {
        user: {
          select: {
            id: true,
            username: true,
            avatarUrl: true,
            userBadges: {
              orderBy: { earnedAt: "desc" },
              select: {
                earnedAt: true,
                badge: { select: { id: true, name: true, iconUrl: true, description: true } },
              },
            },
          },
        },
        images: { orderBy: { order: "asc" }, select: { id: true, imageUrl: true, order: true } },
        dealTags: { include: { tag: { select: { id: true, slug: true, name: true } } } },
        votes: { select: { userId: true, voteType: true } },
        savedBy: { select: { userId: true, createdAt: true } },
        comments: {
          orderBy: { createdAt: "desc" },
          include: {
            user: { select: { id: true, username: true, avatarUrl: true } },
            likes: { select: { userId: true } },
          },
        },
        aiReview: {
          select: {
            bestCategoryId: true,
            tags: true,
            needsReview: true,
            hasIssue: true,
            issueType: true,
            issueReason: true,
          },
        },
      },
    }
  )

  if (!deal) return null
  if (deal.user) {
    await setUserPublicInRedis(deal.user, { ttlSeconds })
  }
  const payload = mapDealToRedisJson(deal)
  const redis = createRedisClient()
  try {
    const key = `${DEAL_KEY_PREFIX}${deal.id}`
    const pipeline = redis.pipeline()
    pipeline.call("JSON.SET", key, "$", JSON.stringify(payload))
    if (ttlSeconds) {
      pipeline.expire(key, Number(ttlSeconds))
    }
    if (Array.isArray(payload.comments) && payload.comments.length) {
      payload.comments.forEach((comment) => {
        pipeline.hset(COMMENT_LOOKUP_KEY, String(comment.id), String(deal.id))
        pipeline.sadd(COMMENT_IDS_KEY, String(comment.id))
      })
    }
    await pipeline.exec()
    await cacheVotesAndAnalytics(redis, deal.id, payload, { ttlSeconds })
  } finally {}
  if (deal.user) {
    await ensureUserMinTtl(deal.user.id, { minSeconds: ttlSeconds })
  }
  return deal.user ? { ...payload, user: deal.user } : payload
}

async function getDealIdByCommentId(commentId) {
  const redis = createRedisClient()
  try {
    const raw = await redis.hget(COMMENT_LOOKUP_KEY, String(commentId))
    if (!raw) {
      await recordCacheMiss({ key: `${COMMENT_LOOKUP_KEY}:${commentId}`, label: "comment-lookup" })
    }
    return raw ? Number(raw) : null
  } finally {}
}

async function getOrCacheDeal(dealId, { ttlSeconds = 1800 } = {}) {
  const cached = await getDealFromRedis(dealId)
  if (cached) {
    await ensureMinDealTtl(dealId, { minSeconds: ttlSeconds })
    return cached
  }
  return cacheDealFromDb(dealId, { ttlSeconds })
}

async function getOrCacheDealForModeration(dealId, { ttlSeconds = 1800 } = {}) {
  const cached = await getDealFromRedis(dealId)
  if (cached) return { deal: cached, fromCache: true }
  const deal = await cacheDealFromDb(dealId, { ttlSeconds })
  return { deal, fromCache: false }
}

async function updateDealInRedis(dealId, patch = {}, { updatedAt = new Date() } = {}) {
  const redis = createRedisClient()
  const key = `${DEAL_KEY_PREFIX}${dealId}`
  const ts = toEpochMs(updatedAt)
  const iso = toIso(updatedAt)

  try {
    const exists = await redis.call("JSON.GET", key)
    if (!exists) return null

    const pipeline = redis.pipeline()
    Object.entries(patch || {}).forEach(([field, value]) => {
      if (value === undefined) return
      pipeline.call("JSON.SET", key, `$.${field}`, JSON.stringify(value))
    })
    if (iso) pipeline.call("JSON.SET", key, "$.updatedAt", JSON.stringify(iso))
    if (ts != null) pipeline.call("JSON.SET", key, "$.updatedAtTs", JSON.stringify(ts))
    await pipeline.exec()

    const raw = await redis.call("JSON.GET", key)
    return raw ? JSON.parse(raw) : null
  } finally {}
}

async function setDealInRedis(
  dealId,
  payload,
  { ttlSeconds = 31 * 24 * 60 * 60, skipAnalyticsInit = false } = {}
) {
  if (!dealId || !payload) return null
  const redis = createRedisClient()
  const key = `${DEAL_KEY_PREFIX}${dealId}`
  try {
    const pipeline = redis.pipeline()
    pipeline.call("JSON.SET", key, "$", JSON.stringify(payload))
    if (ttlSeconds) {
      pipeline.expire(key, Number(ttlSeconds))
    }
    if (Array.isArray(payload.comments) && payload.comments.length) {
      payload.comments.forEach((comment) => {
        pipeline.hset(COMMENT_LOOKUP_KEY, String(comment.id), String(dealId))
        pipeline.sadd(COMMENT_IDS_KEY, String(comment.id))
      })
    }
    await pipeline.exec()
    await cacheVotesAndAnalytics(redis, dealId, payload, {
      ttlSeconds,
      skipDbEnsure: skipAnalyticsInit,
    })
    return payload
  } finally {}
}

module.exports = {
  getDealFromRedis,
  cacheDealFromDb,
  getOrCacheDeal,
  getDealIdByCommentId,
  getOrCacheDealForModeration,
  updateDealInRedis,
  setDealInRedis,
  ensureMinDealTtl,
  updateDealSavesInRedis,
}
