import { computeHotScore } from './plaza-algorithm.mjs';

const RECALL_LIMIT = 80;
const CANDIDATE_POOL_LIMIT = 320;
const RANK_CACHE_TTL_MS = 180_000;
const RANK_CACHE_MAX = 240;

export const DEFAULT_RECOMMEND_CONFIG = {
  w_category: 0.26,
  w_tag: 0.18,
  w_author: 0.15,
  w_browse: 0.08,
  w_hot: 0.11,
  w_fresh: 0.1,
  w_quality: 0.07,
  w_ctr: 0.05,
  w_seen_penalty: 0.38,
  w_impression_penalty: 0.14,
  w_dislike_penalty: 1.35,
  w_category_dislike: 0.55,
  mmr_lambda: 0.72,
  profile_half_life_days: 7,
  explore_ratio: 0.1,
  follow_boost: 0.35,
  channel_boost_cap: 0.24,
};

const CHANNEL_WEIGHTS = {
  interest: 1.0,
  follow: 0.95,
  similar: 0.88,
  tag: 0.92,
  tag_similar: 0.86,
  fresh: 0.82,
  hot: 0.65,
  explore: 0.55,
};

const rankSessionCache = new Map();

function parseTags(raw) {
  if (Array.isArray(raw)) return raw.map((tag) => String(tag).toLowerCase());
  try {
    return JSON.parse(raw ?? '[]').map((tag) => String(tag).toLowerCase());
  } catch {
    return [];
  }
}

function parseRecommendCursor(cursor) {
  if (!cursor) return { offset: 0, profileVersion: null };
  try {
    const parsed = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
    return {
      offset: Math.max(0, Number(parsed.o) || 0),
      profileVersion: parsed.p == null ? null : Number(parsed.p),
    };
  } catch {
    return { offset: 0, profileVersion: null };
  }
}

function encodeRecommendCursor({ offset, profileVersion }) {
  return Buffer.from(JSON.stringify({ o: offset, p: profileVersion }), 'utf8').toString('base64url');
}

function jaccard(a, b) {
  const left = new Set(a);
  const right = new Set(b);
  if (left.size === 0 || right.size === 0) return 0;
  let inter = 0;
  for (const item of left) {
    if (right.has(item)) inter += 1;
  }
  const union = left.size + right.size - inter;
  return union > 0 ? inter / union : 0;
}

function freshnessScore(publishedAt, now = Date.now()) {
  const ageHours = Math.max(0, (now - publishedAt) / 3_600_000);
  return Math.exp(-ageHours / 72);
}

function qualityScore(row) {
  const views = Number(row.view_count ?? 0);
  const likes = Number(row.like_count ?? 0);
  const comments = Number(row.comment_count ?? 0);
  return Math.log1p(views) * 0.35 + Math.log1p(likes) * 0.45 + Math.log1p(comments) * 0.2;
}

function ctrPrior(row) {
  const views = Math.max(1, Number(row.view_count ?? 0));
  const likes = Number(row.like_count ?? 0);
  const collects = Number(row.collect_count ?? 0);
  return (likes * 2 + collects * 3) / views;
}

function itemSimilarity(a, b) {
  if (a.category_slug === b.category_slug) return 1;
  if (a.author_id === b.author_id) return 0.82;
  return jaccard(a.tags, b.tags) * 0.75;
}

function mmrRerank(items, { lambda, limit }) {
  const selected = [];
  const remaining = [...items];
  while (selected.length < limit && remaining.length > 0) {
    let bestIndex = 0;
    let bestScore = -Infinity;
    for (let index = 0; index < remaining.length; index += 1) {
      const candidate = remaining[index];
      let maxSimilarity = 0;
      for (const chosen of selected) {
        maxSimilarity = Math.max(maxSimilarity, itemSimilarity(candidate, chosen));
      }
      const mmrScore = candidate.rank_score - lambda * maxSimilarity;
      if (mmrScore > bestScore) {
        bestScore = mmrScore;
        bestIndex = index;
      }
    }
    selected.push(remaining.splice(bestIndex, 1)[0]);
  }
  return selected;
}

function topWeightedKeys(weightMap, limit = 5) {
  return Object.entries(weightMap ?? {})
    .sort((a, b) => b[1] - a[1])
    .slice(0, limit)
    .map(([key]) => key);
}

