// Selecting a *diverse* set of genres for the music landing page.
//
// The naive approach ("keep the genres with the most albums") tends to surface
// a cluster of near-synonyms — "Rock", "Hard Rock", "Classic Rock", "Pop Rock"
// — because big umbrella genres and their sub-genres are all populous. The
// result reads as one genre repeated, not a tour of the library.
//
// Instead we pick greedily for *spread*: start from the most populous genre,
// then repeatedly add whichever remaining genre is least similar to everything
// already chosen (a max-min / farthest-point selection). Similarity is word-
// token overlap (Jaccard), so "Hard Rock" stays close to "Rock" but far from
// "Jazz" or "Hip Hop". Count still acts as a gentle tie-breaker so we don't
// promote a one-album novelty genre over a healthy distinct one.

/** Anything with a name and a relative weight (album count) we can rank by. */
export interface DiversityCandidate {
  name: string;
}

/**
 * Sample up to `count` items at an even stride across `items`. Genre lists come
 * back alphabetical, so taking the first N would only ever surface A-genres;
 * striding spreads the sample A→Z. Always includes the first item.
 */
export function sampleAcross<T>(items: T[], count: number): T[] {
  if (items.length <= count) return items.slice();
  const stride = Math.max(1, Math.floor(items.length / count));
  return items.filter((_, i) => i % stride === 0).slice(0, count);
}

/** Split a genre name into a set of lowercased word tokens. */
function tokenize(name: string): Set<string> {
  return new Set(
    name
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter(Boolean)
  );
}

/** Jaccard similarity of two token sets: |A∩B| / |A∪B|, in [0, 1]. */
function jaccard(a: Set<string>, b: Set<string>): number {
  if (a.size === 0 && b.size === 0) return 1;
  let intersection = 0;
  for (const t of a) if (b.has(t)) intersection++;
  const union = a.size + b.size - intersection;
  return union === 0 ? 0 : intersection / union;
}

/**
 * Pick up to `limit` genres that are textually distinct from one another,
 * preferring more populous genres. Input order is treated as the count ranking
 * (most albums first); ties in distance fall back to that order.
 */
export function selectDiverseGenres<T extends DiversityCandidate>(
  candidates: T[],
  limit: number
): T[] {
  if (candidates.length <= limit) return candidates.slice();

  const tokens = candidates.map(c => tokenize(c.name));
  const chosen: number[] = [];
  const remaining = new Set(candidates.map((_, i) => i));

  // Seed with the most populous genre (candidates[0]).
  chosen.push(0);
  remaining.delete(0);

  while (chosen.length < limit && remaining.size > 0) {
    let best = -1;
    // The best candidate is the one *least* similar to its most-similar chosen
    // member. We track that max-similarity and minimise it: a genre is only
    // "diverse" if it resembles *nothing* already chosen, so looking at the
    // single nearest neighbour (as plain farthest-point does) isn't enough —
    // "Hard Rock" must stay close to "Rock" even after unrelated "Jazz" is in.
    let bestMaxSim = Infinity;
    for (const i of remaining) {
      let maxSim = 0;
      for (const c of chosen) {
        const sim = jaccard(tokens[i], tokens[c]);
        if (sim > maxSim) maxSim = sim;
      }
      // Lower max-similarity wins; on a tie keep the earlier (more populous)
      // one, guaranteed because `remaining` iterates in insertion order.
      if (maxSim < bestMaxSim) {
        bestMaxSim = maxSim;
        best = i;
      }
    }
    chosen.push(best);
    remaining.delete(best);
  }

  return chosen.map(i => candidates[i]);
}
