// services/deal.service.js
const dealDB = require("../db/deal.db")
const userDB = require("../db/user.db")
const { findSellerFromLink } = require("./sellerLookup.service")
const { makeDetailWebp, makeThumbWebp } = require("../utils/processImage")
const { v4: uuidv4 } = require("uuid")
const { uploadImage } = require("./uploadImage.service")
const categoryDB = require("../db/category.db")
const { enqueueDealClassification } = require("../jobs/dealClassification.queue")
const { getCommentsForDeal } = require("./redis/commentCache.service")
const { getOrCacheDeal } = require("./redis/dealCache.service")
const { mapDealToRedisJson } = require("./redis/dealIndexing.service")
const { setDealInRedis } = require("./redis/dealCache.service")
const { queueDealCreate } = require("./redis/dbSync.service")
const { generateDealId } = require("./redis/dealId.service")
const { getSellerById } = require("./redis/sellerCache.service")
const { getDealVoteFromRedis } = require("./redis/dealVote.service")
const { getHotDealIds, getHotRangeDealIds, getDealsByIdsFromRedis } = require("./redis/hotDealList.service")
const { getTrendingDealIds } = require("./redis/trendingDealList.service")
const { getNewDealIds } = require("./redis/newDealList.service")
const { setUserPublicInRedis } = require("./redis/userPublicCache.service")
const {
  buildDealSearchQuery,
  searchDeals,
  buildTitlePrefixQuery,
  buildTextSearchQuery,
  buildPrefixTextQuery,
  buildFuzzyTextQuery,
} = require("./redis/dealSearch.service")

const DEFAULT_LIMIT = 20
const MAX_LIMIT = 50
const MAX_SKIP = 5000
const MS_PER_DAY = 24 * 60 * 60 * 1000

const DEAL_CARD_SELECT = {
  id: true,
  title: true,
  description: true,
  price: true,
  originalPrice: true,
  shippingPrice: true,
  couponCode: true,
  location: true,
  discountType: true,
  discountValue: true,
  score: true,
  commentCount: true,
  url: true,
  status: true,
  saletype: true,
  affiliateType: true,
  createdAt: true,
  updatedAt: true,
  customSeller: true,
  user: { select: { id: true, username: true, avatarUrl: true } },
  seller: { select: { id: true, name: true, url: true } },
  images: {
    orderBy: { order: "asc" },
    take: 1,
    select: { imageUrl: true },
  },
}

const DEAL_DETAIL_SELECT = {
  id: true,
  title: true,
  description: true,
  url: true,
  price: true,
  originalPrice: true,
  shippingPrice: true,
  couponCode: true,
  location: true,
  discountType: true,
  discountValue: true,
  score: true,
  commentCount: true,
  status: true,
  saletype: true,
  affiliateType: true,
  createdAt: true,
  updatedAt: true,
  categoryId: true,
  sellerId: true,
  customSeller: true,
  user: { select: { id: true, username: true, avatarUrl: true } },
  seller: { select: { id: true, name: true, url: true } },
  images: { orderBy: { order: "asc" }, select: { id: true, imageUrl: true, order: true } },
  notices: {
    where: { isActive: true },
    orderBy: { createdAt: "desc" },
    take: 1,
    select: {
      id: true,
      dealId: true,
      title: true,
      body: true,
      severity: true,
      isActive: true,
      createdBy: true,
      createdAt: true,
      updatedAt: true,
    },
  },
  _count: { select: { comments: true } },
}

const SIMILAR_DEAL_SELECT = {
  id: true,
  title: true,
  price: true,
  score: true,
  createdAt: true,
  categoryId: true,
  sellerId: true,
  customSeller: true,
  seller: { select: { name: true } },
  images: { take: 1, orderBy: { order: "asc" }, select: { imageUrl: true } },
}

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

function clampPagination({ page, limit }) {
  const rawPage = Number(page)
  const rawLimit = Number(limit)
  const normalizedPage = Number.isFinite(rawPage) ? Math.max(1, Math.floor(rawPage)) : 1
  let normalizedLimit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : DEFAULT_LIMIT
  normalizedLimit = Math.min(MAX_LIMIT, normalizedLimit)
  const skip = (normalizedPage - 1) * normalizedLimit
  if (skip > MAX_SKIP) {
    const err = new Error("PAGE_TOO_DEEP")
    err.statusCode = 400
    throw err
  }
  return { page: normalizedPage, limit: normalizedLimit, skip }
}

function buildSearchClause(q) {
  if (q === undefined || q === null) return null
  const normalized = String(q).trim()
  if (!normalized) return null
  return {
    OR: [
      { title: { contains: normalized, mode: "insensitive" } },
      { description: { contains: normalized, mode: "insensitive" } },
    ],
  }
}