function hashSeed(value) {
  let hash = 2166136261;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
}

function seededShuffle(items, seed) {
  const list = [...items];
  let state = hashSeed(String(seed));
  for (let index = list.length - 1; index > 0; index -= 1) {
    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
    const swapIndex = state % (index + 1);
    [list[index], list[swapIndex]] = [list[swapIndex], list[index]];
  }
  return list;
}

function coldStartInterleave(candidates, sessionSeed = 'anon') {
  const groups = new Map();
  for (const candidate of candidates) {
    const key = candidate.category_slug || 'other';
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(candidate);
  }
  for (const list of groups.values()) {
    list.sort((a, b) => b.hot_score - a.hot_score || b.published_at - a.published_at);
  }
  const categories = seededShuffle([...groups.keys()], sessionSeed);
  const ordered = [];
  let progress = true;
  while (progress && ordered.length < candidates.length) {
    progress = false;
    for (const category of categories) {
      const list = groups.get(category);
      if (!list || list.length === 0) continue;
      ordered.push(list.shift());
      progress = true;
    }
  }
  return ordered.map((candidate, index) => ({
    ...candidate,
    rank_score: 1 - index * 0.0008,
  }));
}

function rankCacheKey(viewerId, sessionId, categorySlug, profileVersion) {
  return `${viewerId || sessionId || 'anon'}:${categorySlug || 'all'}:${profileVersion}`;
}

function getRankCache(key) {
  const cached = rankSessionCache.get(key);
  if (!cached) return null;
  if (cached.expiresAt <= Date.now()) {
    rankSessionCache.delete(key);
    return null;
  }
  return cached;
}

function setRankCache(key, orderedItems) {
  if (rankSessionCache.size >= RANK_CACHE_MAX) {
    const oldestKey = rankSessionCache.keys().next().value;
    if (oldestKey) rankSessionCache.delete(oldestKey);
  }
  rankSessionCache.set(key, {
    orderedItems,
    expiresAt: Date.now() + RANK_CACHE_TTL_MS,
  });
}

function buildTagMatchClause(tags, params) {
  if (tags.length === 0) return { clause: ' AND 1 = 0', params };
  const parts = tags.map((tag) => {
    params.push(JSON.stringify(tag));
    return 'JSON_CONTAINS(pp.tags, ?, "$")';
  });
  return { clause: ` AND (${parts.join(' OR ')})`, params };
}