const DEAL_STATUSES = new Set(["PENDING", "ACTIVE", "EXPIRED", "REJECTED"])
const SALE_TYPES = new Set(["ONLINE", "OFFLINE", "CODE"])
const AFFILIATE_TYPES = new Set(["AFFILIATE", "NON_AFFILIATE", "USER_AFFILIATE"])

function normalizeListInput(value) {
  if (Array.isArray(value)) {
    return value.flatMap((item) => String(item).split(","))
  }
  if (value === undefined || value === null) return []
  return String(value).split(",")
}

function parseInteger(value) {
  if (value === undefined || value === null || value === "") return null
  const num = Number(value)
  return Number.isInteger(num) ? num : null
}

function parseNumber(value) {
  if (value === undefined || value === null || value === "") return null
  const num = Number(value)
  return Number.isFinite(num) ? num : null
}

function parseBoolean(value) {
  if (typeof value === "boolean") return value
  if (typeof value === "number") return value === 1 ? true : value === 0 ? false : null
  if (typeof value === "string") {
    const normalized = value.trim().toLowerCase()
    if (["true", "1", "yes"].includes(normalized)) return true
    if (["false", "0", "no"].includes(normalized)) return false
  }
  return null
}

function parseDate(value) {
  if (value === undefined || value === null || value === "") return null
  const date = new Date(value)
  return Number.isNaN(date.getTime()) ? null : date
}

function parseIdList(value) {
  const items = normalizeListInput(value)
  const ids = items
    .map((item) => parseInteger(String(item).trim()))
    .filter((item) => item && item > 0)
  return ids.length ? Array.from(new Set(ids)) : null
}

function parseEnumList(value, allowedSet) {
  const items = normalizeListInput(value)
  const filtered = items
    .map((item) => String(item).trim().toUpperCase())
    .filter((item) => allowedSet.has(item))
  return filtered.length ? Array.from(new Set(filtered)) : null
}

function parseSort(value) {
  const normalized = String(value || "").trim().toLowerCase()
  if (["score", "price", "createdat", "createdatts"].includes(normalized)) return normalized
  return "createdAtTs"
}

function parseSortDir(value, sortBy) {
  const normalized = String(value || "").trim().toLowerCase()
  if (normalized === "asc") return "ASC"
  if (normalized === "desc") return "DESC"
  if (String(sortBy).toLowerCase() === "price") return "ASC"
  return "DESC"
}

function buildFilterWhere(rawFilters = {}, { allowStatus = false } = {}) {
  if (!rawFilters || typeof rawFilters !== "object") return null

  const clauses = []
  const categoryIds = parseIdList(rawFilters.categoryId ?? rawFilters.categoryIds)
  if (categoryIds?.length) {
    clauses.push({ categoryId: { in: categoryIds } })
  }

  const sellerIds = parseIdList(rawFilters.sellerId ?? rawFilters.sellerIds)
  if (sellerIds?.length) {
    clauses.push({ sellerId: { in: sellerIds } })
  }

  const saleTypes = parseEnumList(rawFilters.saleType, SALE_TYPES)
  if (saleTypes?.length) {
    const hasCode = saleTypes.includes("CODE")
    const others = saleTypes.filter((t) => t !== "CODE")
    if (hasCode) {
      const orClauses = []
      orClauses.push({
        saletype: "CODE",
        couponCode: { not: null },
      })
      if (others.length) {
        orClauses.push({ saletype: { in: others } })
      }
      clauses.push(orClauses.length === 1 ? orClauses[0] : { OR: orClauses })
    } else {
      clauses.push({ saletype: { in: saleTypes } })
    }
  }

  const affiliateTypes = parseEnumList(rawFilters.affiliateType, AFFILIATE_TYPES)
  if (affiliateTypes?.length) {
    clauses.push({ affiliateType: { in: affiliateTypes } })
  }

  if (allowStatus) {
    const statuses = parseEnumList(rawFilters.status, DEAL_STATUSES)
    if (statuses?.length) {
      clauses.push({ status: { in: statuses } })
    }
  }

  const minPrice = parseNumber(rawFilters.minPrice ?? rawFilters.priceMin)
  const maxPrice = parseNumber(rawFilters.maxPrice ?? rawFilters.priceMax)
  if (minPrice !== null || maxPrice !== null) {
    const price = {}
    if (minPrice !== null) price.gte = minPrice
    if (maxPrice !== null) price.lte = maxPrice
    clauses.push({ price })
  }

  const minScore = parseNumber(rawFilters.minScore)
  const maxScore = parseNumber(rawFilters.maxScore)
  if (minScore !== null || maxScore !== null) {
    const score = {}
    if (minScore !== null) score.gte = minScore
    if (maxScore !== null) score.lte = maxScore
    clauses.push({ score })
  }

  const createdAfter = parseDate(rawFilters.createdAfter ?? rawFilters.from)
  const createdBefore = parseDate(rawFilters.createdBefore ?? rawFilters.to)
  if (createdAfter || createdBefore) {
    const createdAt = {}
    if (createdAfter) createdAt.gte = createdAfter
    if (createdBefore) createdAt.lte = createdBefore
    clauses.push({ createdAt })
  }

  const hasImage = parseBoolean(rawFilters.hasImage)
  if (hasImage === true) {
    clauses.push({ images: { some: {} } })
  } else if (hasImage === false) {
    clauses.push({ images: { none: {} } })
  }

  if (!clauses.length) return null
  return clauses.length === 1 ? clauses[0] : { AND: clauses }
}

function buildRedisSearchFilters(rawFilters = {}, baseWhere = null) {
  const filters = rawFilters || {}
  const statuses = parseEnumList(filters.status, DEAL_STATUSES)

  const categoryIds = parseIdList(filters.categoryId ?? filters.categoryIds)
  const sellerIds = parseIdList(filters.sellerId ?? filters.sellerIds)
  const saleTypes = parseEnumList(filters.saleType, SALE_TYPES)

  const minPrice = parseNumber(filters.minPrice ?? filters.priceMin)
  const maxPrice = parseNumber(filters.maxPrice ?? filters.priceMax)

  const minScore = parseNumber(filters.minScore)
  const maxScore = parseNumber(filters.maxScore)

  const merged = {
    statuses,
    categoryIds,
    sellerIds,
    saleTypes,
    minPrice,
    maxPrice,
    minScore,
    maxScore,
  }

  if (baseWhere) {
    if (baseWhere.status) {
      merged.statuses = parseEnumList(baseWhere.status, DEAL_STATUSES) || merged.statuses
    }
    if (baseWhere.categoryId?.in) {
      merged.categoryIds = Array.from(new Set([...(merged.categoryIds || []), ...baseWhere.categoryId.in]))
    } else if (Number.isInteger(baseWhere.categoryId)) {
      merged.categoryIds = Array.from(new Set([...(merged.categoryIds || []), baseWhere.categoryId]))
    }
    if (baseWhere.sellerId?.in) {
      merged.sellerIds = Array.from(new Set([...(merged.sellerIds || []), ...baseWhere.sellerId.in]))
    } else if (Number.isInteger(baseWhere.sellerId)) {
      merged.sellerIds = Array.from(new Set([...(merged.sellerIds || []), baseWhere.sellerId]))
    }
  }

  return merged
}

const REDIS_SEARCH_LIMIT = 20