export function createPlazaRecommendService(
  pool,
  {
    eventService,
    formatPostRow,
    loadViewerReactions = null,
    algorithmConfig = null,
    config: configOverride = null,
  },
) {
  const config = { ...DEFAULT_RECOMMEND_CONFIG, ...configOverride };

  const rowToCandidate = (row) => ({
    id: row.id,
    category_slug: row.category_slug,
    category_name: row.category_name,
    category_icon: row.category_icon,
    author_id: row.user_id,
    tags: parseTags(row.tags),
    hot_score: Number(row.hot_score ?? 0),
    published_at: Number(row.published_at),
    view_count: Number(row.view_count ?? 0),
    like_count: Number(row.like_count ?? 0),
    collect_count: Number(row.collect_count ?? 0),
    comment_count: Number(row.comment_count ?? 0),
    row,
  });

  const loadPublishedRows = async ({
    categorySlug = null,
    limit = CANDIDATE_POOL_LIMIT,
    orderBy = 'pp.hot_score DESC, pp.id DESC',
  }) => {
    const params = ['published'];
    let filter = '';
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    params.push(limit);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY ${orderBy}
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const scoreCandidate = (candidate, profile, now, maxHotScore, categorySlug) => {
    if (profile.dislikedPostIds.has(candidate.id)) {
      return { ...candidate, rank_score: -999, features: { filtered: true } };
    }
    if (profile.dislikedCategorySlugs.has(candidate.category_slug)) {
      return { ...candidate, rank_score: -999, features: { filtered: true } };
    }

    const categoryAffinity = profile.categoryWeights[candidate.category_slug] ?? 0;
    const tagAffinity = jaccard(candidate.tags, topWeightedKeys(profile.tagWeights, 20));
    const authorAffinity = profile.authorWeights[candidate.author_id] ?? 0;
    const browseAffinity =
      !categorySlug && profile.feedCategoryWeights
        ? profile.feedCategoryWeights[candidate.category_slug] ?? 0
        : 0;
    const followBoost = profile.followedAuthorIds.has(candidate.author_id) ? config.follow_boost : 0;
    const hot = candidate.hot_score / Math.max(maxHotScore, 1);
    const fresh = freshnessScore(candidate.published_at, now);
    const quality = qualityScore(candidate);
    const ctr = ctrPrior(candidate);
    const seenPenalty = profile.deepSeenPostIds.has(candidate.id)
      ? config.w_seen_penalty
      : profile.seenPostIds.has(candidate.id)
        ? config.w_impression_penalty
        : 0;
    const dislikePenalty = profile.dislikedPostIds.has(candidate.id) ? config.w_dislike_penalty : 0;
    const categoryDislikePenalty = profile.dislikedCategorySlugs.has(candidate.category_slug)
      ? config.w_category_dislike
      : 0;
    const channelBoost = Math.min(
      config.channel_boost_cap,
      (candidate.recall_channels ?? []).reduce(
        (sum, channel) => sum + (CHANNEL_WEIGHTS[channel] ?? 0) * 0.05,
        0,
      ),
    );

    const rankScore =
      config.w_category * categoryAffinity +
      config.w_tag * tagAffinity +
      config.w_author * (authorAffinity + followBoost) +
      config.w_browse * browseAffinity +
      config.w_hot * hot +
      config.w_fresh * fresh +
      config.w_quality * quality +
      config.w_ctr * ctr +
      channelBoost -
      seenPenalty -
      dislikePenalty -
      categoryDislikePenalty;

    return {
      ...candidate,
      rank_score: rankScore,
      features: {
        category_affinity: categoryAffinity,
        tag_affinity: tagAffinity,
        author_affinity: authorAffinity,
        browse_affinity: browseAffinity,
        follow_boost: followBoost,
        hot,
        fresh,
        quality,
        ctr,
        seen_penalty: seenPenalty,
        dislike_penalty: dislikePenalty,
        category_dislike_penalty: categoryDislikePenalty,
        channel_boost: channelBoost,
      },
    };
  };

  const mergeRecall = (target, rows, channel) => {
    for (const row of rows) {
      const candidate = rowToCandidate(row);
      const existing = target.get(candidate.id);
      if (existing) {
        existing.recall_channels = [...new Set([...(existing.recall_channels ?? []), channel])];
        existing.recall_score = Math.max(existing.recall_score ?? 0, CHANNEL_WEIGHTS[channel] ?? 0);
        continue;
      }
      target.set(candidate.id, {
        ...candidate,
        recall_channels: [channel],
        recall_score: CHANNEL_WEIGHTS[channel] ?? 0,
      });
    }
  };

  const recallInterest = async (profile, categorySlug) => {
    const categories = topWeightedKeys(profile.categoryWeights, 4);
    if (categories.length === 0) return [];
    const params = ['published', ...categories];
    let filter = ` AND c.slug IN (${categories.map(() => '?').join(',')})`;
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.hot_score DESC, pp.published_at DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallFollow = async (profile, categorySlug) => {
    const authors = [...profile.followedAuthorIds];
    if (authors.length === 0) return [];
    const params = ['published', ...authors];
    let filter = ` AND pp.user_id IN (${authors.map(() => '?').join(',')})`;
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.published_at DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallSimilar = async (profile, userId, categorySlug) => {
    const seeds = [...profile.likedPostIds].slice(0, 12);
    if (seeds.length === 0) return [];
    const params = ['published', ...seeds];
    let filter = ` AND r1.post_id IN (${seeds.map(() => '?').join(',')})`;
    if (userId) {
      filter += ' AND r2.user_id <> ?';
      params.push(userId);
    }
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon,
              pr.public_url,
              COUNT(*) AS overlap_score
       FROM plaza_reactions r1
       JOIN plaza_reactions r2
         ON r2.user_id = r1.user_id
        AND r2.type IN ('like', 'collect')
        AND r2.post_id <> r1.post_id
       JOIN plaza_posts pp ON pp.id = r2.post_id AND pp.status = 'published'
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE r1.type IN ('like', 'collect') ${filter}
       GROUP BY pp.id, c.name, c.slug, c.icon
       ORDER BY overlap_score DESC, pp.hot_score DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallFresh = async (profile, categorySlug) => {
    const categories = topWeightedKeys(profile.categoryWeights, 3);
    const params = ['published'];
    let filter = '';
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    } else if (categories.length > 0) {
      filter += ` AND c.slug IN (${categories.map(() => '?').join(',')})`;
      params.push(...categories);
    }
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.published_at DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallExplore = async (profile, categorySlug) => {
    const dominant = new Set(topWeightedKeys(profile.categoryWeights, 2));
    const params = ['published'];
    let filter = '';
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    } else if (dominant.size > 0) {
      filter += ` AND c.slug NOT IN (${[...dominant].map(() => '?').join(',')})`;
      params.push(...dominant);
    }
    params.push(Math.max(12, Math.floor(RECALL_LIMIT * config.explore_ratio)));
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.published_at DESC, pp.hot_score DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallByTags = async (profile, categorySlug) => {
    const tags = topWeightedKeys(profile.tagWeights, 6);
    if (tags.length === 0) return [];
    const params = ['published'];
    let filter = '';
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    const tagMatch = buildTagMatchClause(tags, params);
    filter += tagMatch.clause;
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.hot_score DESC, pp.published_at DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallContentSimilar = async (profile, categorySlug) => {
    const seeds = [...profile.likedPostIds].slice(0, 8);
    if (seeds.length === 0) return [];
    const [seedRows] = await pool.query(
      `SELECT tags FROM plaza_posts WHERE id IN (${seeds.map(() => '?').join(',')}) AND status = 'published'`,
      seeds,
    );
    const tagSet = new Set();
    for (const row of seedRows) {
      for (const tag of parseTags(row.tags)) tagSet.add(tag);
    }
    const tags = [...tagSet].slice(0, 10);
    if (tags.length === 0) return [];
    const params = ['published'];
    let filter = '';
    if (categorySlug) {
      filter += ' AND c.slug = ?';
      params.push(categorySlug);
    }
    if (seeds.length > 0) {
      filter += ` AND pp.id NOT IN (${seeds.map(() => '?').join(',')})`;
      params.push(...seeds);
    }
    const tagMatch = buildTagMatchClause(tags, params);
    filter += tagMatch.clause;
    params.push(RECALL_LIMIT);
    const [rows] = await pool.query(
      `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url
       FROM plaza_posts pp
       JOIN plaza_categories c ON c.id = pp.category_id
       JOIN h5_publish_records pr ON pr.id = pp.publication_id
       WHERE pp.status = ? ${filter}
       ORDER BY pp.published_at DESC, pp.hot_score DESC
       LIMIT ?`,
      params,
    );
    return rows;
  };

  const recallHotFallback = async (categorySlug) =>
    loadPublishedRows({
      categorySlug,
      limit: RECALL_LIMIT,
      orderBy: 'pp.hot_score DESC, pp.id DESC',
    });

  const buildRecallPool = async ({ profile, viewerId, categorySlug }) => {
    const poolMap = new Map();
    const [
      interestRows,
      followRows,
      similarRows,
      tagRows,
      tagSimilarRows,
      freshRows,
      exploreRows,
      hotRows,
    ] = await Promise.all([
      recallInterest(profile, categorySlug),
      recallFollow(profile, categorySlug),
      recallSimilar(profile, viewerId, categorySlug),
      recallByTags(profile, categorySlug),
      recallContentSimilar(profile, categorySlug),
      recallFresh(profile, categorySlug),
      recallExplore(profile, categorySlug),
      recallHotFallback(categorySlug),
    ]);
    mergeRecall(poolMap, interestRows, 'interest');
    mergeRecall(poolMap, followRows, 'follow');
    mergeRecall(poolMap, similarRows, 'similar');
    mergeRecall(poolMap, tagRows, 'tag');
    mergeRecall(poolMap, tagSimilarRows, 'tag_similar');
    mergeRecall(poolMap, freshRows, 'fresh');
    mergeRecall(poolMap, exploreRows, 'explore');
    mergeRecall(poolMap, hotRows, 'hot');

    if (poolMap.size < 24) {
      mergeRecall(
        poolMap,
        await loadPublishedRows({ categorySlug, limit: CANDIDATE_POOL_LIMIT }),
        'hot',
      );
    }
    return poolMap;
  };

  const listRecommendedFeed = async ({
    viewerId = null,
    sessionId = null,
    categorySlug = null,
    cursor = null,
    limit = 20,
  } = {}) => {
    const pageLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
    const halfLifeMs = Number(config.profile_half_life_days ?? 7) * 24 * 60 * 60 * 1000;
    const profile = await eventService.getProfile({
      userId: viewerId,
      sessionId,
      halfLifeMs,
    });
    const parsedCursor = parseRecommendCursor(cursor);
    if (
      parsedCursor.profileVersion != null &&
      parsedCursor.profileVersion !== profile.profileVersion &&
      parsedCursor.offset > 0
    ) {
      parsedCursor.offset = 0;
    }

    const cacheKey = rankCacheKey(viewerId, sessionId, categorySlug, profile.profileVersion);
    const isColdStart = profile.eventCount === 0 && profile.likedPostIds.size === 0;
    let reranked = null;

    if (parsedCursor.offset > 0) {
      const cached = getRankCache(cacheKey);
      if (cached) reranked = cached.orderedItems;
    }

    const recallPool = reranked
      ? null
      : await buildRecallPool({
          profile,
          viewerId,
          categorySlug,
        });
    const now = Date.now();
    const hotConfig = algorithmConfig ?? {};

    if (!reranked) {
      const scored = [...recallPool.values()]
        .map((candidate) => {
          if (!candidate.hot_score) {
            candidate.hot_score = computeHotScore(candidate.row, hotConfig, now);
          }
          return candidate;
        });
      const maxHotScore = Math.max(...scored.map((candidate) => candidate.hot_score), 1);

      let ranked;
      if (isColdStart && !categorySlug) {
        ranked = coldStartInterleave(scored, sessionId || viewerId || 'anon');
      } else {
        ranked = scored
          .map((candidate) => scoreCandidate(candidate, profile, now, maxHotScore, categorySlug))
          .filter((candidate) => candidate.rank_score > -100)
          .sort((a, b) => b.rank_score - a.rank_score);
      }

      reranked = mmrRerank(ranked, {
        lambda: config.mmr_lambda,
        limit: Math.max(ranked.length, parsedCursor.offset + pageLimit + 24),
      });
      setRankCache(cacheKey, reranked);
    }

    const pageItems = reranked.slice(parsedCursor.offset, parsedCursor.offset + pageLimit);
    const hasMore = reranked.length > parsedCursor.offset + pageLimit;

    let reactionMap = new Map();
    if (viewerId && loadViewerReactions) {
      reactionMap = await loadViewerReactions(
        viewerId,
        pageItems.map((item) => item.id),
      );
    }

    const posts = pageItems.map((item) =>
      formatPostRow(item.row, {
        viewerReacted: viewerId ? reactionMap.get(item.id) ?? null : null,
      }),
    );

    return {
      posts,
      featured: { homepage_banner: [], trending: [] },
      next_cursor: hasMore
        ? encodeRecommendCursor({
            offset: parsedCursor.offset + pageLimit,
            profileVersion: profile.profileVersion,
          })
        : null,
      has_more: hasMore,
      recommend_meta: {
        profile_version: profile.profileVersion,
        candidate_count: recallPool?.size ?? reranked.length,
        event_count: profile.eventCount,
        cold_start: isColdStart,
        top_categories: topWeightedKeys(profile.categoryWeights, 3),
        top_tags: topWeightedKeys(profile.tagWeights, 5),
        session_cached: parsedCursor.offset > 0,
      },
    };
  };

  return {
    listRecommendedFeed,
    internals: {
      parseRecommendCursor,
      encodeRecommendCursor,
      scoreCandidate,
      mmrRerank,
      jaccard,
      itemSimilarity,
      DEFAULT_RECOMMEND_CONFIG,
      coldStartInterleave,
      seededShuffle,
      rankCacheKey,
    },
  };
}

export const recommendInternals = {
  parseRecommendCursor,
  encodeRecommendCursor,
  jaccard,
  itemSimilarity,
  mmrRerank,
  freshnessScore,
  qualityScore,
  ctrPrior,
  coldStartInterleave,
  seededShuffle,
  rankCacheKey,
};