async function getDealsFromRedisSearch({
  q,
  page,
  viewer,
  filters,
  baseWhere,
} = {}) {
  const pagination = clampPagination({ page, limit: REDIS_SEARCH_LIMIT })
  const filterValues = buildRedisSearchFilters(filters, baseWhere)
  const filterQuery = buildDealSearchQuery(filterValues)
  const primaryTextQuery = buildPrefixTextQuery(q) ?? buildTextSearchQuery(q)
  const primaryQuery = [filterQuery, primaryTextQuery].filter(Boolean).join(" ") || "*"
  const sortBy = parseSort(filters?.sortBy)
  const sortDir = parseSortDir(filters?.sortDir, sortBy)

  let searchResult = await searchDeals({
    query: primaryQuery,
    page: pagination.page,
    limit: REDIS_SEARCH_LIMIT,
    sortBy,
    sortDir,
    includeMinMax: pagination.page === 1,
  })

  if (searchResult.total === 0 && q) {
    const fuzzyTextQuery = buildFuzzyTextQuery(q)
    if (fuzzyTextQuery) {
      const fuzzyQuery = [filterQuery, fuzzyTextQuery].filter(Boolean).join(" ") || "*"
      searchResult = await searchDeals({
        query: fuzzyQuery,
        page: pagination.page,
        limit: REDIS_SEARCH_LIMIT,
        sortBy,
        sortDir,
        includeMinMax: pagination.page === 1,
      })
    }
  }

  if (!searchResult.dealIds.length) {
    return {
      page: pagination.page,
      total: 0,
      totalPages: 0,
      results: [],
    }
  }

  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(searchResult.dealIds, viewerId)
  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return {
    page: searchResult.page,
    total: searchResult.total,
    totalPages: searchResult.totalPages,
    results: enriched,
    minPrice: searchResult.minPrice,
    maxPrice: searchResult.maxPrice,
  }
}
function buildPresetCriteria(preset, { viewer, targetUserId } = {}) {
  const now = new Date()
  switch (preset) {
    case "NEW":
      return { where: { status: "ACTIVE" }, orderBy: [{ createdAt: "desc" }] }
    case "HOT": {
      const cutoff = new Date(now.getTime() - 3 * MS_PER_DAY)
      return {
        where: { status: "ACTIVE", createdAt: { gte: cutoff } },
        orderBy: [{ score: "desc" }, { createdAt: "desc" }],
      }
    }
    case "TRENDING": {
      const cutoff = new Date(now.getTime() - 2 * MS_PER_DAY)
      return {
        where: { status: "ACTIVE", createdAt: { gte: cutoff } },
        orderBy: [{ score: "desc" }, { createdAt: "desc" }],
      }
    }
    case "MY": {
      if (!viewer?.userId) {
        const err = new Error("AUTH_REQUIRED")
        err.statusCode = 401
        throw err
      }
      return { where: { userId: viewer.userId }, orderBy: [{ createdAt: "desc" }] }
    }
    case "USER_PUBLIC": {
      if (!targetUserId) {
        const err = new Error("TARGET_USER_REQUIRED")
        err.statusCode = 400
        throw err
      }
      return { where: { userId: targetUserId, status: "ACTIVE" }, orderBy: [{ createdAt: "desc" }] }
    }
    case "HOT_DAY": {
      const cutoff = new Date(now.getTime() - 1 * MS_PER_DAY)
      return {
        where: { status: "ACTIVE", createdAt: { gte: cutoff } },
        orderBy: [{ score: "desc" }, { createdAt: "desc" }],
      }
    }
    case "HOT_WEEK": {
      const cutoff = new Date(now.getTime() - 7 * MS_PER_DAY)
      return {
        where: { status: "ACTIVE", createdAt: { gte: cutoff } },
        orderBy: [{ score: "desc" }, { createdAt: "desc" }],
      }
    }
    case "HOT_MONTH": {
      const cutoff = new Date(now.getTime() - 30 * MS_PER_DAY)
      return {
        where: { status: "ACTIVE", createdAt: { gte: cutoff } },
        orderBy: [{ score: "desc" }, { createdAt: "desc" }],
      }
    }
    case "RAW": {
      return { where: {}, orderBy: [{ createdAt: "desc" }] }
    }
    default: {
      const err = new Error("INVALID_PRESET")
      err.statusCode = 400
      throw err
    }
  }
}

// --------------------
// Similar deals helpers (tagsiz, lightweight)
// --------------------
function clamp(n, min, max) {
  return Math.max(min, Math.min(max, n))
}

function tokenizeTitle(title = "") {
  return String(title)
    .toLowerCase()
    .replace(/[^a-z0-9çğıöşü\s]/gi, " ")
    .split(/\s+/)
    .filter(Boolean)
    .filter((w) => w.length >= 3)
}

function titleOverlapScore(aTitle, bTitle) {
  const a = tokenizeTitle(aTitle)
  const b = tokenizeTitle(bTitle)
  if (!a.length || !b.length) return 0

  const aset = new Set(a)
  const bset = new Set(b)
  let hit = 0
  for (const w of bset) if (aset.has(w)) hit++

  const denom = Math.min(aset.size, bset.size) || 1
  return hit / denom // 0..1
}

/**
 * SimilarDeals: DealCard değil, minimal summary döndürür.
 * Beklenen candidate shape:
 * - id, title, price, score, createdAt, categoryId, sellerId, customSeller
 * - seller?: { name }
 * - images?: [{ imageUrl }]
 */
async function buildSimilarDealsForDetail(targetDeal, { limit = 12 } = {}) {
  const take = clamp(Number(limit) || 12, 1, 12)

  // Bu 2 DB fonksiyonu: ACTIVE filter + images(take:1) + seller(name) getirmeli
  const [byCategory, bySeller] = await Promise.all([
    dealDB.findSimilarCandidates(
      {
        id: { not: Number(targetDeal.id) },
        status: "ACTIVE",
        categoryId: Number(targetDeal.categoryId),
      },
      { take: 80, select: SIMILAR_DEAL_SELECT }
    ),
    targetDeal.sellerId
      ? dealDB.findSimilarCandidates(
          {
            id: { not: Number(targetDeal.id) },
            status: "ACTIVE",
            sellerId: Number(targetDeal.sellerId),
          },
          { take: 30, select: SIMILAR_DEAL_SELECT }
        )
      : Promise.resolve([]),
  ])

  const dedup = new Map()
  for (const d of [...byCategory, ...bySeller]) dedup.set(d.id, d)
  const candidates = Array.from(dedup.values())

  const now = Date.now()

  const scored = candidates.map((d) => {
    const sameCategory = d.categoryId === targetDeal.categoryId
    const sameSeller = Boolean(targetDeal.sellerId && d.sellerId === targetDeal.sellerId)

    const titleSim = titleOverlapScore(targetDeal.title, d.title) // 0..1
    const titlePoints = Math.round(titleSim * 25)

    const scoreVal = Number.isFinite(d.score) ? d.score : 0
    const scorePoints = clamp(Math.round(scoreVal / 10), 0, 25)

    const ageDays = Math.floor((now - new Date(d.createdAt).getTime()) / (1000 * 60 * 60 * 24))
    const recencyPoints = ageDays <= 3 ? 10 : ageDays <= 10 ? 6 : ageDays <= 30 ? 3 : 0

    const rank =
      (sameCategory ? 60 : 0) +
      (sameSeller ? 25 : 0) +
      titlePoints +
      scorePoints +
      recencyPoints

    return { d, rank }
  })

  scored.sort((a, b) => b.rank - a.rank)

  return scored.slice(0, take).map(({ d }) => ({
    id: d.id,
    title: d.title,
    price: d.price ?? null,
    score: Number.isFinite(d.score) ? d.score : 0,
    imageUrl: d.images?.[0]?.imageUrl || "",
    sellerName: d.seller?.name || d.customSeller || "Bilinmiyor",
    createdAt: formatDateAsString(d.createdAt),
    // url istersen:
    // url: d.url ?? null,
  }))
}

async function getHotDealsFromRedis({ page, limit, viewer, hotListId } = {}) {
  const pagination = clampPagination({ page, limit })
  const { hotListId: listId, dealIds } = await getHotDealIds({ hotListId })

  if (!dealIds.length) {
    return {
      page: pagination.page,
      total: 0,
      totalPages: 0,
      results: [],
      hotListId: listId,
    }
  }

  const pageIds = dealIds.slice(pagination.skip, pagination.skip + pagination.limit)
  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(pageIds, viewerId)

  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return {
    page: pagination.page,
    total: dealIds.length,
    totalPages: Math.ceil(dealIds.length / pagination.limit),
    results: enriched,
    hotListId: listId,
  }
}

async function getTrendingDealsFromRedis({ page, limit, viewer, trendingListId } = {}) {
  const pagination = clampPagination({ page, limit })
  const { trendingListId: listId, dealIds } = await getTrendingDealIds({ trendingListId })

  if (!dealIds.length) {
    return {
      page: pagination.page,
      total: 0,
      totalPages: 0,
      results: [],
      trendingListId: listId,
    }
  }

  const pageIds = dealIds.slice(pagination.skip, pagination.skip + pagination.limit)
  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(pageIds, viewerId)

  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return {
    page: pagination.page,
    total: dealIds.length,
    totalPages: Math.ceil(dealIds.length / pagination.limit),
    results: enriched,
    trendingListId: listId,
  }
}

async function getHotRangeDealsFromRedis({ page, limit, viewer, range } = {}) {
  const pagination = clampPagination({ page, limit })
  const { listId, dealIds } = await getHotRangeDealIds({ range })

  if (!dealIds.length) {
    return {
      page: pagination.page,
      total: 0,
      totalPages: 0,
      results: [],
      hotListId: listId,
    }
  }

  const pageIds = dealIds.slice(pagination.skip, pagination.skip + pagination.limit)
  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(pageIds, viewerId)

  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return {
    page: pagination.page,
    total: dealIds.length,
    totalPages: Math.ceil(dealIds.length / pagination.limit),
    results: enriched,
    hotListId: listId,
  }
}

async function getBestWidgetDeals({ viewer = null, limit = 5 } = {}) {
  const take = Math.max(1, Math.min(Number(limit) || 5, 20))
  const viewerId = viewer?.userId ? Number(viewer.userId) : null

  const [dayList, weekList, monthList] = await Promise.all([
    getHotRangeDealIds({ range: "day" }),
    getHotRangeDealIds({ range: "week" }),
    getHotRangeDealIds({ range: "month" }),
  ])

  const pickTop = async (ids = []) => {
    const pageIds = ids.slice(0, take)
    const deals = await getDealsByIdsFromRedis(pageIds, viewerId)
    return deals.map((deal) => ({
      ...deal,
      id: Number(deal.id),
      score: Number.isFinite(deal.score) ? deal.score : 0,
      commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
      price: deal.price ?? null,
      originalPrice: deal.originalPrice ?? null,
      shippingPrice: deal.shippingPrice ?? null,
      discountValue: deal.discountValue ?? null,
    }))
  }

  const [hotDay, hotWeek, hotMonth] = await Promise.all([
    pickTop(dayList?.dealIds || []),
    pickTop(weekList?.dealIds || []),
    pickTop(monthList?.dealIds || []),
  ])

  return { hotDay, hotWeek, hotMonth }
}

async function getDealSuggestions({ q, limit = 8, viewer } = {}) {
  const term = String(q || "").trim()
  if (!term) return { results: [] }

  const query = buildTitlePrefixQuery(term)
  if (!query) return { results: [] }

  const normalizedLimit = Math.max(1, Math.min(Number(limit) || 8, 20))
  const searchResult = await searchDeals({
    query,
    page: 1,
    limit: normalizedLimit,
    sortBy: "score",
    sortDir: "DESC",
  })

  if (!searchResult.dealIds.length) return { results: [] }

  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(searchResult.dealIds, viewerId)
  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return { results: enriched }
}

function hasSearchFilters(filters = {}) {
  if (!filters || typeof filters !== "object") return false
  const keys = [
    "status",
    "categoryId",
    "categoryIds",
    "sellerId",
    "sellerIds",
    "saleType",
    "minPrice",
    "maxPrice",
    "priceMin",
    "priceMax",
    "minScore",
    "maxScore",
    "sortBy",
    "sortDir",
  ]
  return keys.some((key) => filters[key] !== undefined && filters[key] !== null && String(filters[key]) !== "")
}

async function getNewDealsFromRedis({ page, viewer, newListId } = {}) {
  const pagination = clampPagination({ page, limit: REDIS_SEARCH_LIMIT })
  const { newListId: listId, dealIds } = await getNewDealIds({ newListId })

  if (!dealIds.length) {
    return {
      page: pagination.page,
      total: 0,
      totalPages: 0,
      results: [],
    }
  }

  const pageIds = dealIds.slice(pagination.skip, pagination.skip + pagination.limit)
  const viewerId = viewer?.userId ? Number(viewer.userId) : null
  const deals = await getDealsByIdsFromRedis(pageIds, viewerId)

  const enriched = deals.map((deal) => ({
    ...deal,
    id: Number(deal.id),
    score: Number.isFinite(deal.score) ? deal.score : 0,
    commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
    price: deal.price ?? null,
    originalPrice: deal.originalPrice ?? null,
    shippingPrice: deal.shippingPrice ?? null,
    discountValue: deal.discountValue ?? null,
  }))

  return {
    page: pagination.page,
    total: dealIds.length,
    totalPages: Math.ceil(dealIds.length / pagination.limit),
    results: enriched,
  }
}

async function getDeals({
  preset = "NEW",
  q,
  page,
  limit,
  viewer = null,
  targetUserId = null,
  filters = null,
  baseWhere = null,
  scope = "USER",
  hotListId = null,
  trendingListId = null,
  useRedisSearch = false,
}) {
  const pagination = clampPagination({ page, limit })
  if (preset === "HOT") {
    const listId = hotListId ?? filters?.hotListId ?? filters?.hotlistId ?? null
    return getHotDealsFromRedis({ page, limit, viewer, hotListId: listId })
  }
  if (preset === "HOT_DAY") {
    return getHotRangeDealsFromRedis({ page, limit, viewer, range: "day" })
  }
  if (preset === "HOT_WEEK") {
    return getHotRangeDealsFromRedis({ page, limit, viewer, range: "week" })
  }
  if (preset === "HOT_MONTH") {
    return getHotRangeDealsFromRedis({ page, limit, viewer, range: "month" })
  }
  if (preset === "TRENDING") {
    const listId = trendingListId ?? filters?.trendingListId ?? filters?.trendinglistId ?? null
    return getTrendingDealsFromRedis({ page, limit, viewer, trendingListId: listId })
  }

  if (preset === "NEW" && !q && !hasSearchFilters(filters) && !useRedisSearch) {
    const listId = filters?.newListId ?? filters?.newlistId ?? null
    return getNewDealsFromRedis({ page, viewer, newListId: listId })
  }

  if (useRedisSearch) {
    return getDealsFromRedisSearch({
      q,
      page,
      limit,
      viewer,
      filters,
      baseWhere,
    })
  }

  const { where: presetWhere, orderBy: presetOrder } = buildPresetCriteria(preset, {
    viewer,
    targetUserId,
  })
  const searchClause = buildSearchClause(q)
  const allowStatus = preset === "MY" || scope === "MOD"
  const filterWhere = buildFilterWhere(filters, { allowStatus })

  const clauses = []
  if (presetWhere && Object.keys(presetWhere).length > 0) clauses.push(presetWhere)
  if (baseWhere && Object.keys(baseWhere).length > 0) clauses.push(baseWhere)
  if (searchClause) clauses.push(searchClause)
  if (filterWhere) clauses.push(filterWhere)

  const finalWhere = clauses.length === 0 ? {} : clauses.length === 1 ? clauses[0] : { AND: clauses }
  const orderBy = presetOrder ?? [{ createdAt: "desc" }]

  const [deals, total] = await Promise.all([
    dealDB.findDeals(finalWhere, {
      skip: pagination.skip,
      take: pagination.limit,
      orderBy,
      select: DEAL_CARD_SELECT,
    }),
    dealDB.countDeals(finalWhere),
  ])

  const dealIds = deals.map((d) => d.id)
  const viewerId = viewer?.userId ? Number(viewer.userId) : null

  const enriched = deals.map((deal) => ({
    ...deal,
    myVote: viewerId
      ? Number(deal.votes?.find((vote) => Number(vote.userId) === viewerId)?.voteType ?? 0)
      : 0,
  }))

  return {
    page: pagination.page,
    total,
    totalPages: Math.ceil(total / pagination.limit),
    results: enriched,
  }
}

async function getDealById(id, viewer = null) {
  const deal = await getOrCacheDeal(Number(id), { ttlSeconds: 15 * 60 })

  if (!deal) return null

  const dealUserId = Number(deal.userId ?? deal.user?.id)

  if (deal.status === "PENDING" || deal.status === "REJECTED") {
    const isOwner = viewer?.userId && dealUserId === Number(viewer.userId)
    const isMod = viewer?.role === "MOD" || viewer?.role === "ADMIN"
    if (!isOwner && !isMod) return null
  }

  const [breadcrumb, similarDeals, userStatsAgg, myVote, commentsResp, seller] = await Promise.all([
    categoryDB.getCategoryBreadcrumb(deal.categoryId, { includeUndefined: false }),
    buildSimilarDealsForDetail(
      {
        id: deal.id,
        title: deal.title,
        categoryId: deal.categoryId,
        sellerId: deal.sellerId ?? null,
      },
      { limit: 12 }
    ),
    dealUserId
      ? dealDB.aggregateDeals({ userId: dealUserId, status: { in: ["ACTIVE", "EXPIRED"] } })
      : Promise.resolve(null),
    viewer?.userId ? getDealVoteFromRedis(deal.id, viewer.userId) : Promise.resolve(0),
    getCommentsForDeal({ dealId: deal.id, parentId: null, page: 1, limit: 10, sort: "NEW", viewerId: viewer?.userId ?? null }).catch(() => ({ results: [] })),
    deal.sellerId ? getSellerById(Number(deal.sellerId)) : Promise.resolve(null),
  ])

  const userStats = {
    totalLikes: userStatsAgg?._sum?.score ?? 0,
    totalDeals: userStatsAgg?._count?._all ?? 0,
  }

  return {
    ...deal,
    seller: deal.seller ?? seller ?? null,
    comments: commentsResp?.results || [],
    breadcrumb,
    similarDeals,
    userStats,
    myVote,
    isSaved: viewer?.userId
      ? Array.isArray(deal.savedBy) &&
        deal.savedBy.some((s) => Number(s?.userId) === Number(viewer.userId))
      : false,
  }
}

async function createDeal(dealCreateData, files = []) {
  const dealId = await generateDealId()
  const now = new Date()

  let sellerId = null
  if (dealCreateData.url) {
    const seller = await findSellerFromLink(dealCreateData.url)
    if (seller) {
      sellerId = seller.id
      dealCreateData.customSeller = null
    }
  }

  const userId = Number(dealCreateData?.user?.connect?.id)
  if (!Number.isInteger(userId) || userId <= 0) {
    const err = new Error("INVALID_USER")
    err.statusCode = 400
    throw err
  }

  const user = await userDB.findUser(
    { id: 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 err = new Error("USER_NOT_FOUND")
    err.statusCode = 404
    throw err
  }

  const images = []
  for (let i = 0; i < files.length && i < 5; i++) {
    const file = files[i]
    const order = i
    const key = uuidv4()
    const basePath = `deals/${dealId}/${key}`
    const detailPath = `${basePath}_detail.webp`
    const thumbPath = `${basePath}_thumb.webp`
    const BUCKET = "deal"

    const detailBuffer = await makeDetailWebp(file.buffer)
    const detailUrl = await uploadImage({
      bucket: BUCKET,
      path: detailPath,
      fileBuffer: detailBuffer,
      contentType: "image/webp",
    })

    if (order === 0) {
      const thumbBuffer = await makeThumbWebp(file.buffer)
      await uploadImage({
        bucket: BUCKET,
        path: thumbPath,
        fileBuffer: thumbBuffer,
        contentType: "image/webp",
      })
    }

    images.push({ id: 0, order, imageUrl: detailUrl })
  }

  const dealPayload = {
    id: dealId,
    title: dealCreateData.title,
    description: dealCreateData.description ?? null,
    url: dealCreateData.url ?? null,
    price: dealCreateData.price ?? null,
    originalPrice: dealCreateData.originalPrice ?? null,
    shippingPrice: dealCreateData.shippingPrice ?? null,
    percentOff: dealCreateData.percentOff ?? null,
    couponCode: dealCreateData.couponCode ?? null,
    location: dealCreateData.location ?? null,
    discountType: dealCreateData.discountType ?? null,
    discountValue: dealCreateData.discountValue ?? null,
    maxNotifiedMilestone: 0,
    userId,
    score: 0,
    commentCount: 0,
    status: "PENDING",
    saletype: dealCreateData.saletype ?? "ONLINE",
    affiliateType: dealCreateData.affiliateType ?? "NON_AFFILIATE",
    sellerId: sellerId ?? null,
    customSeller: dealCreateData.customSeller ?? null,
    categoryId: dealCreateData.categoryId ?? 0,
    createdAt: now,
    updatedAt: now,
    user,
    images,
    dealTags: [],
    votes: [],
    comments: [],
    aiReview: null,
  }

  const redisPayload = mapDealToRedisJson(dealPayload)
  await setUserPublicInRedis(user, { ttlSeconds: 31 * 24 * 60 * 60 })
  await setDealInRedis(dealId, redisPayload, {
    ttlSeconds: 31 * 24 * 60 * 60,
    skipAnalyticsInit: true,
  })

  queueDealCreate({
    dealId,
    data: {
      id: dealId,
      title: dealPayload.title,
      description: dealPayload.description,
      url: dealPayload.url,
      price: dealPayload.price,
      originalPrice: dealPayload.originalPrice,
      shippingPrice: dealPayload.shippingPrice,
      percentOff: dealPayload.percentOff,
      couponCode: dealPayload.couponCode,
      location: dealPayload.location,
      discountType: dealPayload.discountType,
      discountValue: dealPayload.discountValue,
      maxNotifiedMilestone: dealPayload.maxNotifiedMilestone,
      userId,
      status: dealPayload.status,
      saletype: dealPayload.saletype,
      affiliateType: dealPayload.affiliateType,
      sellerId: dealPayload.sellerId,
      customSeller: dealPayload.customSeller,
      categoryId: dealPayload.categoryId,
      createdAt: now.toISOString(),
      updatedAt: now.toISOString(),
    },
    images: images.map((img) => ({ imageUrl: img.imageUrl, order: img.order })),
    createdAt: now.toISOString(),
  }).catch((err) => console.error("DB sync deal create failed:", err?.message || err))

  await enqueueDealClassification({ dealId })

  const seller = dealPayload.sellerId ? await getSellerById(dealPayload.sellerId) : null
  const responseDeal = {
    ...dealPayload,
    seller: seller ?? null,
    images,
    comments: [],
    notices: [],
    breadcrumb: [],
    similarDeals: [],
    userStats: { totalLikes: 0, totalDeals: 0 },
    myVote: 0,
    _count: { comments: 0 },
  }

  return responseDeal
}

async function getDealEngagement(ids = [], viewer = null) {
  const normalized = (Array.isArray(ids) ? ids : [])
    .map((id) => Number(id))
    .filter((id) => Number.isInteger(id) && id > 0)

  const uniqueIds = Array.from(new Set(normalized))

  if (!viewer?.userId || uniqueIds.length === 0) {
    return normalized.map((id) => ({ id, myVote: 0 }))
  }

  const votes = await dealDB.findVotes(
    { userId: viewer.userId, dealId: { in: uniqueIds } },
    { select: { dealId: true, voteType: true } }
  )
  const voteByDealId = new Map()
  votes.forEach((vote) => voteByDealId.set(vote.dealId, vote.voteType))

  return normalized.map((id) => ({
    id,
    myVote: voteByDealId.get(id) ?? 0,
  }))
}

module.exports = {
  getDeals,
  getDealById,
  createDeal,
  getDealEngagement,
  getDealSuggestions,
  getBestWidgetDeals,
}
